diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 937a3cf1..df6ca06d 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1152,8 +1152,7 @@ impl Interpreter { // errexit (set -e): stop on non-zero exit for top-level simple commands. // List commands handle errexit internally (with && / || chain awareness). // Negated pipelines (! cmd) explicitly handle the exit code. - // Compound commands (for/while/until) propagate errexit_suppressed when - // their body ends with an AND-OR chain failure. + // Compound commands propagate errexit_suppressed from inner AND-OR chains. if self.is_errexit_enabled() && exit_code != 0 { let suppressed = matches!(command, Command::List(_)) || matches!(command, Command::Pipeline(p) if p.negated) diff --git a/crates/bashkit/tests/set_e_and_or_tests.rs b/crates/bashkit/tests/set_e_and_or_tests.rs index 0ab415e3..c6a877b9 100644 --- a/crates/bashkit/tests/set_e_and_or_tests.rs +++ b/crates/bashkit/tests/set_e_and_or_tests.rs @@ -191,6 +191,30 @@ echo "SHOULD NOT APPEAR" assert!(!result.stdout.contains("SHOULD NOT APPEAR")); } +/// set -e: && chain failure at end of for loop body should NOT exit (issue #873) +#[tokio::test] +async fn set_e_and_chain_at_end_of_for_body() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -euo pipefail +result="" +for src in yes no; do + [[ "${src}" == "yes" ]] && result="${src}" +done +echo "result: ${result}" +"#, + ) + .await + .unwrap(); + assert!( + result.stdout.contains("result: yes"), + "should print 'result: yes' but got: {:?}", + result.stdout + ); +} + /// set -e should still exit on non-AND-OR failures #[tokio::test] async fn set_e_exits_on_plain_failure() {