Skip to content

Single-bracket test commands are merged across redirected statements #316

Open

Description

@wrefactor

If two single-bracket test commands appear above and below a series of statements containing a redirection, both tests and all statements between will be interpreted as one large redirected_statement inside a single test_command. I believe this is a minimal example:

[ 2 -lt 3 ]
echo >1
[ ]

which is (roughly) parsed as:

test_command(command("2 -lt 3 ]\necho"), file_redirect(">1\n["))
Full parse tree from playground

From the playground, but also seen on tree-sitter-bash 0.25.1 / tree-sitter 0.25.2

program [0, 0] - [4, 0]
  test_command [0, 0] - [2, 3]
    "[" [0, 0] - [0, 1]
    redirected_statement [0, 2] - [2, 1]
      body: command [0, 2] - [1, 4]
        name: command_name [0, 2] - [0, 3]
          number [0, 2] - [0, 3]
        argument: word [0, 4] - [0, 7]
        argument: number [0, 8] - [0, 9]
        argument: word [0, 10] - [0, 11]
        argument: word [1, 0] - [1, 4]
      redirect: file_redirect [1, 5] - [2, 1]
        ">" [1, 5] - [1, 6]
        destination: number [1, 6] - [1, 7]
        destination: word [2, 0] - [2, 1]
    "]" [2, 2] - [2, 3]

The problem still occurs when:

  • adding more statements above/below the echo line (these will all be swallowed up by the redirected_statement)
  • using other operand types (tested numbers "strings", $variables)
  • using other operators (tested ==, -eq, -ne, -lt, -gt, -ge, -le)
  • prefixing with !; adding terms with && or ||; encloding in the ternary operator, e.g. 1 == 2 ? 3 : 4
  • using other redirections (tested >, <, &>, >&', <&, >>`)
  • different commands (instead of echo), different destinations (tested descriptors 0, 1, 2, and names bar)
  • modifying the second test without any of the above restrictions

The problem does not occur when the first test:

  • is empty, a single word, a unary operator (e.g. [ ], [ 0 ], [ -z "" ]), or their negation (e.g. [ ! 0 ], or [ ! -z "" ])
  • contains a parenthesised sub-expression
  • contains any symbolic (i.e. not -[a-z][a-z]) binary operator, except ==, && or ||
  • contains a parenthesised sub-expression
    or when the > redirection is replaced with a heredoc/herestring (<<< or <<)

Hope that's useful, and thanks again!

Activity

  1. marcospgp commented on Sep 16, 2026

    @marcospgp

    The same merge happens after a pipeline of 3 or more commands, with no test command involved. A line carrying any redirect right after the pipeline is joined onto the pipeline's last command, and the tree has no ERROR node.

    a | b | c
    d > f

    parses as

    (program (pipeline (command name: (command_name (word))) (redirected_statement body: (pipeline (command name: (command_name (word))) (command name: (command_name (word)) argument: (word))) redirect: (file_redirect destination: (word)))))
    

    so d becomes an argument of c. A realistic case:

    git log | grep fix | head -4
    echo done > out.txt

    gives head the arguments -4, echo and done, while bash runs echo done > out.txt as its own command.

    These parse correctly:

    • A pipeline of 2 commands, a | b then d > f.
    • A ; at the end of the pipeline's line.

    A blank line between the two lines does not help.

    Seen on 0.25.1 through web-tree-sitter 0.27.0. We hit it replaying 8187 shell commands through a guard that reads the tree: 5 of them were affected, each hiding the next line's command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions