Skip to content

Commit 30537e2

Browse files
chaliyclaude
andauthored
fix(interpreter): shift builtin now updates positional parameters (#296)
## Summary - `shift` builtin set `_SHIFT_COUNT` marker variable but interpreter never consumed it - Added post-processing after builtin execution to drain positional parameters from call frame - Fixes infinite loop in `while [[ $# -gt 0 ]]; do case ... shift 2 ... esac; done` pattern ## Test plan - [x] `issue_290_while_case_shift_loop` — while/case with `shift 2` terminates correctly - [x] `issue_290_shift_1_default` — default `shift` (shift 1) works Closes #290 Co-authored-by: Claude <noreply@anthropic.com>
1 parent ba6feef commit 30537e2

File tree

2 files changed

+50
-0
lines changed

2 files changed

+50
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3401,6 +3401,18 @@ impl Interpreter {
34013401
self.variables.remove(&marker);
34023402
}
34033403

3404+
// Post-process: shift builtin updates positional parameters
3405+
if let Some(shift_str) = self.variables.remove("_SHIFT_COUNT") {
3406+
let n: usize = shift_str.parse().unwrap_or(1);
3407+
if let Some(frame) = self.call_stack.last_mut() {
3408+
if n <= frame.positional.len() {
3409+
frame.positional.drain(..n);
3410+
} else {
3411+
frame.positional.clear();
3412+
}
3413+
}
3414+
}
3415+
34043416
// Handle output redirections
34053417
return self.apply_redirections(result, &command.redirects).await;
34063418
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//! Regression test for #290: while/case arg parsing hits MaxLoopIterations
2+
3+
use bashkit::Bash;
4+
use std::path::Path;
5+
6+
#[tokio::test]
7+
async fn issue_290_while_case_shift_loop() {
8+
let mut bash = Bash::new();
9+
let fs = bash.fs();
10+
fs.write_file(
11+
Path::new("/parse_args.sh"),
12+
b"#!/bin/bash\nset -e\n\nwhile [[ $# -gt 0 ]]; do\n case $1 in\n --name)\n NAME=\"$2\"\n shift 2\n ;;\n --value)\n VALUE=\"$2\"\n shift 2\n ;;\n *)\n echo \"Unknown: $1\"\n exit 1\n ;;\n esac\ndone\n\necho \"name=$NAME value=$VALUE\"",
13+
)
14+
.await
15+
.unwrap();
16+
fs.chmod(Path::new("/parse_args.sh"), 0o755).await.unwrap();
17+
let r = bash
18+
.exec("/parse_args.sh --name foo --value bar")
19+
.await
20+
.unwrap();
21+
assert_eq!(r.stdout.trim(), "name=foo value=bar");
22+
assert_eq!(r.exit_code, 0);
23+
}
24+
25+
#[tokio::test]
26+
async fn issue_290_shift_1_default() {
27+
let mut bash = Bash::new();
28+
let fs = bash.fs();
29+
fs.write_file(
30+
Path::new("/shift1.sh"),
31+
b"#!/bin/bash\nwhile [[ $# -gt 0 ]]; do\n echo \"$1\"\n shift\ndone",
32+
)
33+
.await
34+
.unwrap();
35+
fs.chmod(Path::new("/shift1.sh"), 0o755).await.unwrap();
36+
let r = bash.exec("/shift1.sh a b c").await.unwrap();
37+
assert_eq!(r.stdout.trim(), "a\nb\nc");
38+
}

0 commit comments

Comments
 (0)