Skip to main content

shrub_core/
shell.rs

1//! Module defining the core shell structure and behavior.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use tokio::sync::Mutex;
9
10use crate::env::ShellEnvironment;
11use crate::options::RuntimeOptions;
12use crate::{
13    ExecutionControlFlow, ExecutionResult, builtins, error, extensions, functions, interfaces,
14    jobs, keywords, openfiles, pathcache, wellknownvars,
15};
16
17/// Type for storing a key bindings helper.
18pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;
19
20/// Type alias for shell file descriptors.
21pub type ShellFd = i32;
22
23// NOTE: The submodule files below (e.g., `shell/traps.rs`,
24// `shell/callstack.rs`) contain `impl Shell<SE>` blocks that provide methods
25// coordinating with types defined in the corresponding top-level modules (e.g.,
26// `traps.rs`, `callstack.rs`). This is an intentional layered architecture:
27// top-level modules define domain types and data structures, while
28// shell/ submodules implement Shell methods that operate on those types.
29
30mod builder;
31mod builtin_registry;
32mod callstack;
33mod completion;
34mod env;
35mod execution;
36mod expansion;
37mod fs;
38mod funcs;
39mod history;
40mod initscripts;
41mod io;
42mod job_control;
43mod parsing;
44mod prompts;
45mod readline;
46mod state;
47mod traps;
48
49pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState};
50pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior};
51pub use state::ShellState;
52
53/// Represents an instance of a shell.
54///
55/// # Type Parameters
56///
57/// * `SE` - The shell extensions implementation to use. These extensions are
58///   statically injected into the shell at compile time to provide custom
59///   behavior. When unspecified, defaults to `DefaultShellExtensions`, which
60///   provide standard behavior.
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct Shell<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
63    /// Injected error behavior.
64    #[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))]
65    error_formatter: SE::ErrorFormatter,
66
67    /// Trap handler configuration for the shell.
68    traps: crate::traps::TrapHandlerConfig,
69
70    /// Manages files opened and accessible via redirection operators.
71    open_files: openfiles::OpenFiles,
72
73    /// The current working directory.
74    working_dir: PathBuf,
75
76    /// The shell environment, containing shell variables.
77    env: ShellEnvironment,
78
79    /// Shell function definitions.
80    funcs: functions::FunctionEnv,
81
82    /// Runtime shell options.
83    options: RuntimeOptions,
84
85    /// State of managed jobs.
86    /// TODO(serde): Need to warn somehow that jobs cannot be serialized.
87    #[cfg_attr(feature = "serde", serde(skip))]
88    jobs: jobs::JobManager,
89
90    /// Shell aliases.
91    aliases: HashMap<String, String>,
92
93    /// The status of the last completed command.
94    last_exit_status: u8,
95
96    /// Tracks changes to `last_exit_status`. Assignment-only commands observe
97    /// it to tell whether an expansion set a status (see `interp.rs`), so
98    /// code that rolls back `$?` must roll this back too.
99    last_exit_status_change_count: usize,
100
101    /// The status of each of the commands in the last pipeline.
102    last_pipeline_statuses: Vec<u8>,
103
104    /// Clone depth from the original ancestor shell.
105    depth: usize,
106
107    /// Shell name
108    name: Option<String>,
109
110    /// Positional shell arguments (not including shell name).
111    args: Vec<String>,
112
113    /// Shell version
114    version: Option<String>,
115
116    /// Detailed display string for the shell
117    product_display_str: Option<String>,
118
119    /// Function/script call stack.
120    call_stack: crate::callstack::CallStack,
121
122    /// Directory stack used by pushd et al.
123    directory_stack: Vec<PathBuf>,
124
125    /// Programmable completion state.
126    completion: crate::completion::State,
127
128    /// Shell built-in commands.
129    #[cfg_attr(feature = "serde", serde(skip))]
130    builtins: HashMap<String, builtins::Registration<SE>>,
131
132    /// Shell program location cache.
133    program_location_cache: pathcache::PathCache,
134
135    /// Last "SECONDS" captured time.
136    last_stopwatch_time: std::time::SystemTime,
137
138    /// Last "SECONDS" offset requested.
139    last_stopwatch_offset: u32,
140
141    /// Parser implementation to use.
142    #[cfg_attr(feature = "serde", serde(skip))]
143    parser_impl: crate::parser::ParserImpl,
144
145    /// Key bindings for the shell, optionally implemented by an interactive
146    /// shell.
147    #[cfg_attr(feature = "serde", serde(skip))]
148    key_bindings: Option<KeyBindingsHelper>,
149
150    /// History of commands executed in the shell.
151    history: Option<crate::history::History>,
152}
153
154impl<SE: extensions::ShellExtensions> Clone for Shell<SE> {
155    fn clone(&self) -> Self {
156        Self {
157            error_formatter: self.error_formatter.clone(),
158            traps: self.traps.clone(),
159            open_files: self.open_files.clone(),
160            working_dir: self.working_dir.clone(),
161            env: self.env.clone(),
162            funcs: self.funcs.clone(),
163            options: self.options.clone(),
164            jobs: jobs::JobManager::new(),
165            aliases: self.aliases.clone(),
166            last_exit_status: self.last_exit_status,
167            last_exit_status_change_count: self.last_exit_status_change_count,
168            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
169            name: self.name.clone(),
170            args: self.args.clone(),
171            version: self.version.clone(),
172            product_display_str: self.product_display_str.clone(),
173            call_stack: {
174                // Subshells must not inherit the parent's "currently handling
175                // signal X" state; otherwise a trap handler
176                // that spawns a subshell would see itself
177                // as already inside that handler and skip re-entrant delivery.
178                let mut cs = self.call_stack.clone();
179                cs.clear_active_trap_signals();
180                cs
181            },
182            directory_stack: self.directory_stack.clone(),
183            // A subshell of a completion function (e.g. `$(compgen ...)`) is in that
184            // completion too. Like a forked subshell in bash, it changes only its own copy
185            // (e.g. with `compopt`), which its parent doesn't see.
186            completion: self.completion.clone(),
187            builtins: self.builtins.clone(),
188            program_location_cache: self.program_location_cache.clone(),
189            last_stopwatch_time: self.last_stopwatch_time,
190            last_stopwatch_offset: self.last_stopwatch_offset,
191            parser_impl: self.parser_impl,
192            key_bindings: self.key_bindings.clone(),
193            history: self.history.clone(),
194            depth: self.depth + 1,
195        }
196    }
197}
198
199impl<SE: extensions::ShellExtensions> AsRef<Self> for Shell<SE> {
200    fn as_ref(&self) -> &Self {
201        self
202    }
203}
204
205impl<SE: extensions::ShellExtensions> AsMut<Self> for Shell<SE> {
206    fn as_mut(&mut self) -> &mut Self {
207        self
208    }
209}
210
211impl<SE: extensions::ShellExtensions> Shell<SE> {
212    /// Returns a new shell instance created with the given options.
213    /// Does *not* load any configuration files (e.g., bashrc).
214    ///
215    /// # Arguments
216    ///
217    /// * `options` - The options to use when creating the shell.
218    pub(crate) fn new(options: CreateOptions<SE>) -> Result<Self, error::Error> {
219        // Compute runtime options before moving fields out of `options`.
220        let runtime_options = RuntimeOptions::defaults_from(&options);
221
222        // Instantiate the shell with some defaults.
223        let mut shell = Self {
224            error_formatter: options.error_formatter,
225            open_files: openfiles::OpenFiles::new(),
226            options: runtime_options,
227            name: options.shell_name,
228            args: options.shell_args.unwrap_or_default(),
229            version: options.shell_version,
230            product_display_str: options.shell_product_display_str,
231            working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?,
232            builtins: options.builtins,
233            parser_impl: options.parser,
234            key_bindings: options.key_bindings,
235            ..Self::default()
236        };
237
238        // Add in any open files provided.
239        shell.open_files.update_from(options.fds.into_iter());
240
241        // TODO(patterns): Without this a script that sets extglob will fail
242        // because we parse the entire script with the same settings.
243        shell.options.extended_globbing = true;
244
245        // If requested, seed parameters from environment.
246        if !options.do_not_inherit_env {
247            wellknownvars::inherit_env_vars(&mut shell)?;
248        }
249
250        // If requested, set well-known variables.
251        if !options.skip_well_known_vars {
252            wellknownvars::init_well_known_vars(&mut shell)?;
253        }
254
255        // Set any provided variables.
256        for (var_name, var_value) in options.vars {
257            shell.env.set_global(var_name, var_value)?;
258        }
259
260        // Set up history, if relevant. The history file itself is loaded later,
261        // once startup files have run, since they may change HISTFILE.
262        if shell.options.enable_command_history {
263            shell.history = Some(crate::history::History::default());
264        }
265
266        Ok(shell)
267    }
268}
269
270impl<SE: extensions::ShellExtensions> Shell<SE> {
271    /// Increments the interactive line offset in the shell by the indicated
272    /// number of lines.
273    ///
274    /// # Arguments
275    ///
276    /// * `delta` - The number of lines to increment the current line offset by.
277    pub fn increment_interactive_line_offset(&mut self, delta: usize) {
278        self.call_stack.increment_current_line_offset(delta);
279    }
280
281    /// Updates the currently executing command in the shell.
282    pub fn set_current_cmd(&mut self, cmd: &impl shrub_parser::ast::Node) {
283        self.call_stack
284            .set_current_pos(cmd.location().map(|span| span.start));
285    }
286
287    /// Updates the `$_` shell variable (last-argument of the previous simple
288    /// command).
289    ///
290    /// Passes `Some(last_arg)` to record the last argument of the just-executed
291    /// command, or `None` to clear `$_` (used for assignment-only statements,
292    /// which bash treats as having no "last argument").
293    ///
294    /// The update is applied in-place so that attributes on `_` (notably
295    /// `readonly`) are preserved: attempting to update a readonly `_` is a
296    /// silent no-op, matching bash's observable stdout behavior.
297    pub(crate) fn update_last_arg_variable(&mut self, last_arg: Option<String>) {
298        // Bash refuses to update a readonly `_`, emitting an error to stderr
299        // on each attempt. We silently skip the update here — the observable
300        // stdout effect ($_ stays unchanged) matches bash; the missing stderr
301        // diagnostics are harmless.
302        if self
303            .env
304            .get_using_policy("_", crate::env::EnvironmentLookup::Anywhere)
305            .is_some_and(|v| v.is_readonly())
306        {
307            return;
308        }
309
310        // Replace the variable entirely (fresh, non-exported). This matches
311        // bash, which never exports `_` — even under `set -a` — and always
312        // clears any previously-set attributes (except readonly, handled
313        // above).
314        let value = last_arg.unwrap_or_default();
315        let _ = self
316            .env
317            .set_global("_", crate::variables::ShellVariable::new(value));
318    }
319
320    /// Captures the state the last command left behind; see
321    /// [`SavedCommandStatus`].
322    pub fn save_command_status(&self) -> SavedCommandStatus {
323        SavedCommandStatus {
324            exit_status: self.last_exit_status,
325            exit_status_change_count: self.last_exit_status_change_count,
326            pipeline_statuses: self.last_pipeline_statuses.clone(),
327            last_arg: self.env_str("_").map(|value| value.into_owned()),
328        }
329    }
330
331    /// Reapplies a snapshot, exit-status change counter included. Consumes it;
332    /// `clone` it to put the same one back more than once, e.g. between
333    /// successive hook functions.
334    ///
335    /// # Arguments
336    ///
337    /// * `saved` - The snapshot to restore.
338    pub fn restore_command_status(&mut self, saved: SavedCommandStatus) {
339        self.last_pipeline_statuses = saved.pipeline_statuses;
340        // Assigned directly rather than through `set_last_exit_status`, which
341        // would bump the change counter we're about to put back.
342        self.last_exit_status = saved.exit_status;
343        self.last_exit_status_change_count = saved.exit_status_change_count;
344        match saved.last_arg {
345            Some(last_arg) => self.update_last_arg_variable(Some(last_arg)),
346            // `_` was unset when the snapshot was taken, so put it back that way. Unsetting
347            // a readonly `_` fails; ignore that, as `update_last_arg_variable` does.
348            None => _ = self.env.unset("_"),
349        }
350    }
351
352    /// Applies errexit semantics to a result if enabled and appropriate.
353    /// This should be called at "statement boundaries" where errexit should be
354    /// checked.
355    ///
356    /// # Arguments
357    ///
358    /// * `result` - The execution result to potentially modify.
359    pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) {
360        if self.options.exit_on_nonzero_command_exit
361            && !result.is_success()
362            && result.is_normal_flow()
363        {
364            result.next_control_flow = ExecutionControlFlow::ExitShell;
365        }
366    }
367
368    /// Returns the keywords that are reserved by the shell, in the order bash
369    /// lists them.
370    pub(crate) fn get_keywords(&self) -> impl IntoIterator<Item = &'static str> {
371        keywords::keywords(self.options.sh_mode)
372    }
373
374    /// Checks if the given string is a keyword reserved in this shell.
375    ///
376    /// # Arguments
377    ///
378    /// * `s` - The string to check.
379    pub fn is_keyword(&self, s: &str) -> bool {
380        keywords::keywords(self.options.sh_mode).any(|keyword| keyword == s)
381    }
382
383    pub(crate) const fn last_exit_status_change_count(&self) -> usize {
384        self.last_exit_status_change_count
385    }
386}
387
388/// Snapshot of the state the last command left behind: `$?`, `PIPESTATUS`, and
389/// `$_`.
390///
391/// Take one with [`Shell::save_command_status`] and put it back with
392/// [`Shell::restore_command_status`] around anything the user didn't type -- a
393/// shell hook, `PROMPT_COMMAND`, prompt expansion -- so it stays invisible to
394/// the next command.
395#[derive(Clone, Debug)]
396pub struct SavedCommandStatus {
397    exit_status: u8,
398    exit_status_change_count: usize,
399    pipeline_statuses: Vec<u8>,
400    last_arg: Option<String>,
401}
402
403#[inherent::inherent]
404impl<SE: extensions::ShellExtensions> ShellState for Shell<SE> {
405    /// Returns whether or not this shell is a subshell.
406    pub fn is_subshell(&self) -> bool {
407        self.depth > 0
408    }
409
410    /// Returns the last "SECONDS" captured time.
411    pub fn last_stopwatch_time(&self) -> std::time::SystemTime {
412        self.last_stopwatch_time
413    }
414
415    /// Returns the last "SECONDS" offset requested.
416    pub fn last_stopwatch_offset(&self) -> u32 {
417        self.last_stopwatch_offset
418    }
419
420    /// Returns the shell environment containing variables.
421    pub fn env(&self) -> &ShellEnvironment {
422        &self.env
423    }
424
425    /// Returns a mutable reference to the shell environment.
426    pub fn env_mut(&mut self) -> &mut ShellEnvironment {
427        &mut self.env
428    }
429
430    /// Returns the shell's runtime options.
431    pub fn options(&self) -> &RuntimeOptions {
432        &self.options
433    }
434
435    /// Returns a mutable reference to the shell's runtime options.
436    pub fn options_mut(&mut self) -> &mut RuntimeOptions {
437        &mut self.options
438    }
439
440    /// Returns the shell's aliases.
441    pub fn aliases(&self) -> &HashMap<String, String> {
442        &self.aliases
443    }
444
445    /// Returns a mutable reference to the shell's aliases.
446    pub fn aliases_mut(&mut self) -> &mut HashMap<String, String> {
447        &mut self.aliases
448    }
449
450    /// Returns the shell's job manager.
451    pub fn jobs(&self) -> &jobs::JobManager {
452        &self.jobs
453    }
454
455    /// Returns a mutable reference to the shell's job manager.
456    pub fn jobs_mut(&mut self) -> &mut jobs::JobManager {
457        &mut self.jobs
458    }
459
460    /// Returns the shell's trap handler configuration.
461    pub fn traps(&self) -> &crate::traps::TrapHandlerConfig {
462        &self.traps
463    }
464
465    /// Returns a mutable reference to the shell's trap handler configuration.
466    pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig {
467        &mut self.traps
468    }
469
470    /// Returns the shell's directory stack.
471    pub fn directory_stack(&self) -> &[PathBuf] {
472        &self.directory_stack
473    }
474
475    /// Returns a mutable reference to the shell's directory stack.
476    pub fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf> {
477        &mut self.directory_stack
478    }
479
480    /// Returns the statuses of commands in the last pipeline.
481    pub fn last_pipeline_statuses(&self) -> &[u8] {
482        &self.last_pipeline_statuses
483    }
484
485    /// Returns a mutable reference to the statuses of commands in the last
486    /// pipeline.
487    pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8> {
488        &mut self.last_pipeline_statuses
489    }
490
491    /// Returns the shell's program location cache.
492    pub fn program_location_cache(&self) -> &pathcache::PathCache {
493        &self.program_location_cache
494    }
495
496    /// Returns a mutable reference to the shell's program location cache.
497    pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache {
498        &mut self.program_location_cache
499    }
500
501    /// Returns the shell's completion configuration.
502    pub fn completion_config(&self) -> &crate::completion::Config {
503        &self.completion.config
504    }
505
506    /// Returns a mutable reference to the shell's completion configuration.
507    pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config {
508        &mut self.completion.config
509    }
510
511    /// Returns the shell's open files.
512    pub fn open_files(&self) -> &openfiles::OpenFiles {
513        &self.open_files
514    }
515
516    /// Returns a mutable reference to the shell's open files.
517    pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles {
518        &mut self.open_files
519    }
520
521    /// Returns the *current* name of the shell ($0).
522    /// Influenced by the current call stack.
523    pub fn current_shell_name(&self) -> Option<Cow<'_, str>> {
524        for frame in self.call_stack.iter() {
525            // Executed scripts shadow the shell name.
526            if frame.frame_type.is_run_script() {
527                return Some(frame.frame_type.name());
528            }
529        }
530
531        self.name.as_deref().map(|name| name.into())
532    }
533
534    /// Returns the current subshell depth; 0 is returned if this shell is not a
535    /// subshell.
536    pub fn depth(&self) -> usize {
537        self.depth
538    }
539
540    /// Returns the call stack for the shell.
541    pub fn call_stack(&self) -> &crate::callstack::CallStack {
542        &self.call_stack
543    }
544
545    /// Returns the shell's history, if it exists.
546    pub fn history(&self) -> Option<&crate::history::History> {
547        self.history.as_ref()
548    }
549
550    /// Returns a mutable reference to the shell's history, if it exists.
551    pub fn history_mut(&mut self) -> Option<&mut crate::history::History> {
552        self.history.as_mut()
553    }
554
555    /// Returns the shell's official version string (if available).
556    pub fn version(&self) -> Option<&str> {
557        self.version.as_deref()
558    }
559
560    /// Returns the exit status of the last command executed in this shell.
561    pub fn last_exit_status(&self) -> u8 {
562        self.last_exit_status
563    }
564
565    /// Updates the last exit status, bumping `last_exit_status_change_count`.
566    /// To *restore* a status rather than set one, use
567    /// [`Self::restore_command_status`], which puts the counter back too.
568    pub fn set_last_exit_status(&mut self, status: u8) {
569        self.last_exit_status = status;
570        self.last_exit_status_change_count += 1;
571    }
572
573    /// Returns the key bindings helper for the shell.
574    pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> {
575        self.key_bindings.as_ref()
576    }
577
578    /// Sets the key bindings helper for the shell.
579    pub fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>) {
580        self.key_bindings = key_bindings;
581    }
582
583    /// Returns the shell's current working directory.
584    pub fn working_dir(&self) -> &Path {
585        &self.working_dir
586    }
587
588    /// Returns a mutable reference to the shell's current working directory.
589    /// This is only accessible within the crate.
590    pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf {
591        &mut self.working_dir
592    }
593
594    /// Returns the product display name for this shell.
595    pub fn product_display_str(&self) -> Option<&str> {
596        self.product_display_str.as_deref()
597    }
598}
599
600#[cfg(feature = "serde")]
601fn default_error_formatter<EF: extensions::ErrorFormatter>() -> EF {
602    EF::default()
603}
604
605#[cfg(test)]
606#[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")]
607mod tests {
608    use super::*;
609
610    /// `$_` round-trips through a snapshot, unset included, so an embedder that
611    /// saves before running commands of its own gets back exactly what was
612    /// there -- not an empty string. (The interactive loop can't reach the
613    /// unset case: every command resets `$_`.)
614    #[tokio::test]
615    async fn saved_command_status_round_trips_last_arg() -> Result<(), error::Error> {
616        let mut shell = Shell::builder()
617            .profile(ProfileLoadBehavior::Skip)
618            .rc(RcLoadBehavior::Skip)
619            .build()
620            .await?;
621
622        shell.update_last_arg_variable(Some(String::from("saved")));
623        let with_value = shell.save_command_status();
624
625        shell.env.unset("_")?;
626        let while_unset = shell.save_command_status();
627
628        shell.restore_command_status(with_value);
629        assert_eq!(shell.env_str("_").as_deref(), Some("saved"));
630
631        shell.restore_command_status(while_unset);
632        assert_eq!(shell.env_str("_"), None);
633
634        Ok(())
635    }
636
637    #[tokio::test]
638    async fn unset_env_str_contract_is_independent_of_ifs_default() -> Result<(), error::Error> {
639        let mut shell = Shell::builder()
640            .profile(ProfileLoadBehavior::Skip)
641            .rc(RcLoadBehavior::Skip)
642            .build()
643            .await?;
644
645        shell.env.set_global(
646            "IFS",
647            crate::ShellVariable::new(crate::ShellValue::Unset(
648                crate::variables::ShellValueUnsetType::Untyped,
649            )),
650        )?;
651        assert_eq!(shell.env_str("IFS").as_deref(), Some(""));
652        assert_eq!(shell.ifs(), " \t\n");
653
654        shell.env.set_global("IFS", crate::ShellVariable::new(""))?;
655        assert_eq!(shell.env_str("IFS").as_deref(), Some(""));
656        assert_eq!(shell.ifs(), "");
657
658        Ok(())
659    }
660}