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
8 changes: 5 additions & 3 deletions crates/bashkit/src/builtins/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ impl Builtin for Exit {
.unwrap_or(0)
& 0xFF;

// For now, we just return the exit code
// In a full implementation, this would terminate the shell
Ok(ExecResult::err(String::new(), exit_code))
Ok(ExecResult {
exit_code,
control_flow: ControlFlow::Exit(exit_code),
..Default::default()
})
}
}

Expand Down
22 changes: 22 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,16 @@ impl Interpreter {
self.last_exit_code = saved_exit;
self.aliases = saved_aliases;
self.coproc_buffers = saved_coproc;

// Consume Exit control flow at subshell boundary — exit only
// terminates the subshell, not the parent shell.
if let Ok(ref mut res) = result
&& let ControlFlow::Exit(code) = res.control_flow
{
res.exit_code = code;
res.control_flow = ControlFlow::None;
}

result
}
CompoundCommand::BraceGroup(commands) => self.execute_command_sequence(commands).await,
Expand Down Expand Up @@ -1680,6 +1690,15 @@ impl Interpreter {
..Default::default()
});
}
ControlFlow::Exit(code) => {
return Ok(ExecResult {
stdout,
stderr,
exit_code: code,
control_flow: ControlFlow::Exit(code),
..Default::default()
});
}
ControlFlow::None => {}
}
}
Expand Down Expand Up @@ -5880,6 +5899,9 @@ impl Interpreter {
let cmd_result = self.execute_command(cmd).await?;
stdout.push_str(&cmd_result.stdout);
self.last_exit_code = cmd_result.exit_code;
if matches!(cmd_result.control_flow, ControlFlow::Exit(_)) {
break;
}
}
// Fire EXIT trap set inside the command substitution
if let Some(trap_cmd) = self.traps.get("EXIT").cloned()
Expand Down
5 changes: 4 additions & 1 deletion crates/bashkit/src/interpreter/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub enum ControlFlow {
Continue(u32),
/// Return from a function (with exit code)
Return(i32),
/// Exit the shell (with exit code)
Exit(i32),
}

/// Structured side-effect channel for builtins that need to communicate
Expand Down Expand Up @@ -174,6 +176,7 @@ impl LoopAccumulator {
ControlFlow::Return(code) => {
LoopAction::Exit(self.build_exit(ControlFlow::Return(code)))
}
ControlFlow::Exit(code) => LoopAction::Exit(self.build_exit(ControlFlow::Exit(code))),
ControlFlow::None => LoopAction::None,
}
}
Expand All @@ -193,7 +196,7 @@ impl LoopAccumulator {
/// Build an exit result, draining accumulated stdout/stderr.
fn build_exit(&mut self, control_flow: ControlFlow) -> ExecResult {
let exit_code = match control_flow {
ControlFlow::Return(code) => code,
ControlFlow::Return(code) | ControlFlow::Exit(code) => code,
_ => self.exit_code,
};
ExecResult {
Expand Down
71 changes: 71 additions & 0 deletions crates/bashkit/tests/spec_cases/bash/exit-status.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,74 @@ false || false; echo $?
1
0
### end

### exit_in_if_block
# exit inside if block terminates script
if true; then echo before; exit 0; fi
echo SHOULD_NOT_REACH
### expect
before
### end

### exit_in_if_block_with_code
# exit with non-zero code inside if block
if true; then exit 42; fi
echo SHOULD_NOT_REACH
### exit_code: 42
### expect
### end

### exit_in_while_loop
# exit inside while loop terminates script
i=0
while true; do
echo "iter $i"
if [ "$i" -eq 1 ]; then exit 0; fi
i=$((i + 1))
done
echo SHOULD_NOT_REACH
### expect
iter 0
iter 1
### end

### exit_in_for_loop
# exit inside for loop terminates script
for x in a b c; do
echo "$x"
if [ "$x" = "b" ]; then exit 5; fi
done
echo SHOULD_NOT_REACH
### exit_code: 5
### expect
a
b
### end

### exit_in_case_block
# exit inside case block terminates script
case "yes" in
yes) echo matched; exit 0 ;;
esac
echo SHOULD_NOT_REACH
### expect
matched
### end

### exit_in_subshell_does_not_propagate
# exit inside subshell only terminates the subshell
(exit 7)
echo "after subshell: $?"
### expect
after subshell: 7
### end

### exit_in_function
# exit inside function terminates the whole script
f() { echo in_func; exit 3; }
f
echo SHOULD_NOT_REACH
### exit_code: 3
### expect
in_func
### end
14 changes: 7 additions & 7 deletions supply-chain/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,7 @@ version = "0.1.34"
criteria = "safe-to-deploy"

[[exemptions.js-sys]]
version = "0.3.92"
version = "0.3.93"
criteria = "safe-to-deploy"

[[exemptions.leb128fmt]]
Expand Down Expand Up @@ -1467,23 +1467,23 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
criteria = "safe-to-run"

[[exemptions.wasm-bindgen]]
version = "0.2.115"
version = "0.2.116"
criteria = "safe-to-deploy"

[[exemptions.wasm-bindgen-futures]]
version = "0.4.65"
version = "0.4.66"
criteria = "safe-to-deploy"

[[exemptions.wasm-bindgen-macro]]
version = "0.2.115"
version = "0.2.116"
criteria = "safe-to-deploy"

[[exemptions.wasm-bindgen-macro-support]]
version = "0.2.115"
version = "0.2.116"
criteria = "safe-to-deploy"

[[exemptions.wasm-bindgen-shared]]
version = "0.2.115"
version = "0.2.116"
criteria = "safe-to-deploy"

[[exemptions.wasm-encoder]]
Expand All @@ -1503,7 +1503,7 @@ version = "0.244.0"
criteria = "safe-to-deploy"

[[exemptions.web-sys]]
version = "0.3.92"
version = "0.3.93"
criteria = "safe-to-deploy"

[[exemptions.web-time]]
Expand Down
Loading