Skip to content

Commit d4839ac

Browse files
authored
fix(interpreter): set -e should not trigger on compound commands with && chain failure (#879)
Check result.errexit_suppressed in execute_script_body() so compound commands (for/while/until) whose body ends with an AND-OR chain failure do not incorrectly trigger errexit. Fixes #873
1 parent b372d61 commit d4839ac

File tree

2 files changed

+76
-2
lines changed

2 files changed

+76
-2
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,8 @@ impl Interpreter {
11421142
// Run ERR trap on non-zero exit (unless in conditional chain)
11431143
if exit_code != 0 {
11441144
let suppressed = matches!(command, Command::List(_))
1145-
|| matches!(command, Command::Pipeline(p) if p.negated);
1145+
|| matches!(command, Command::Pipeline(p) if p.negated)
1146+
|| result.errexit_suppressed;
11461147
if !suppressed {
11471148
self.run_err_trap(&mut stdout, &mut stderr).await;
11481149
}
@@ -1151,9 +1152,12 @@ impl Interpreter {
11511152
// errexit (set -e): stop on non-zero exit for top-level simple commands.
11521153
// List commands handle errexit internally (with && / || chain awareness).
11531154
// Negated pipelines (! cmd) explicitly handle the exit code.
1155+
// Compound commands (for/while/until) propagate errexit_suppressed when
1156+
// their body ends with an AND-OR chain failure.
11541157
if self.is_errexit_enabled() && exit_code != 0 {
11551158
let suppressed = matches!(command, Command::List(_))
1156-
|| matches!(command, Command::Pipeline(p) if p.negated);
1159+
|| matches!(command, Command::Pipeline(p) if p.negated)
1160+
|| result.errexit_suppressed;
11571161
if !suppressed {
11581162
break;
11591163
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Test for issue #873: set -e incorrectly triggers on compound commands
2+
//! whose body ends with && chain failure.
3+
4+
use bashkit::Bash;
5+
6+
#[tokio::test]
7+
async fn set_e_for_loop_and_chain_no_exit() {
8+
let mut bash = Bash::new();
9+
let result = bash
10+
.exec(
11+
r#"
12+
set -euo pipefail
13+
result=""
14+
for src in yes no; do
15+
[[ "${src}" == "yes" ]] && result="${src}"
16+
done
17+
echo "result: ${result}"
18+
"#,
19+
)
20+
.await
21+
.unwrap();
22+
assert!(
23+
result.stdout.contains("result: yes"),
24+
"expected 'result: yes', got: {}",
25+
result.stdout
26+
);
27+
}
28+
29+
#[tokio::test]
30+
async fn set_e_while_loop_and_chain_no_exit() {
31+
let mut bash = Bash::new();
32+
let result = bash
33+
.exec(
34+
r#"
35+
set -e
36+
i=0
37+
while [[ $i -lt 3 ]]; do
38+
[[ $i -eq 1 ]] && echo "found one"
39+
((i++)) || true
40+
done
41+
echo "done"
42+
"#,
43+
)
44+
.await
45+
.unwrap();
46+
assert!(
47+
result.stdout.contains("found one"),
48+
"stdout: {}",
49+
result.stdout
50+
);
51+
assert!(result.stdout.contains("done"), "stdout: {}", result.stdout);
52+
}
53+
54+
#[tokio::test]
55+
async fn set_e_plain_failure_in_loop_still_exits() {
56+
let mut bash = Bash::new();
57+
let result = bash
58+
.exec(
59+
r#"
60+
set -e
61+
for x in a b; do
62+
false
63+
done
64+
echo "SHOULD NOT APPEAR"
65+
"#,
66+
)
67+
.await
68+
.unwrap();
69+
assert!(!result.stdout.contains("SHOULD NOT APPEAR"));
70+
}

0 commit comments

Comments
 (0)