Skip to content

fix(parser): a case pattern's ) does not end a command substitution - #1255

Open
bresilla wants to merge 1 commit into
reubeno:mainfrom
bresilla:fix/case-in-command-substitution
Open

fix(parser): a case pattern's ) does not end a command substitution#1255
bresilla wants to merge 1 commit into
reubeno:mainfrom
bresilla:fix/case-in-command-substitution

Conversation

@bresilla

@bresilla bresilla commented Jul 31, 2026

Copy link
Copy Markdown

$(case ...) doesn't parse:

$ brush -c 'x=$(case a in a) echo Y;; esac); echo "[$x]"'
error: -c: syntax error at end of input

bash, dash and ksh all print [Y].

A case pattern ends in ), and two places read that as the ) that closes the substitution. consume_nested_construct in tokenizer.rs counts parens, so it stops at $(case a in a. The command() rule in word.rs stops at the first ) for the same reason.

The tokenizer now tracks where it is in a case (case / in / ;; / esac), so a pattern's ) closes the pattern. The ( pattern ) form's leading ( doesn't open anything either. In the word grammar I added a rule that consumes a whole case ... esac before the generic one sees it; it recurses, so nested cases work.

Checked against bash: both pattern forms, several arms, nested case, nested $(), subshells, backticks, $(( )), and a quoted ). All 225 brush-parser tests pass.

Fixes #1052

Copilot AI review requested due to automatic review settings July 31, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes #1052 by distinguishing case-pattern delimiters from command-substitution terminators.

Changes:

  • Adds tokenizer state tracking for case constructs.
  • Adds recursive case ... esac handling in command substitutions.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
brush-parser/src/word.rs Consumes case blocks within substitutions.
brush-parser/src/tokenizer.rs Tracks case parsing state around parentheses.
Suppressed comments (1)

brush-parser/src/tokenizer.rs:672

  • Token spelling does not establish reserved-word context. In the valid $(case x in case) echo Y;; esac) form, the pattern word case pushes AwaitingIn; its following ) is then again mistaken for the substitution terminator. Only a case keyword in command position should push a new state.
                    "case" => case_states.push(CaseState::AwaitingIn),

// substitution, so `$(case a in a) echo Y;; esac)` terminated at `$(case a in a` and the
// rest became a syntax error. A pattern's `)` closes nothing that was opened, so it must
// not decrement the nesting count.
let mut case_states: Vec<CaseState> = vec![];
*case_states.last_mut().unwrap() = CaseState::Pattern;
}
"esac" => {
case_states.pop();
Comment thread brush-parser/src/word.rs
// piece above stops at the first one it sees — so `$(case a in a) echo Y;; esac)` was cut
// short at `case a in a`, leaving the rest as a syntax error. Nested `case`s recurse.
rule case_block() -> () =
"case" (!("case" / "esac") [_])* (case_block() (!("case" / "esac") [_])*)* "esac" {}
Comment thread brush-parser/src/word.rs
$(command_piece()*)

pub(crate) rule command_piece() -> () =
case_block() /
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Performance Benchmark Report

Benchmark name Baseline (μs) Test/PR (μs) Delta (μs) Delta %
clone_shell_object 17.71 μs 17.85 μs 0.14 μs ⚪ Unchanged
eval_arithmetic 0.15 μs 0.15 μs 0.00 μs ⚪ Unchanged
expand_one_string 1.72 μs 1.67 μs -0.05 μs ⚪ Unchanged
for_loop 31.68 μs 32.61 μs 0.93 μs 🟠 +2.94%
full_peg_complex 57.14 μs 56.08 μs -1.06 μs 🟢 -1.86%
full_peg_for_loop 6.21 μs 6.09 μs -0.12 μs 🟢 -1.95%
full_peg_nested_expansions 15.93 μs 15.97 μs 0.04 μs ⚪ Unchanged
full_peg_pipeline 4.18 μs 4.20 μs 0.02 μs ⚪ Unchanged
full_peg_simple 1.78 μs 1.81 μs 0.03 μs 🟠 +1.46%
function_call 3.46 μs 3.53 μs 0.06 μs ⚪ Unchanged
instantiate_shell 55.55 μs 55.11 μs -0.43 μs ⚪ Unchanged
instantiate_shell_with_init_scripts 27202.90 μs 27323.28 μs 120.39 μs ⚪ Unchanged
parse_peg_bash_completion 2097.52 μs 2088.09 μs -9.43 μs ⚪ Unchanged
parse_peg_complex 20.59 μs 20.38 μs -0.21 μs 🟢 -1.02%
parse_peg_for_loop 2.12 μs 2.01 μs -0.11 μs 🟢 -5.24%
parse_peg_pipeline 2.08 μs 2.09 μs 0.00 μs ⚪ Unchanged
parse_peg_simple 1.08 μs 1.13 μs 0.05 μs 🟠 +4.44%
run_echo_builtin_command 16.60 μs 16.15 μs -0.45 μs ⚪ Unchanged
tokenize_sample_script 3.44 μs 3.34 μs -0.10 μs 🟢 -2.79%

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
brush-parser/src/tokenizer.rs 🟢 93.53% 🟢 93.67% 🟢 0.14%
brush-parser/src/word.rs 🟢 93.76% 🟢 93.55% 🔴 -0.21%
Overall Coverage 🟢 76.12% 🟢 76.14% 🟢 0.02%

Minimum allowed coverage is 70%, this run produced 76.14%
Maximum allowed coverage difference is -5%, this run produced 0.02%

Test Summary: bash-completion test suite

Outcome Count Percentage
✅ Pass 1581 74.96
❗️ Error 18 0.85
❌ Fail 156 7.40
⏩ Skip 339 16.07
❎ Expected Fail 13 0.62
✔️ Unexpected Pass 2 0.09
📊 Total 2109 100.00

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

$(case ... in pattern) ...) — ) in case pattern misinterpreted as command substitution terminator

2 participants