diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index b17c8daf..aca72bdb 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1457,6 +1457,7 @@ impl Interpreter { let mut stdout = String::new(); let mut stderr = String::new(); let mut exit_code = 0; + let mut last_errexit_suppressed = false; // Get iteration values: expand fields, then apply brace/glob expansion let values: Vec = if let Some(words) = &for_cmd.words { @@ -1512,6 +1513,7 @@ impl Interpreter { stdout.push_str(&result.stdout); stderr.push_str(&result.stderr); exit_code = result.exit_code; + last_errexit_suppressed = result.errexit_suppressed; // Check for break/continue match result.control_flow { @@ -1564,6 +1566,7 @@ impl Interpreter { stderr, exit_code, control_flow: ControlFlow::None, + errexit_suppressed: last_errexit_suppressed, ..Default::default() }) } @@ -1748,6 +1751,7 @@ impl Interpreter { let mut stdout = String::new(); let mut stderr = String::new(); let mut exit_code = 0; + let mut last_errexit_suppressed = false; // Execute initialization if !arith_for.init.is_empty() { @@ -1776,6 +1780,7 @@ impl Interpreter { stdout.push_str(&result.stdout); stderr.push_str(&result.stderr); exit_code = result.exit_code; + last_errexit_suppressed = result.errexit_suppressed; // Check for break/continue match result.control_flow { @@ -1829,6 +1834,7 @@ impl Interpreter { stderr, exit_code, control_flow: ControlFlow::None, + errexit_suppressed: last_errexit_suppressed, ..Default::default() }) } @@ -2180,6 +2186,7 @@ impl Interpreter { let mut stdout = String::new(); let mut stderr = String::new(); let mut exit_code = 0; + let mut last_errexit_suppressed = false; // Reset loop counter for this loop self.counters.reset_loop(); @@ -2215,6 +2222,7 @@ impl Interpreter { stdout.push_str(&result.stdout); stderr.push_str(&result.stderr); exit_code = result.exit_code; + last_errexit_suppressed = result.errexit_suppressed; // Check for break/continue match result.control_flow { @@ -2264,6 +2272,7 @@ impl Interpreter { stderr, exit_code, control_flow: ControlFlow::None, + errexit_suppressed: last_errexit_suppressed, ..Default::default() }) } @@ -2782,6 +2791,7 @@ impl Interpreter { let mut stdout = String::new(); let mut stderr = String::new(); let mut exit_code = 0; + let mut last_errexit_suppressed = false; for command in commands { let emit_before = self.output_emit_count; @@ -2807,11 +2817,15 @@ impl Interpreter { // Skip errexit for commands that are AND-OR lists — per POSIX, set -e // does not exit on failures that are part of && or || chains. // The list executor already handles errexit internally. + // Also skip when the result has errexit_suppressed set — this means + // a compound command (loop, etc.) ended with an AND-OR list exit code + // that should not propagate errexit to the caller. let is_and_or_list = matches!( command, Command::List(list) if list.rest.iter().any(|(op, _)| matches!(op, ListOperator::And | ListOperator::Or)) ); - if check_errexit && self.is_errexit_enabled() && exit_code != 0 && !is_and_or_list { + let suppress = is_and_or_list || result.errexit_suppressed; + if check_errexit && self.is_errexit_enabled() && exit_code != 0 && !suppress { return Ok(ExecResult { stdout, stderr, @@ -2820,6 +2834,7 @@ impl Interpreter { ..Default::default() }); } + last_errexit_suppressed = suppress && exit_code != 0; } Ok(ExecResult { @@ -2827,6 +2842,7 @@ impl Interpreter { stderr, exit_code, control_flow: ControlFlow::None, + errexit_suppressed: last_errexit_suppressed, ..Default::default() }) } @@ -3099,6 +3115,7 @@ impl Interpreter { stderr, exit_code, control_flow: ControlFlow::None, + errexit_suppressed: has_conditional_operators && exit_code != 0, ..Default::default() }) } diff --git a/crates/bashkit/src/interpreter/state.rs b/crates/bashkit/src/interpreter/state.rs index 5a6ee97d..687ab458 100644 --- a/crates/bashkit/src/interpreter/state.rs +++ b/crates/bashkit/src/interpreter/state.rs @@ -66,6 +66,10 @@ pub struct ExecResult { /// Structured side effects from builtin execution. /// The interpreter processes these after the builtin returns. pub side_effects: Vec, + /// When true, the non-zero exit code came from an AND-OR list (e.g. `false && true`) + /// and should NOT trigger `set -e` / errexit at the caller level. + /// Propagated through compound commands so nested loops don't re-trigger errexit. + pub errexit_suppressed: bool, } impl ExecResult { diff --git a/crates/bashkit/tests/set_e_and_or_tests.rs b/crates/bashkit/tests/set_e_and_or_tests.rs index da105c19..0ab415e3 100644 --- a/crates/bashkit/tests/set_e_and_or_tests.rs +++ b/crates/bashkit/tests/set_e_and_or_tests.rs @@ -89,6 +89,108 @@ echo "reached" assert!(result.stdout.contains("reached")); } +/// set -e: [[ false ]] && cmd inside nested C-style for loops (issue #867) +#[tokio::test] +async fn set_e_and_list_in_nested_cfor() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +f() { + for ((i=0; i<2; i++)); do + for ((j=0; j<2; j++)); do + [[ $i -ne $j ]] && echo "$i != $j" + done + done + echo "done" +} +f +echo "after f" +"#, + ) + .await + .unwrap(); + assert!(result.stdout.contains("0 != 1"), "should print 0 != 1"); + assert!(result.stdout.contains("1 != 0"), "should print 1 != 0"); + assert!(result.stdout.contains("done"), "should reach done"); + assert!(result.stdout.contains("after f"), "should reach after f"); +} + +/// set -e: nested for-in loops with AND-OR list +#[tokio::test] +async fn set_e_and_list_in_nested_for_in() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +f() { + for i in a b; do + for j in a b; do + [[ "$i" != "$j" ]] && echo "$i != $j" + done + done + echo "done" +} +f +"#, + ) + .await + .unwrap(); + assert!(result.stdout.contains("a != b")); + assert!(result.stdout.contains("b != a")); + assert!(result.stdout.contains("done")); +} + +/// set -e: nested while loop with AND-OR list +#[tokio::test] +async fn set_e_and_list_in_nested_while() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +i=0 +while [[ $i -lt 2 ]]; do + j=0 + while [[ $j -lt 2 ]]; do + [[ $i -ne $j ]] && echo "$i != $j" + ((j++)) || true + done + ((i++)) || true +done +echo "done" +"#, + ) + .await + .unwrap(); + assert!(result.stdout.contains("0 != 1")); + assert!(result.stdout.contains("1 != 0")); + assert!(result.stdout.contains("done")); +} + +/// set -e should still exit on plain failure inside nested loops +#[tokio::test] +async fn set_e_exits_on_plain_failure_in_nested_loop() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +for ((i=0; i<2; i++)); do + for ((j=0; j<2; j++)); do + false + done +done +echo "SHOULD NOT APPEAR" +"#, + ) + .await + .unwrap(); + assert!(!result.stdout.contains("SHOULD NOT APPEAR")); +} + /// set -e should still exit on non-AND-OR failures #[tokio::test] async fn set_e_exits_on_plain_failure() { diff --git a/supply-chain/config.toml b/supply-chain/config.toml index 4caaa71c..102f8041 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -162,6 +162,10 @@ criteria = "safe-to-deploy" version = "1.2.57" criteria = "safe-to-deploy" +[[exemptions.cc]] +version = "1.2.58" +criteria = "safe-to-deploy" + [[exemptions.cesu8]] version = "1.1.0" criteria = "safe-to-deploy" @@ -206,6 +210,10 @@ criteria = "safe-to-deploy" version = "1.1.0" criteria = "safe-to-deploy" +[[exemptions.cmake]] +version = "0.1.57" +criteria = "safe-to-deploy" + [[exemptions.cmake]] version = "0.1.58" criteria = "safe-to-deploy" @@ -234,6 +242,10 @@ criteria = "safe-to-deploy" version = "0.9.0" criteria = "safe-to-deploy" +[[exemptions.console]] +version = "0.15.11" +criteria = "safe-to-run" + [[exemptions.console]] version = "0.16.3" criteria = "safe-to-run" @@ -570,6 +582,10 @@ criteria = "safe-to-deploy" version = "2.13.0" criteria = "safe-to-deploy" +[[exemptions.insta]] +version = "1.46.3" +criteria = "safe-to-run" + [[exemptions.insta]] version = "1.47.0" criteria = "safe-to-run" @@ -718,6 +734,10 @@ criteria = "safe-to-deploy" version = "1.1.1" criteria = "safe-to-deploy" +[[exemptions.mio]] +version = "1.2.0" +criteria = "safe-to-deploy" + [[exemptions.nalgebra]] version = "0.33.2" criteria = "safe-to-deploy" @@ -1174,6 +1194,10 @@ criteria = "safe-to-deploy" version = "0.9.1" criteria = "safe-to-deploy" +[[exemptions.simd-adler32]] +version = "0.3.8" +criteria = "safe-to-deploy" + [[exemptions.simd-adler32]] version = "0.3.9" criteria = "safe-to-deploy" @@ -1370,6 +1394,10 @@ criteria = "safe-to-deploy" version = "0.1.25" criteria = "safe-to-deploy" +[[exemptions.unicode-segmentation]] +version = "1.13.1" +criteria = "safe-to-deploy" + [[exemptions.unicode-segmentation]] version = "1.13.2" criteria = "safe-to-deploy"