Skip to content

Commit b64e9fd

Browse files
authored
fix(interpreter): fire EXIT trap inside command substitution subshell (#854)
EXIT traps set inside $() now fire and their output is captured by the substitution, matching real bash behavior. Trap state is snapshot/restored so traps don't leak to the parent shell. Closes #806
1 parent 461f29b commit b64e9fd

File tree

2 files changed

+57
-0
lines changed

2 files changed

+57
-0
lines changed

crates/bashkit/src/interpreter/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5795,12 +5795,31 @@ impl Interpreter {
57955795
"maximum nesting depth exceeded in command substitution".to_string(),
57965796
));
57975797
}
5798+
// Command substitution runs in a subshell: snapshot trap state
5799+
// so EXIT traps set inside $() fire and don't leak to parent.
5800+
let saved_traps = self.traps.clone();
57985801
let mut stdout = String::new();
57995802
for cmd in commands {
58005803
let cmd_result = self.execute_command(cmd).await?;
58015804
stdout.push_str(&cmd_result.stdout);
58025805
self.last_exit_code = cmd_result.exit_code;
58035806
}
5807+
// Fire EXIT trap set inside the command substitution
5808+
if let Some(trap_cmd) = self.traps.get("EXIT").cloned()
5809+
&& saved_traps.get("EXIT") != Some(&trap_cmd)
5810+
&& let Ok(trap_script) = Parser::with_limits(
5811+
&trap_cmd,
5812+
self.limits.max_ast_depth,
5813+
self.limits.max_parser_operations,
5814+
)
5815+
.parse()
5816+
&& let Ok(trap_result) =
5817+
self.execute_command_sequence(&trap_script.commands).await
5818+
{
5819+
stdout.push_str(&trap_result.stdout);
5820+
}
5821+
// Restore parent trap state
5822+
self.traps = saved_traps;
58045823
self.counters.pop_function();
58055824
self.subst_generation += 1;
58065825
let trimmed = stdout.trim_end_matches('\n');

crates/bashkit/tests/spec_cases/bash/command-subst.test.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,41 @@ echo "$result"
185185
### expect
186186
(x)(y)(z)
187187
### end
188+
189+
### subst_exit_trap_captured
190+
# EXIT trap output should be captured inside $(), not leak to parent
191+
result="$(trap 'echo TRAPPED' EXIT; echo hello)"
192+
echo "captured=[${result}]"
193+
### expect
194+
captured=[hello
195+
TRAPPED]
196+
### end
197+
198+
### subst_exit_trap_with_explicit_exit
199+
# EXIT trap fires on explicit exit inside $()
200+
result="$(trap 'echo CLEANUP' EXIT; echo data; exit 0)"
201+
echo "captured=[${result}]"
202+
### expect
203+
captured=[data
204+
CLEANUP]
205+
### end
206+
207+
### subst_exit_trap_no_leak
208+
# Trap output must not leak to parent stdout
209+
out="$(trap 'echo INSIDE' EXIT; echo body)"
210+
echo "out=[${out}]"
211+
### expect
212+
out=[body
213+
INSIDE]
214+
### end
215+
216+
### subst_exit_trap_isolation
217+
# EXIT trap in $() should not affect parent traps
218+
trap 'echo PARENT' EXIT
219+
result="$(trap 'echo CHILD' EXIT; echo inner)"
220+
echo "result=[${result}]"
221+
trap - EXIT
222+
### expect
223+
result=[inner
224+
CHILD]
225+
### end

0 commit comments

Comments
 (0)