Skip to main content

shrub_core/shell/
callstack.rs

1//! Call stack management for the shell.
2
3use crate::{callstack, env, error, functions, trace_categories};
4
5impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
6    /// Returns whether or not the shell is actively executing in a sourced
7    /// script.
8    pub fn in_sourced_script(&self) -> bool {
9        self.call_stack.in_sourced_script()
10    }
11
12    /// Returns whether or not the shell is actively executing in a shell
13    /// function.
14    pub fn in_function(&self) -> bool {
15        self.call_stack.in_function()
16    }
17
18    /// Updates the shell's internal tracking state to reflect that a new
19    /// interactive session is being started.
20    ///
21    /// # Errors
22    ///
23    /// This currently always succeeds.
24    pub fn start_interactive_session(&mut self) -> Result<(), error::Error> {
25        self.call_stack.push_interactive_session();
26        Ok(())
27    }
28
29    /// Updates the shell's internal tracking state to reflect that the current
30    /// interactive session is ending.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`crate::error::ErrorKind::NotInInteractiveSession`] if the
35    /// current call stack frame is not an interactive session.
36    pub fn end_interactive_session(&mut self) -> Result<(), error::Error> {
37        if self
38            .call_stack
39            .current_frame()
40            .is_none_or(|frame| !frame.frame_type.is_interactive_session())
41        {
42            return Err(error::ErrorKind::NotInInteractiveSession.into());
43        }
44
45        self.call_stack.pop();
46
47        Ok(())
48    }
49
50    /// Updates the shell's internal tracking state to reflect that command
51    /// string mode is being started.
52    pub fn start_command_string_mode(&mut self) {
53        self.call_stack.push_command_string();
54    }
55
56    /// Updates the shell's internal tracking state to reflect that command
57    /// string mode is ending.
58    ///
59    /// # Errors
60    ///
61    /// Returns [`crate::error::ErrorKind::NotExecutingCommandString`] if the
62    /// current call stack frame is not a command string.
63    pub fn end_command_string_mode(&mut self) -> Result<(), error::Error> {
64        if self
65            .call_stack
66            .current_frame()
67            .is_none_or(|frame| !frame.frame_type.is_command_string())
68        {
69            return Err(error::ErrorKind::NotExecutingCommandString.into());
70        }
71
72        self.call_stack.pop();
73
74        Ok(())
75    }
76
77    /// Enters a trap handler for `signal` on the shell, until the returned
78    /// guard is dropped. Leaving it restores `$?`, so the handler doesn't
79    /// change the status that triggered it.
80    pub(crate) fn enter_trap_handler(
81        &mut self,
82        signal: crate::traps::TrapSignal,
83        handler: Option<&crate::traps::TrapHandler>,
84    ) -> ShellGuard<'_, SE> {
85        let last_exit_status = self.last_exit_status;
86        self.call_stack.push_trap_handler(signal, handler);
87        let frames = self.call_stack.depth();
88        ShellGuard::new(
89            self,
90            Leave::TrapHandler {
91                frames,
92                last_exit_status,
93            },
94        )
95    }
96
97    /// Enters a script on the shell, until the returned guard is dropped.
98    pub(crate) fn enter_script(
99        &mut self,
100        call_type: callstack::ScriptCallType,
101        source_info: &crate::SourceInfo,
102        args: impl IntoIterator<Item = String>,
103    ) -> ShellGuard<'_, SE> {
104        self.call_stack.push_script(call_type, source_info, args);
105        let frames = self.call_stack.depth();
106        ShellGuard::new(self, Leave::Script { frames })
107    }
108
109    /// Enters a call to the shell function `name` on the shell, with its own
110    /// scope for local variables, until the returned guard is dropped.
111    ///
112    /// # Arguments
113    ///
114    /// * `name` - The name of the function being entered.
115    /// * `function` - The function being entered.
116    /// * `args` - The arguments being passed to the function.
117    pub(crate) fn enter_function(
118        &mut self,
119        name: &str,
120        function: &functions::Registration,
121        args: impl IntoIterator<Item = String>,
122    ) -> Result<ShellGuard<'_, SE>, error::Error> {
123        if let Some(max_call_depth) = self.options.max_function_call_depth
124            && self.call_stack.function_call_depth() >= max_call_depth
125        {
126            return Err(error::ErrorKind::MaxFunctionCallDepthExceeded.into());
127        }
128
129        if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
130            let depth = self.call_stack.function_call_depth();
131            let prefix = repeated_char_str(' ', depth);
132            tracing::debug!(target: trace_categories::FUNCTIONS, "Entering func [depth={depth}]: {prefix}{name}");
133        }
134
135        self.call_stack.push_function(name, function, args);
136        self.env.push_scope(env::EnvironmentScope::Local);
137        let leave = Leave::Function {
138            frames: self.call_stack.depth(),
139            scopes: self.env.scope_depth(),
140        };
141
142        Ok(ShellGuard::new(self, leave))
143    }
144
145    /// Enters a new variable scope on the shell, until the returned guard is
146    /// dropped.
147    pub(crate) fn enter_scope(&mut self, scope: env::EnvironmentScope) -> ShellGuard<'_, SE> {
148        self.env.push_scope(scope);
149        let scopes = self.env.scope_depth();
150        ShellGuard::new(self, Leave::Scope { scopes, scope })
151    }
152
153    /// Blocks the delivery of traps until the returned guard is dropped. Blocks
154    /// nest: traps are delivered again only once every block is dropped.
155    pub(crate) const fn block_trap_delivery(&mut self) -> ShellGuard<'_, SE> {
156        self.call_stack.acquire_trap_delivery_block();
157        ShellGuard::new(self, Leave::TrapBlock)
158    }
159
160    /// Returns the *current* positional arguments for the shell ($1 and
161    /// beyond). Influenced by the current call stack.
162    pub fn current_shell_args(&self) -> &[String] {
163        for frame in self.call_stack.iter() {
164            match frame.frame_type {
165                // Function calls always shadow positional parameters.
166                crate::callstack::FrameType::Function(..) => return &frame.args,
167                // Executed scripts always shadow positional parameters.
168                _ if frame.frame_type.is_run_script() => return &frame.args,
169                // Sourced scripts shadow positional parameters if they have arguments.
170                _ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
171                    return &frame.args;
172                }
173                _ => (),
174            }
175        }
176
177        self.args.as_slice()
178    }
179
180    /// Returns a mutable reference to *current* positional parameters for the
181    /// shell ($1 and beyond).
182    pub fn current_shell_args_mut(&mut self) -> &mut Vec<String> {
183        for frame in self.call_stack.iter_mut() {
184            match frame.frame_type {
185                // Function calls always shadow positional parameters.
186                crate::callstack::FrameType::Function(..) => return &mut frame.args,
187                // Executed scripts always shadow positional parameters.
188                _ if frame.frame_type.is_run_script() => return &mut frame.args,
189                // Sourced scripts shadow positional parameters if they have arguments.
190                _ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
191                    return &mut frame.args;
192                }
193                _ => (),
194            }
195        }
196
197        &mut self.args
198    }
199}
200
201fn repeated_char_str(c: char, count: usize) -> String {
202    (0..count).map(|_| c).collect()
203}
204
205/// Guards something entered on a shell -- a function call, a script, a trap
206/// handler, a variable scope, or a block on trap delivery -- and leaves it when
207/// dropped: when whatever runs in it returns, fails, or is cancelled (its
208/// future dropped, as when the user hits Ctrl-C during completion).
209///
210/// It dereferences to the shell, for running whatever's entered.
211pub(crate) struct ShellGuard<'a, SE: crate::extensions::ShellExtensions> {
212    shell: &'a mut crate::Shell<SE>,
213    leave: Leave,
214}
215
216/// What a [`ShellGuard`] leaves when it's dropped, with how deep the call stack
217/// (`frames`) and variable scopes (`scopes`) were once it was entered.
218#[derive(Clone, Copy, Debug)]
219enum Leave {
220    Function {
221        frames: usize,
222        scopes: usize,
223    },
224    Script {
225        frames: usize,
226    },
227    TrapHandler {
228        frames: usize,
229        last_exit_status: u8,
230    },
231    Scope {
232        scopes: usize,
233        scope: env::EnvironmentScope,
234    },
235    TrapBlock,
236}
237
238impl<'a, SE: crate::extensions::ShellExtensions> ShellGuard<'a, SE> {
239    const fn new(shell: &'a mut crate::Shell<SE>, leave: Leave) -> Self {
240        Self { shell, leave }
241    }
242}
243
244impl<SE: crate::extensions::ShellExtensions> std::ops::Deref for ShellGuard<'_, SE> {
245    type Target = crate::Shell<SE>;
246
247    fn deref(&self) -> &Self::Target {
248        self.shell
249    }
250}
251
252impl<SE: crate::extensions::ShellExtensions> std::ops::DerefMut for ShellGuard<'_, SE> {
253    fn deref_mut(&mut self) -> &mut Self::Target {
254        self.shell
255    }
256}
257
258impl<SE: crate::extensions::ShellExtensions> ShellGuard<'_, SE> {
259    /// Returns whether this guard's frame is on top of the call stack, `depth`
260    /// frames deep.
261    fn frame_on_top(&self, depth: usize, is_expected: fn(&callstack::FrameType) -> bool) -> bool {
262        let stack = &self.shell.call_stack;
263        stack.depth() == depth
264            && stack
265                .current_frame()
266                .is_some_and(|frame| is_expected(&frame.frame_type))
267    }
268
269    /// Pops this guard's frame if it's on top, `depth` frames deep; returns
270    /// whether it was.
271    fn pop_frame(&mut self, depth: usize, is_expected: fn(&callstack::FrameType) -> bool) -> bool {
272        let on_top = self.frame_on_top(depth, is_expected);
273        if on_top {
274            self.shell.call_stack.pop();
275        }
276        on_top
277    }
278
279    /// Pops this guard's variable scope if it's on top, `depth` scopes deep;
280    /// returns whether it was.
281    fn pop_scope(&mut self, depth: usize, scope_type: env::EnvironmentScope) -> bool {
282        self.shell.env.scope_depth() == depth && self.shell.env.pop_scope(scope_type).is_ok()
283    }
284
285    /// Reports that what ran in this guard didn't leave everything it entered.
286    fn report_imbalance(&self) {
287        tracing::error!(
288            "shell state out of balance when leaving {:?}: call stack {} deep, {} variable scopes",
289            self.leave,
290            self.shell.call_stack.depth(),
291            self.shell.env.scope_depth()
292        );
293    }
294}
295
296impl<SE: crate::extensions::ShellExtensions> Drop for ShellGuard<'_, SE> {
297    fn drop(&mut self) {
298        // Whatever was entered on top of this has been left already, since its
299        // guard borrowed this one; so unless something left the shell
300        // out of balance, what this entered is on top. If it isn't,
301        // report that and pop nothing, rather than risk
302        // popping something else.
303        let left = match self.leave {
304            Leave::Function { frames, scopes } => {
305                // Pop the frame and its scope together, or neither.
306                let left = self.frame_on_top(frames, callstack::FrameType::is_function)
307                    && self.pop_scope(scopes, env::EnvironmentScope::Local);
308                if left
309                    && let Some(frame) = self.shell.call_stack.pop()
310                    && let callstack::FrameType::Function(call) = frame.frame_type
311                    && tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG)
312                {
313                    let depth = self.shell.call_stack.function_call_depth();
314                    let prefix = repeated_char_str(' ', depth);
315                    tracing::debug!(target: trace_categories::FUNCTIONS, "Exiting func  [depth={depth}]: {prefix}{}", call.function_name);
316                }
317                left
318            }
319            Leave::Script { frames } => self.pop_frame(frames, callstack::FrameType::is_script),
320            Leave::TrapHandler {
321                frames,
322                last_exit_status,
323            } => {
324                self.shell.last_exit_status = last_exit_status;
325                self.pop_frame(frames, callstack::FrameType::is_trap_handler)
326            }
327            Leave::Scope { scopes, scope } => self.pop_scope(scopes, scope),
328            Leave::TrapBlock => {
329                let blocked = self.shell.call_stack.is_trap_delivery_suppressed();
330                self.shell.call_stack.release_trap_delivery_block();
331                blocked
332            }
333        };
334
335        if !left {
336            self.report_imbalance();
337        }
338    }
339}
340
341#[cfg(test)]
342#[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")]
343mod tests {
344    use super::*;
345
346    /// Guards pop nothing -- not even part of what they entered -- if what ran
347    /// in them left the call stack or variable scopes out of balance (as a
348    /// buggy builtin could).
349    #[tokio::test]
350    async fn guards_pop_nothing_when_out_of_balance() -> Result<(), error::Error> {
351        let mut shell = crate::Shell::builder()
352            .profile(crate::ProfileLoadBehavior::Skip)
353            .rc(crate::RcLoadBehavior::Skip)
354            .build()
355            .await?;
356        let params = shell.default_exec_params();
357        shell
358            .run_string("f() { :; }", &crate::SourceInfo::default(), &params)
359            .await?;
360        let f = shell
361            .funcs()
362            .get("f")
363            .cloned()
364            .ok_or_else(|| error::ErrorKind::FunctionNotFound("f".into()))?;
365        let frames = shell.call_stack.depth();
366        let scopes = shell.env.scope_depth();
367
368        // A function whose scope has another left on top of it.
369        let mut function = shell.enter_function("f", &f, std::iter::empty())?;
370        function.env_mut().push_scope(env::EnvironmentScope::Local);
371        drop(function);
372        assert_eq!(shell.call_stack.depth(), frames + 1);
373        assert_eq!(shell.env.scope_depth(), scopes + 2);
374
375        // A script whose frame was replaced by another.
376        let mut script = shell.enter_script(
377            callstack::ScriptCallType::Source,
378            &crate::SourceInfo::default(),
379            std::iter::empty(),
380        );
381        script.call_stack.pop();
382        script.start_command_string_mode();
383        drop(script);
384        assert_eq!(shell.call_stack.depth(), frames + 2);
385
386        Ok(())
387    }
388}