Skip to content

Commit eb8c15c

Browse files
committed
fix(interpreter): reset OPTIND between bash script invocations
OPTIND was persisting in interpreter variables across execute_shell calls, causing getopts to skip all options on subsequent script runs. Now save/restore OPTIND and _OPTCHAR_IDX around script execution, matching real bash behavior where each `bash script.sh` starts fresh. Closes #397
1 parent a943566 commit eb8c15c

File tree

1 file changed

+119
-0
lines changed
  • crates/bashkit/src/interpreter

1 file changed

+119
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2786,16 +2786,33 @@ impl Interpreter {
27862786
});
27872787

27882788
// Save and apply shell options (-e, -x, -u, -o pipefail, etc.)
2789+
// Also save/restore OPTIND so getopts state doesn't leak between scripts
27892790
let mut saved_opts: Vec<(String, Option<String>)> = Vec::new();
27902791
for (var, val) in &shell_opts {
27912792
let prev = self.variables.get(*var).cloned();
27922793
saved_opts.push((var.to_string(), prev));
27932794
self.variables.insert(var.to_string(), val.to_string());
27942795
}
2796+
let saved_optind = self.variables.get("OPTIND").cloned();
2797+
let saved_optchar = self.variables.get("_OPTCHAR_IDX").cloned();
2798+
self.variables.insert("OPTIND".to_string(), "1".to_string());
2799+
self.variables.remove("_OPTCHAR_IDX");
27952800

27962801
// Execute the script
27972802
let result = self.execute(&script).await;
27982803

2804+
// Restore OPTIND and internal getopts state
2805+
if let Some(val) = saved_optind {
2806+
self.variables.insert("OPTIND".to_string(), val);
2807+
} else {
2808+
self.variables.remove("OPTIND");
2809+
}
2810+
if let Some(val) = saved_optchar {
2811+
self.variables.insert("_OPTCHAR_IDX".to_string(), val);
2812+
} else {
2813+
self.variables.remove("_OPTCHAR_IDX");
2814+
}
2815+
27992816
// Restore shell options
28002817
for (var, prev) in saved_opts {
28012818
if let Some(val) = prev {
@@ -9561,4 +9578,106 @@ mod tests {
95619578
// With the fix, the cycle is detected and 'a' resolves to itself.
95629579
assert_eq!(result.exit_code, 0);
95639580
}
9581+
9582+
#[tokio::test]
9583+
async fn test_getopts_while_loop() {
9584+
// Issue #397: getopts in while loop should iterate over all options
9585+
let mut bash = crate::Bash::new();
9586+
let result = bash
9587+
.exec(
9588+
r#"
9589+
set -- -f json -v
9590+
while getopts "f:vh" opt; do
9591+
case "$opt" in
9592+
f) FORMAT="$OPTARG" ;;
9593+
v) VERBOSE=1 ;;
9594+
esac
9595+
done
9596+
echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9597+
"#,
9598+
)
9599+
.await
9600+
.unwrap();
9601+
assert_eq!(result.exit_code, 0);
9602+
assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9603+
}
9604+
9605+
#[tokio::test]
9606+
async fn test_getopts_script_with_args() {
9607+
// Issue #397: getopts via bash -c with script args
9608+
let mut bash = crate::Bash::new();
9609+
// Write a script that uses getopts, then invoke it with arguments
9610+
let result = bash
9611+
.exec(
9612+
r#"
9613+
cat > /tmp/test_getopts.sh << 'SCRIPT'
9614+
while getopts "f:vh" opt; do
9615+
case "$opt" in
9616+
f) FORMAT="$OPTARG" ;;
9617+
v) VERBOSE=1 ;;
9618+
esac
9619+
done
9620+
echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9621+
SCRIPT
9622+
bash /tmp/test_getopts.sh -f json -v
9623+
"#,
9624+
)
9625+
.await
9626+
.unwrap();
9627+
assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9628+
}
9629+
9630+
#[tokio::test]
9631+
async fn test_getopts_bash_c_with_args() {
9632+
// Issue #397: getopts via bash -c 'script' -- args
9633+
let mut bash = crate::Bash::new();
9634+
let result = bash
9635+
.exec(
9636+
r#"bash -c '
9637+
FORMAT="csv"
9638+
VERBOSE=0
9639+
while getopts "f:vh" opt; do
9640+
case "$opt" in
9641+
f) FORMAT="$OPTARG" ;;
9642+
v) VERBOSE=1 ;;
9643+
esac
9644+
done
9645+
echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9646+
' -- -f json -v"#,
9647+
)
9648+
.await
9649+
.unwrap();
9650+
assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9651+
}
9652+
9653+
#[tokio::test]
9654+
async fn test_getopts_optind_reset_between_scripts() {
9655+
// Issue #397: OPTIND persists across bash script invocations, causing
9656+
// getopts to skip all options on the second run
9657+
let mut bash = crate::Bash::new();
9658+
let result = bash
9659+
.exec(
9660+
r#"
9661+
cat > /tmp/opts.sh << 'SCRIPT'
9662+
FORMAT="csv"
9663+
VERBOSE=0
9664+
while getopts "f:vh" opt; do
9665+
case "$opt" in
9666+
f) FORMAT="$OPTARG" ;;
9667+
v) VERBOSE=1 ;;
9668+
esac
9669+
done
9670+
echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9671+
SCRIPT
9672+
bash /tmp/opts.sh -f json -v
9673+
bash /tmp/opts.sh -f xml -v
9674+
"#,
9675+
)
9676+
.await
9677+
.unwrap();
9678+
let lines: Vec<&str> = result.stdout.trim().lines().collect();
9679+
assert_eq!(lines.len(), 2, "expected 2 lines: {}", result.stdout);
9680+
assert_eq!(lines[0], "FORMAT=json VERBOSE=1");
9681+
assert_eq!(lines[1], "FORMAT=xml VERBOSE=1");
9682+
}
95649683
}

0 commit comments

Comments
 (0)