Skip to content

Commit d9db292

Browse files
authored
fix(parser): track bracket/brace depth in array subscript reader (#603)
## Summary - Fix the parser's array subscript reader to track `[]` bracket depth and `${}` brace depth - Without this, `${arr[$RANDOM % ${#arr[@]}]}` had subscripts truncated at the `]` inside `${#arr[@]}` - Added parser unit tests for nested subscript parsing and assignment parsing Depends on #602 (lexer fix). ## Test plan - [x] Parser unit test: `test_nested_expansion_in_array_subscript` verifies correct AST - [x] Parser unit test: `test_assignment_nested_subscript_parses` verifies no fuel exhaustion - [x] Full test suite passes (0 new failures) Closes #600
1 parent 5c2729c commit d9db292

File tree

1 file changed

+66
-1
lines changed
  • crates/bashkit/src/parser

1 file changed

+66
-1
lines changed

crates/bashkit/src/parser/mod.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2388,11 +2388,36 @@ impl<'a> Parser<'a> {
23882388
if chars.peek() == Some(&'[') {
23892389
chars.next(); // consume '['
23902390
let mut index = String::new();
2391+
// Track nesting so nested ${...} containing
2392+
// brackets (e.g. ${#arr[@]}) don't prematurely
2393+
// close the subscript.
2394+
let mut bracket_depth: i32 = 0;
2395+
let mut brace_depth: i32 = 0;
23912396
while let Some(&c) = chars.peek() {
2392-
if c == ']' {
2397+
if c == ']' && bracket_depth == 0 && brace_depth == 0 {
23932398
chars.next();
23942399
break;
23952400
}
2401+
match c {
2402+
'[' => bracket_depth += 1,
2403+
']' => bracket_depth -= 1,
2404+
'$' => {
2405+
index.push(chars.next().unwrap());
2406+
if chars.peek() == Some(&'{') {
2407+
brace_depth += 1;
2408+
index.push(chars.next().unwrap());
2409+
continue;
2410+
}
2411+
continue;
2412+
}
2413+
'{' => brace_depth += 1,
2414+
'}' => {
2415+
if brace_depth > 0 {
2416+
brace_depth -= 1;
2417+
}
2418+
}
2419+
_ => {}
2420+
}
23962421
index.push(chars.next().unwrap());
23972422
}
23982423
// Strip surrounding quotes from index (e.g. "foo" -> foo)
@@ -3045,4 +3070,44 @@ mod tests {
30453070
"non-empty while body should be accepted"
30463071
);
30473072
}
3073+
3074+
/// Issue #600: Subscript reader must handle nested ${...} containing brackets.
3075+
#[test]
3076+
fn test_nested_expansion_in_array_subscript() {
3077+
// ${arr[$RANDOM % ${#arr[@]}]} must parse without error.
3078+
// The subscript contains ${#arr[@]} which has its own [ and ].
3079+
let parser = Parser::new("echo ${arr[$RANDOM % ${#arr[@]}]}");
3080+
let script = parser.parse().unwrap();
3081+
assert_eq!(script.commands.len(), 1);
3082+
if let Command::Simple(cmd) = &script.commands[0] {
3083+
assert_eq!(cmd.name.to_string(), "echo");
3084+
assert_eq!(cmd.args.len(), 1);
3085+
// The arg should contain an ArrayAccess with the full nested index
3086+
let arg = &cmd.args[0];
3087+
let has_array_access = arg.parts.iter().any(|p| {
3088+
matches!(
3089+
p,
3090+
WordPart::ArrayAccess { name, index }
3091+
if name == "arr" && index.contains("${#arr[@]}")
3092+
)
3093+
});
3094+
assert!(
3095+
has_array_access,
3096+
"expected ArrayAccess with nested index, got: {:?}",
3097+
arg.parts
3098+
);
3099+
} else {
3100+
panic!("expected simple command");
3101+
}
3102+
}
3103+
3104+
/// Assignment with nested subscript must parse (previously caused fuel exhaustion).
3105+
#[test]
3106+
fn test_assignment_nested_subscript_parses() {
3107+
let parser = Parser::new("x=${arr[$RANDOM % ${#arr[@]}]}");
3108+
assert!(
3109+
parser.parse().is_ok(),
3110+
"assignment with nested subscript should parse"
3111+
);
3112+
}
30483113
}

0 commit comments

Comments
 (0)