fix: scanner-assisted mid-command redirect detection - #333
Conversation
Redirects between arguments (e.g. grep 2>/dev/null -q pattern file) previously caused the parser to misattach subsequent arguments as redirect destinations, because file_redirect uses repeat1 for its destination field. This patch uses the external scanner to peek ahead past redirect operators and their destinations. If more non-redirect words follow, the scanner emits a mid-command token that keeps the redirect inside command's repeat. If no words follow, the redirect is trailing and uses redirected_statement as before. Covers all redirect operators: > >> < 2> 2>&1 >& <& >| &> &>> >&- <&-. Handles chained redirects, process substitution destinations, herestrings, and close-fd operators. Also adds redirect support inside [ ] test brackets (e.g. [ -f file 2>/dev/null ]). The mid-command redirect rules are aliased to file_redirect, so node-types.json is unchanged. Trailing redirects still produce redirected_statement exactly as before. The one known tree shape change is for trailing redirects inside backtick command substitutions (e.g. echo `cmd >file` arg), where the redirect moves from redirected_statement to a direct child of command. Backticks use the same symbol to open and close, making it impractical for the scanner's lookahead to determine whether a backtick terminates the current context. This is safe for consumers because command already accepts redirect children for the pre-name case (e.g. 2>&1 cmd), so all consumers must already handle redirects on command nodes. Related: tree-sitter#233, tree-sitter#331 PR tree-sitter#331 takes a different approach: changing file_redirect to accept a single destination and adding argument fields to redirected_statement. That is a simpler grammar change but a breaking change to node-types.json that requires consumers to handle the new argument field. This patch preserves the existing tree structure for trailing redirects and only changes behavior for inputs that previously produced incorrect trees.
|
Maintaining compatibility with the existing parse shapes did require some ... rather intense scanner hackery. I'm not upset if this PR is not taken, I just want it to be an option because right now the parse deviates badly from bash's actual behavior with mid-command and mid-test redirects. PR #331 is a much simpler fix that gets you about 80% of the way there. Once I cracked the scanner open, I couldn't stop myself from dealing with various incorrect parses of "fd-close" and other redirection bugs that I was going to have to touch on anyway. I did hit a lot of birds with this stone. But, it is a very ugly and complex stone. Happy to remove some of the inline scanner comments if you think they break up the code too much (eg, the enum definitions). If retaining tree shape isn't a concern and we are willing to just force consumers to accept interleaved redirects on all commands and test_commands, I think the line count of the patch can be cut roughly in half. But that also feels like a breaking change that might hurt consumers of this project. |
Problem
Redirects between arguments (e.g.
grep 2>/dev/null -q pattern file) produce incorrect parse trees. Thefile_redirectrule usesrepeat1($._literal)for its destination, so it greedily consumes all subsequent words as redirect destinations instead of leaving them as command arguments.Approach
The external scanner peeks ahead past redirect operators and their destinations. If more non-redirect words follow, the scanner emits a mid-command token that keeps the redirect inside
command's repeat. If no words follow, the redirect is trailing and usesredirected_statementas before.Two new grammar rules (
_mid_command_file_redirectand_mid_command_plain_redirect) are aliased tofile_redirect, sonode-types.jsonis unchanged. Consumers that already handleredirectfields oncommandnodes -- which is required for pre-command redirects like2>&1 cmd-- need no changes.What's fixed
All redirect operators as mid-command:
>>><2>2>&1>&<&>|&>&>>>&-<&-. Chained redirects (cmd >a >b arg), process substitution destinations (cmd > >(tee log) arg), herestrings (cmd <<< "input" arg), and close-fd operators (cmd >&- arg). Also adds redirect support inside[ ]test brackets ([ -f file 2>/dev/null ]).Compatibility
Trailing redirects still produce
redirected_statementexactly as before. The one known tree shape change is for trailing redirects inside backtick command substitutions (e.g.echo `cmd >file` arg), where the redirect moves fromredirected_statementto a direct child ofcommand. Backticks use the same symbol to open and close, making it impractical for the scanner's lookahead to determine whether a backtick terminates the current context. This is safe for consumers becausecommandalready acceptsredirectchildren for the pre-name case (e.g.2>&1 cmd), so all consumers must already handle redirects oncommandnodes.Known limitations (pre-existing, same as master)
-a/-oin test expressions:[ -f file 2>/dev/null -a -d dir ][ 2>/dev/null -f file ]Both are rare in practice. They are legal because
[is a command like any other and the shell strips redirects before[sees its arguments. Interior redirects are not legal on[[ ]]constructs, only[ ].Related
repeat1as root causefile_redirectto single destination and addsargumentfields toredirected_statement. That is a simpler grammar change but a breaking change tonode-types.jsonrequiring consumers to handle the newargumentfield. This PR preserves the existing tree structure for trailing redirects and only changes behavior for inputs that previously produced incorrect trees.