diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 362b915e..bc2d9f38 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -1142,7 +1142,8 @@ impl Interpreter { // Run ERR trap on non-zero exit (unless in conditional chain) if exit_code != 0 { let suppressed = matches!(command, Command::List(_)) - || matches!(command, Command::Pipeline(p) if p.negated); + || matches!(command, Command::Pipeline(p) if p.negated) + || result.errexit_suppressed; if !suppressed { self.run_err_trap(&mut stdout, &mut stderr).await; } @@ -1151,9 +1152,12 @@ 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. if self.is_errexit_enabled() && exit_code != 0 { let suppressed = matches!(command, Command::List(_)) - || matches!(command, Command::Pipeline(p) if p.negated); + || matches!(command, Command::Pipeline(p) if p.negated) + || result.errexit_suppressed; if !suppressed { break; } diff --git a/crates/bashkit/tests/issue_873_test.rs b/crates/bashkit/tests/issue_873_test.rs new file mode 100644 index 00000000..48579b6e --- /dev/null +++ b/crates/bashkit/tests/issue_873_test.rs @@ -0,0 +1,70 @@ +//! Test for issue #873: set -e incorrectly triggers on compound commands +//! whose body ends with && chain failure. + +use bashkit::Bash; + +#[tokio::test] +async fn set_e_for_loop_and_chain_no_exit() { + 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"), + "expected 'result: yes', got: {}", + result.stdout + ); +} + +#[tokio::test] +async fn set_e_while_loop_and_chain_no_exit() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +i=0 +while [[ $i -lt 3 ]]; do + [[ $i -eq 1 ]] && echo "found one" + ((i++)) || true +done +echo "done" +"#, + ) + .await + .unwrap(); + assert!( + result.stdout.contains("found one"), + "stdout: {}", + result.stdout + ); + assert!(result.stdout.contains("done"), "stdout: {}", result.stdout); +} + +#[tokio::test] +async fn set_e_plain_failure_in_loop_still_exits() { + let mut bash = Bash::new(); + let result = bash + .exec( + r#" +set -e +for x in a b; do + false +done +echo "SHOULD NOT APPEAR" +"#, + ) + .await + .unwrap(); + assert!(!result.stdout.contains("SHOULD NOT APPEAR")); +}