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