Skip to main content

shrub_core/shell/
traps.rs

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