Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = if let Some(words) = &for_cmd.words {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1564,6 +1566,7 @@ impl Interpreter {
stderr,
exit_code,
control_flow: ControlFlow::None,
errexit_suppressed: last_errexit_suppressed,
..Default::default()
})
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -1829,6 +1834,7 @@ impl Interpreter {
stderr,
exit_code,
control_flow: ControlFlow::None,
errexit_suppressed: last_errexit_suppressed,
..Default::default()
})
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -2264,6 +2272,7 @@ impl Interpreter {
stderr,
exit_code,
control_flow: ControlFlow::None,
errexit_suppressed: last_errexit_suppressed,
..Default::default()
})
}
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -2820,13 +2834,15 @@ impl Interpreter {
..Default::default()
});
}
last_errexit_suppressed = suppress && exit_code != 0;
}

Ok(ExecResult {
stdout,
stderr,
exit_code,
control_flow: ControlFlow::None,
errexit_suppressed: last_errexit_suppressed,
..Default::default()
})
}
Expand Down Expand Up @@ -3099,6 +3115,7 @@ impl Interpreter {
stderr,
exit_code,
control_flow: ControlFlow::None,
errexit_suppressed: has_conditional_operators && exit_code != 0,
..Default::default()
})
}
Expand Down
4 changes: 4 additions & 0 deletions crates/bashkit/src/interpreter/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BuiltinSideEffect>,
/// 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 {
Expand Down
102 changes: 102 additions & 0 deletions crates/bashkit/tests/set_e_and_or_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
28 changes: 28 additions & 0 deletions supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading