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