Skip to main content

brush_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::{
11    ExecutionControlFlow, ExecutionResult, builtins, env::ShellEnvironment, error, extensions,
12    functions, interfaces, jobs, keywords, openfiles, options::RuntimeOptions, pathcache,
13    wellknownvars,
14};
15
16/// Type for storing a key bindings helper.
17pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;
18
19/// Type alias for shell file descriptors.
20pub type ShellFd = i32;
21
22// NOTE: The submodule files below (e.g., `shell/traps.rs`, `shell/callstack.rs`) contain
23// `impl Shell<SE>` blocks that provide methods coordinating with types defined in the
24// corresponding top-level modules (e.g., `traps.rs`, `callstack.rs`). This is an intentional
25// layered architecture: top-level modules define domain types and data structures, while
26// shell/ submodules implement Shell methods that operate on those types.
27
28mod builder;
29mod builtin_registry;
30mod callstack;
31mod completion;
32mod env;
33mod execution;
34mod expansion;
35mod fs;
36mod funcs;
37mod history;
38mod initscripts;
39mod io;
40mod job_control;
41mod parsing;
42mod prompts;
43mod readline;
44mod state;
45mod traps;
46
47pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState};
48pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior};
49pub use state::ShellState;
50
51/// Represents an instance of a shell.
52///
53/// # Type Parameters
54///
55/// * `SE` - The shell extensions implementation to use. These extensions are statically injected
56///   into the shell at compile time to provide custom behavior. When unspecified, defaults to
57///   `DefaultShellExtensions`, which provide standard behavior.
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct Shell<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
60    /// Injected error behavior.
61    #[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))]
62    error_formatter: SE::ErrorFormatter,
63
64    /// Trap handler configuration for the shell.
65    traps: crate::traps::TrapHandlerConfig,
66
67    /// Manages files opened and accessible via redirection operators.
68    open_files: openfiles::OpenFiles,
69
70    /// The current working directory.
71    working_dir: PathBuf,
72
73    /// The shell environment, containing shell variables.
74    env: ShellEnvironment,
75
76    /// Shell function definitions.
77    funcs: functions::FunctionEnv,
78
79    /// Runtime shell options.
80    options: RuntimeOptions,
81
82    /// State of managed jobs.
83    /// TODO(serde): Need to warn somehow that jobs cannot be serialized.
84    #[cfg_attr(feature = "serde", serde(skip))]
85    jobs: jobs::JobManager,
86
87    /// Shell aliases.
88    aliases: HashMap<String, String>,
89
90    /// The status of the last completed command.
91    last_exit_status: u8,
92
93    /// Tracks changes to `last_exit_status`.
94    last_exit_status_change_count: usize,
95
96    /// The status of each of the commands in the last pipeline.
97    last_pipeline_statuses: Vec<u8>,
98
99    /// Clone depth from the original ancestor shell.
100    depth: usize,
101
102    /// Shell name
103    name: Option<String>,
104
105    /// Positional shell arguments (not including shell name).
106    args: Vec<String>,
107
108    /// Shell version
109    version: Option<String>,
110
111    /// Detailed display string for the shell
112    product_display_str: Option<String>,
113
114    /// Function/script call stack.
115    call_stack: crate::callstack::CallStack,
116
117    /// Directory stack used by pushd et al.
118    directory_stack: Vec<PathBuf>,
119
120    /// Completion configuration.
121    completion_config: crate::completion::Config,
122
123    /// Shell built-in commands.
124    #[cfg_attr(feature = "serde", serde(skip))]
125    builtins: HashMap<String, builtins::Registration<SE>>,
126
127    /// Shell program location cache.
128    program_location_cache: pathcache::PathCache,
129
130    /// Last "SECONDS" captured time.
131    last_stopwatch_time: std::time::SystemTime,
132
133    /// Last "SECONDS" offset requested.
134    last_stopwatch_offset: u32,
135
136    /// Parser implementation to use.
137    #[cfg_attr(feature = "serde", serde(skip))]
138    parser_impl: crate::parser::ParserImpl,
139
140    /// Key bindings for the shell, optionally implemented by an interactive shell.
141    #[cfg_attr(feature = "serde", serde(skip))]
142    key_bindings: Option<KeyBindingsHelper>,
143
144    /// History of commands executed in the shell.
145    history: Option<crate::history::History>,
146}
147
148impl<SE: extensions::ShellExtensions> Clone for Shell<SE> {
149    fn clone(&self) -> Self {
150        Self {
151            error_formatter: self.error_formatter.clone(),
152            traps: self.traps.clone(),
153            open_files: self.open_files.clone(),
154            working_dir: self.working_dir.clone(),
155            env: self.env.clone(),
156            funcs: self.funcs.clone(),
157            options: self.options.clone(),
158            jobs: jobs::JobManager::new(),
159            aliases: self.aliases.clone(),
160            last_exit_status: self.last_exit_status,
161            last_exit_status_change_count: self.last_exit_status_change_count,
162            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
163            name: self.name.clone(),
164            args: self.args.clone(),
165            version: self.version.clone(),
166            product_display_str: self.product_display_str.clone(),
167            call_stack: {
168                // Subshells must not inherit the parent's "currently handling signal X"
169                // state; otherwise a trap handler that spawns a subshell would see itself
170                // as already inside that handler and skip re-entrant delivery.
171                let mut cs = self.call_stack.clone();
172                cs.clear_active_trap_signals();
173                cs
174            },
175            directory_stack: self.directory_stack.clone(),
176            completion_config: self.completion_config.clone(),
177            builtins: self.builtins.clone(),
178            program_location_cache: self.program_location_cache.clone(),
179            last_stopwatch_time: self.last_stopwatch_time,
180            last_stopwatch_offset: self.last_stopwatch_offset,
181            parser_impl: self.parser_impl,
182            key_bindings: self.key_bindings.clone(),
183            history: self.history.clone(),
184            depth: self.depth + 1,
185        }
186    }
187}
188
189impl<SE: extensions::ShellExtensions> AsRef<Self> for Shell<SE> {
190    fn as_ref(&self) -> &Self {
191        self
192    }
193}
194
195impl<SE: extensions::ShellExtensions> AsMut<Self> for Shell<SE> {
196    fn as_mut(&mut self) -> &mut Self {
197        self
198    }
199}
200
201impl<SE: extensions::ShellExtensions> Shell<SE> {
202    /// Returns a new shell instance created with the given options.
203    /// Does *not* load any configuration files (e.g., bashrc).
204    ///
205    /// # Arguments
206    ///
207    /// * `options` - The options to use when creating the shell.
208    pub(crate) fn new(options: CreateOptions<SE>) -> Result<Self, error::Error> {
209        // Compute runtime options before moving fields out of `options`.
210        let runtime_options = RuntimeOptions::defaults_from(&options);
211
212        // Instantiate the shell with some defaults.
213        let mut shell = Self {
214            error_formatter: options.error_formatter,
215            open_files: openfiles::OpenFiles::new(),
216            options: runtime_options,
217            name: options.shell_name,
218            args: options.shell_args.unwrap_or_default(),
219            version: options.shell_version,
220            product_display_str: options.shell_product_display_str,
221            working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?,
222            builtins: options.builtins,
223            parser_impl: options.parser,
224            key_bindings: options.key_bindings,
225            ..Self::default()
226        };
227
228        // Add in any open files provided.
229        shell.open_files.update_from(options.fds.into_iter());
230
231        // TODO(patterns): Without this a script that sets extglob will fail because we
232        // parse the entire script with the same settings.
233        shell.options.extended_globbing = true;
234
235        // If requested, seed parameters from environment.
236        if !options.do_not_inherit_env {
237            wellknownvars::inherit_env_vars(&mut shell)?;
238        }
239
240        // If requested, set well-known variables.
241        if !options.skip_well_known_vars {
242            wellknownvars::init_well_known_vars(&mut shell)?;
243        }
244
245        // Set any provided variables.
246        for (var_name, var_value) in options.vars {
247            shell.env.set_global(var_name, var_value)?;
248        }
249
250        // Set up history, if relevant. Do NOT fail if we can't load history.
251        if shell.options.enable_command_history {
252            shell.history = shell
253                .load_history()
254                .unwrap_or_default()
255                .or_else(|| Some(crate::history::History::default()));
256        }
257
258        Ok(shell)
259    }
260}
261
262impl<SE: extensions::ShellExtensions> Shell<SE> {
263    /// Increments the interactive line offset in the shell by the indicated number
264    /// of lines.
265    ///
266    /// # Arguments
267    ///
268    /// * `delta` - The number of lines to increment the current line offset by.
269    pub fn increment_interactive_line_offset(&mut self, delta: usize) {
270        self.call_stack.increment_current_line_offset(delta);
271    }
272
273    /// Updates the currently executing command in the shell.
274    pub fn set_current_cmd(&mut self, cmd: &impl brush_parser::ast::Node) {
275        self.call_stack
276            .set_current_pos(cmd.location().map(|span| span.start));
277    }
278
279    /// Updates the `$_` shell variable (last-argument of the previous simple
280    /// command).
281    ///
282    /// Passes `Some(last_arg)` to record the last argument of the just-executed
283    /// command, or `None` to clear `$_` (used for assignment-only statements,
284    /// which bash treats as having no "last argument").
285    ///
286    /// The update is applied in-place so that attributes on `_` (notably
287    /// `readonly`) are preserved: attempting to update a readonly `_` is a
288    /// silent no-op, matching bash's observable stdout behavior.
289    pub(crate) fn update_last_arg_variable(&mut self, last_arg: Option<String>) {
290        // Bash refuses to update a readonly `_`, emitting an error to stderr
291        // on each attempt. We silently skip the update here — the observable
292        // stdout effect ($_ stays unchanged) matches bash; the missing stderr
293        // diagnostics are harmless.
294        if self
295            .env
296            .get_using_policy("_", crate::env::EnvironmentLookup::Anywhere)
297            .is_some_and(|v| v.is_readonly())
298        {
299            return;
300        }
301
302        // Replace the variable entirely (fresh, non-exported). This matches
303        // bash, which never exports `_` — even under `set -a` — and always
304        // clears any previously-set attributes (except readonly, handled
305        // above).
306        let value = last_arg.unwrap_or_default();
307        let _ = self
308            .env
309            .set_global("_", crate::variables::ShellVariable::new(value));
310    }
311
312    /// Applies errexit semantics to a result if enabled and appropriate.
313    /// This should be called at "statement boundaries" where errexit should be checked.
314    ///
315    /// # Arguments
316    ///
317    /// * `result` - The execution result to potentially modify.
318    pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) {
319        if self.options.exit_on_nonzero_command_exit
320            && !result.is_success()
321            && result.is_normal_flow()
322        {
323            result.next_control_flow = ExecutionControlFlow::ExitShell;
324        }
325    }
326
327    /// Returns the keywords that are reserved by the shell.
328    pub(crate) fn get_keywords(&self) -> Vec<&str> {
329        if self.options.sh_mode {
330            keywords::SH_MODE_KEYWORDS.iter().copied().collect()
331        } else {
332            keywords::KEYWORDS.iter().copied().collect()
333        }
334    }
335
336    /// Checks if the given string is a keyword reserved in this shell.
337    ///
338    /// # Arguments
339    ///
340    /// * `s` - The string to check.
341    pub fn is_keyword(&self, s: &str) -> bool {
342        if self.options.sh_mode {
343            keywords::SH_MODE_KEYWORDS.contains(s)
344        } else {
345            keywords::KEYWORDS.contains(s)
346        }
347    }
348
349    pub(crate) const fn last_exit_status_change_count(&self) -> usize {
350        self.last_exit_status_change_count
351    }
352}
353
354#[inherent::inherent]
355impl<SE: extensions::ShellExtensions> ShellState for Shell<SE> {
356    /// Returns whether or not this shell is a subshell.
357    pub fn is_subshell(&self) -> bool {
358        self.depth > 0
359    }
360
361    /// Returns the last "SECONDS" captured time.
362    pub fn last_stopwatch_time(&self) -> std::time::SystemTime {
363        self.last_stopwatch_time
364    }
365
366    /// Returns the last "SECONDS" offset requested.
367    pub fn last_stopwatch_offset(&self) -> u32 {
368        self.last_stopwatch_offset
369    }
370
371    /// Returns the shell environment containing variables.
372    pub fn env(&self) -> &ShellEnvironment {
373        &self.env
374    }
375
376    /// Returns a mutable reference to the shell environment.
377    pub fn env_mut(&mut self) -> &mut ShellEnvironment {
378        &mut self.env
379    }
380
381    /// Returns the shell's runtime options.
382    pub fn options(&self) -> &RuntimeOptions {
383        &self.options
384    }
385
386    /// Returns a mutable reference to the shell's runtime options.
387    pub fn options_mut(&mut self) -> &mut RuntimeOptions {
388        &mut self.options
389    }
390
391    /// Returns the shell's aliases.
392    pub fn aliases(&self) -> &HashMap<String, String> {
393        &self.aliases
394    }
395
396    /// Returns a mutable reference to the shell's aliases.
397    pub fn aliases_mut(&mut self) -> &mut HashMap<String, String> {
398        &mut self.aliases
399    }
400
401    /// Returns the shell's job manager.
402    pub fn jobs(&self) -> &jobs::JobManager {
403        &self.jobs
404    }
405
406    /// Returns a mutable reference to the shell's job manager.
407    pub fn jobs_mut(&mut self) -> &mut jobs::JobManager {
408        &mut self.jobs
409    }
410
411    /// Returns the shell's trap handler configuration.
412    pub fn traps(&self) -> &crate::traps::TrapHandlerConfig {
413        &self.traps
414    }
415
416    /// Returns a mutable reference to the shell's trap handler configuration.
417    pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig {
418        &mut self.traps
419    }
420
421    /// Returns the shell's directory stack.
422    pub fn directory_stack(&self) -> &[PathBuf] {
423        &self.directory_stack
424    }
425
426    /// Returns a mutable reference to the shell's directory stack.
427    pub fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf> {
428        &mut self.directory_stack
429    }
430
431    /// Returns the statuses of commands in the last pipeline.
432    pub fn last_pipeline_statuses(&self) -> &[u8] {
433        &self.last_pipeline_statuses
434    }
435
436    /// Returns a mutable reference to the statuses of commands in the last pipeline.
437    pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8> {
438        &mut self.last_pipeline_statuses
439    }
440
441    /// Returns the shell's program location cache.
442    pub fn program_location_cache(&self) -> &pathcache::PathCache {
443        &self.program_location_cache
444    }
445
446    /// Returns a mutable reference to the shell's program location cache.
447    pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache {
448        &mut self.program_location_cache
449    }
450
451    /// Returns the shell's completion configuration.
452    pub fn completion_config(&self) -> &crate::completion::Config {
453        &self.completion_config
454    }
455
456    /// Returns a mutable reference to the shell's completion configuration.
457    pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config {
458        &mut self.completion_config
459    }
460
461    /// Returns the shell's open files.
462    pub fn open_files(&self) -> &openfiles::OpenFiles {
463        &self.open_files
464    }
465
466    /// Returns a mutable reference to the shell's open files.
467    pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles {
468        &mut self.open_files
469    }
470
471    /// Returns the *current* name of the shell ($0).
472    /// Influenced by the current call stack.
473    pub fn current_shell_name(&self) -> Option<Cow<'_, str>> {
474        for frame in self.call_stack.iter() {
475            // Executed scripts shadow the shell name.
476            if frame.frame_type.is_run_script() {
477                return Some(frame.frame_type.name());
478            }
479        }
480
481        self.name.as_deref().map(|name| name.into())
482    }
483
484    /// Returns the current subshell depth; 0 is returned if this shell is not a subshell.
485    pub fn depth(&self) -> usize {
486        self.depth
487    }
488
489    /// Returns the call stack for the shell.
490    pub fn call_stack(&self) -> &crate::callstack::CallStack {
491        &self.call_stack
492    }
493
494    /// Returns the shell's history, if it exists.
495    pub fn history(&self) -> Option<&crate::history::History> {
496        self.history.as_ref()
497    }
498
499    /// Returns a mutable reference to the shell's history, if it exists.
500    pub fn history_mut(&mut self) -> Option<&mut crate::history::History> {
501        self.history.as_mut()
502    }
503
504    /// Returns the shell's official version string (if available).
505    pub fn version(&self) -> Option<&str> {
506        self.version.as_deref()
507    }
508
509    /// Returns the exit status of the last command executed in this shell.
510    pub fn last_exit_status(&self) -> u8 {
511        self.last_exit_status
512    }
513
514    /// Updates the last exit status.
515    pub fn set_last_exit_status(&mut self, status: u8) {
516        self.last_exit_status = status;
517        self.last_exit_status_change_count += 1;
518    }
519
520    /// Returns the key bindings helper for the shell.
521    pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> {
522        self.key_bindings.as_ref()
523    }
524
525    /// Sets the key bindings helper for the shell.
526    pub fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>) {
527        self.key_bindings = key_bindings;
528    }
529
530    /// Returns the shell's current working directory.
531    pub fn working_dir(&self) -> &Path {
532        &self.working_dir
533    }
534
535    /// Returns a mutable reference to the shell's current working directory.
536    /// This is only accessible within the crate.
537    pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf {
538        &mut self.working_dir
539    }
540
541    /// Returns the product display name for this shell.
542    pub fn product_display_str(&self) -> Option<&str> {
543        self.product_display_str.as_deref()
544    }
545}
546
547#[cfg(feature = "serde")]
548fn default_error_formatter<EF: extensions::ErrorFormatter>() -> EF {
549    EF::default()
550}