diff --git a/crates/bashkit/src/interpreter/mod.rs b/crates/bashkit/src/interpreter/mod.rs index 872e02f8..915b1d26 100644 --- a/crates/bashkit/src/interpreter/mod.rs +++ b/crates/bashkit/src/interpreter/mod.rs @@ -5795,12 +5795,31 @@ impl Interpreter { "maximum nesting depth exceeded in command substitution".to_string(), )); } + // Command substitution runs in a subshell: snapshot trap state + // so EXIT traps set inside $() fire and don't leak to parent. + let saved_traps = self.traps.clone(); let mut stdout = String::new(); for cmd in commands { let cmd_result = self.execute_command(cmd).await?; stdout.push_str(&cmd_result.stdout); self.last_exit_code = cmd_result.exit_code; } + // Fire EXIT trap set inside the command substitution + if let Some(trap_cmd) = self.traps.get("EXIT").cloned() + && saved_traps.get("EXIT") != Some(&trap_cmd) + && let Ok(trap_script) = Parser::with_limits( + &trap_cmd, + self.limits.max_ast_depth, + self.limits.max_parser_operations, + ) + .parse() + && let Ok(trap_result) = + self.execute_command_sequence(&trap_script.commands).await + { + stdout.push_str(&trap_result.stdout); + } + // Restore parent trap state + self.traps = saved_traps; self.counters.pop_function(); self.subst_generation += 1; let trimmed = stdout.trim_end_matches('\n'); diff --git a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh index 38210a8d..3342a7bf 100644 --- a/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh +++ b/crates/bashkit/tests/spec_cases/bash/command-subst.test.sh @@ -185,3 +185,41 @@ echo "$result" ### expect (x)(y)(z) ### end + +### subst_exit_trap_captured +# EXIT trap output should be captured inside $(), not leak to parent +result="$(trap 'echo TRAPPED' EXIT; echo hello)" +echo "captured=[${result}]" +### expect +captured=[hello +TRAPPED] +### end + +### subst_exit_trap_with_explicit_exit +# EXIT trap fires on explicit exit inside $() +result="$(trap 'echo CLEANUP' EXIT; echo data; exit 0)" +echo "captured=[${result}]" +### expect +captured=[data +CLEANUP] +### end + +### subst_exit_trap_no_leak +# Trap output must not leak to parent stdout +out="$(trap 'echo INSIDE' EXIT; echo body)" +echo "out=[${out}]" +### expect +out=[body +INSIDE] +### end + +### subst_exit_trap_isolation +# EXIT trap in $() should not affect parent traps +trap 'echo PARENT' EXIT +result="$(trap 'echo CHILD' EXIT; echo inner)" +echo "result=[${result}]" +trap - EXIT +### expect +result=[inner +CHILD] +### end