brush_core/shell/traps.rs
1//! Trap handling for the shell.
2
3use crate::{ExecutionParameters, ExecutionResult, ProcessGroupPolicy, error, traps::TrapSignal};
4
5impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
6 /// Runs any exit steps for the shell.
7 ///
8 /// This currently includes invoking the `EXIT` trap handler, if any.
9 pub async fn on_exit(&mut self) -> Result<(), error::Error> {
10 if self.traps.handles(TrapSignal::Exit) {
11 self.invoke_trap_handler(TrapSignal::Exit, &self.default_exec_params())
12 .await?;
13 }
14
15 Ok(())
16 }
17
18 /// Invokes the handler registered for `signal`, if any.
19 ///
20 /// Behavior varies by signal type:
21 ///
22 /// * **Per-signal recursion guard** — each trap guards against its own self-recursion, but
23 /// different traps *can* fire from within each other's handlers (matching bash semantics).
24 ///
25 /// * **Inheritance** — in functions and subshells, some traps are only inherited when the
26 /// corresponding shell option is enabled (e.g. `errtrace` / `set -E` for `ERR`, `functrace` /
27 /// `set -T` for `DEBUG`/`RETURN`).
28 ///
29 /// * **`$?` preservation** — `last_exit_status` is saved before and restored after the handler
30 /// runs so the trap does not clobber the status that triggered it.
31 ///
32 /// # Arguments
33 ///
34 /// * `signal`: Signal to run handler for.
35 ///
36 /// * `params`: Execution parameters to use for handler.
37 pub(crate) async fn invoke_trap_handler(
38 &mut self,
39 signal: TrapSignal,
40 params: &ExecutionParameters,
41 ) -> Result<ExecutionResult, error::Error> {
42 // Per-signal self-recursion guard: don't re-enter a trap that is
43 // already being handled. Different traps *can* fire from each
44 // other's handlers (e.g. ERR inside EXIT, EXIT inside ERR).
45 if self.call_stack().is_trap_signal_active(signal) {
46 return Ok(ExecutionResult::success());
47 }
48
49 // Don't fire traps that have been explicitly suppressed (e.g. DEBUG
50 // during programmable completion).
51 if self.call_stack().is_trap_delivery_suppressed() {
52 return Ok(ExecutionResult::success());
53 }
54
55 // In functions and subshells, some traps are only inherited when the
56 // corresponding option is enabled.
57 if (self.in_function() || self.is_subshell())
58 && !self.is_trap_inherited_in_current_scope(signal)
59 {
60 return Ok(ExecutionResult::success());
61 }
62
63 let Some(handler) = self.traps.get_handler(signal).cloned() else {
64 return Ok(ExecutionResult::success());
65 };
66
67 let mut params = params.clone();
68 params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
69
70 // Preserve $? across trap handler execution so the handler doesn't
71 // clobber the status that triggered it.
72 let orig_last_exit_status = self.last_exit_status;
73
74 // N.B. We use manual enter/leave rather than an RAII guard because a guard
75 // would need to hold `&mut Shell`, preventing the mutable borrow required by
76 // `run_string()`. This is safe because `result` is captured into a variable
77 // (never early-returned with `?`), so `leave_trap_handler()` always runs.
78 self.enter_trap_handler(signal, Some(&handler));
79
80 let result = self
81 .run_string(&handler.command, &handler.source_info, ¶ms)
82 .await;
83
84 self.leave_trap_handler();
85 self.last_exit_status = orig_last_exit_status;
86
87 result
88 }
89
90 /// Returns whether the given trap signal is inherited in the current
91 /// function or subshell scope.
92 fn is_trap_inherited_in_current_scope(&self, signal: TrapSignal) -> bool {
93 match signal {
94 TrapSignal::Err => self.options().shell_functions_inherit_err_trap,
95 TrapSignal::Debug | TrapSignal::Return => {
96 self.options()
97 .shell_functions_inherit_debug_and_return_traps
98 }
99 // EXIT and system signals are always inherited — i.e. their visibility is
100 // not gated by errtrace/functrace options. (The actual trap *state* for
101 // subshells is managed separately via `Shell::clone`.)
102 TrapSignal::Exit | TrapSignal::Signal(_) => true,
103 }
104 }
105}