//! Module defining the core shell structure and behavior. use std::borrow::Cow; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::Mutex; use crate::{ ExecutionControlFlow, ExecutionResult, builtins, env::ShellEnvironment, error, extensions, functions, interfaces, jobs, keywords, openfiles, options::RuntimeOptions, pathcache, wellknownvars, }; /// Type for storing a key bindings helper. pub type KeyBindingsHelper = Arc>; /// Type alias for shell file descriptors. pub type ShellFd = i32; // NOTE: The submodule files below (e.g., `shell/traps.rs`, `shell/callstack.rs`) contain // `impl Shell` blocks that provide methods coordinating with types defined in the // corresponding top-level modules (e.g., `traps.rs`, `callstack.rs`). This is an intentional // layered architecture: top-level modules define domain types and data structures, while // shell/ submodules implement Shell methods that operate on those types. mod builder; mod builtin_registry; mod callstack; mod completion; mod env; mod execution; mod expansion; mod fs; mod funcs; mod history; mod initscripts; mod io; mod job_control; mod parsing; mod prompts; mod readline; mod state; mod traps; pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState}; pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior}; pub use state::ShellState; /// Represents an instance of a shell. /// /// # Type Parameters /// /// * `SE` - The shell extensions implementation to use. These extensions are statically injected /// into the shell at compile time to provide custom behavior. When unspecified, defaults to /// `DefaultShellExtensions`, which provide standard behavior. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Shell { /// Injected error behavior. #[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))] error_formatter: SE::ErrorFormatter, /// Injected process launcher. #[cfg_attr(feature = "serde", serde(skip, default = "default_process_launcher"))] process_launcher: SE::ProcessLauncher, /// Injected task spawner. #[cfg_attr(feature = "serde", serde(skip, default = "default_task_spawner"))] task_spawner: SE::TaskSpawner, /// The builtins' state; see [`crate::ExecutionContext::state_mut`]. #[cfg_attr(feature = "serde", serde(skip, default))] builtin_states: builtins::BuiltinStates, /// Trap handler configuration for the shell. traps: crate::traps::TrapHandlerConfig, /// Manages files opened and accessible via redirection operators. open_files: openfiles::OpenFiles, /// The current working directory. working_dir: PathBuf, /// The shell environment, containing shell variables. env: ShellEnvironment, /// Shell function definitions. funcs: functions::FunctionEnv, /// Runtime shell options. options: RuntimeOptions, /// State of managed jobs. /// TODO(serde): Need to warn somehow that jobs cannot be serialized. #[cfg_attr(feature = "serde", serde(skip))] jobs: jobs::JobManager, /// Shell aliases. aliases: HashMap, /// The status of the last completed command. last_exit_status: u8, /// Tracks changes to `last_exit_status`. Assignment-only commands observe it /// to tell whether an expansion set a status (see `interp.rs`), so code that /// rolls back `$?` must roll this back too. last_exit_status_change_count: usize, /// The status of each of the commands in the last pipeline. last_pipeline_statuses: Vec, /// Clone depth from the original ancestor shell. depth: usize, /// The number the `\#` prompt escape shows: 1, plus one for each command the /// interactive loop has run. command_number: usize, /// Shell name name: Option, /// Positional shell arguments (not including shell name). args: Vec, /// Shell version version: Option, /// Detailed display string for the shell product_display_str: Option, /// Function/script call stack. call_stack: crate::callstack::CallStack, /// Directory stack used by pushd et al. directory_stack: Vec, /// Programmable completion state. completion: crate::completion::State, /// Shell built-in commands. #[cfg_attr(feature = "serde", serde(skip))] builtins: builtins::Registry, /// Shell program location cache. program_location_cache: pathcache::PathCache, /// Last "SECONDS" captured time. last_stopwatch_time: std::time::SystemTime, /// Last "SECONDS" offset requested. last_stopwatch_offset: u32, /// Parser implementation to use. #[cfg_attr(feature = "serde", serde(skip))] parser_impl: crate::parser::ParserImpl, /// Key bindings for the shell, optionally implemented by an interactive shell. #[cfg_attr(feature = "serde", serde(skip))] key_bindings: Option, /// History of commands executed in the shell. history: Option, } impl Clone for Shell { fn clone(&self) -> Self { Self { error_formatter: self.error_formatter.clone(), process_launcher: self.process_launcher.clone(), task_spawner: self.task_spawner.clone(), builtin_states: self.builtin_states.clone(), traps: self.traps.clone(), open_files: self.open_files.clone(), working_dir: self.working_dir.clone(), env: self.env.clone(), funcs: self.funcs.clone(), options: self.options.clone(), jobs: jobs::JobManager::new(), aliases: self.aliases.clone(), last_exit_status: self.last_exit_status, last_exit_status_change_count: self.last_exit_status_change_count, last_pipeline_statuses: self.last_pipeline_statuses.clone(), name: self.name.clone(), args: self.args.clone(), version: self.version.clone(), product_display_str: self.product_display_str.clone(), call_stack: { // Subshells must not inherit the parent's "currently handling signal X" // state; otherwise a trap handler that spawns a subshell would see itself // as already inside that handler and skip re-entrant delivery. let mut cs = self.call_stack.clone(); cs.clear_active_trap_signals(); cs }, directory_stack: self.directory_stack.clone(), // A subshell of a completion function (e.g. `$(compgen ...)`) is in that // completion too. Like a forked subshell in bash, it changes only its own copy // (e.g. with `compopt`), which its parent doesn't see. completion: self.completion.clone(), builtins: self.builtins.clone(), program_location_cache: self.program_location_cache.clone(), last_stopwatch_time: self.last_stopwatch_time, last_stopwatch_offset: self.last_stopwatch_offset, parser_impl: self.parser_impl, key_bindings: self.key_bindings.clone(), history: self.history.clone(), depth: self.depth + 1, command_number: self.command_number, } } } impl AsRef for Shell { fn as_ref(&self) -> &Self { self } } impl AsMut for Shell { fn as_mut(&mut self) -> &mut Self { self } } impl Shell { /// Returns a new shell instance created with the given options. /// Does *not* load any configuration files (e.g., bashrc). /// /// # Arguments /// /// * `options` - The options to use when creating the shell. pub(crate) fn new(options: CreateOptions) -> Result { // Compute runtime options before moving fields out of `options`. let runtime_options = RuntimeOptions::defaults_from(&options); // Instantiate the shell with some defaults. let mut shell = Self { error_formatter: options.error_formatter, process_launcher: options.process_launcher, task_spawner: options.task_spawner, builtin_states: builtins::BuiltinStates::default(), open_files: openfiles::OpenFiles::new(), options: runtime_options, name: options.shell_name, args: options.shell_args.unwrap_or_default(), version: options.shell_version, product_display_str: options.shell_product_display_str, working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?, builtins: options.builtins, parser_impl: options.parser, key_bindings: options.key_bindings, ..Self::default() }; // Add in any open files provided. shell.open_files.update_from(options.fds.into_iter()); // TODO(patterns): Without this a script that sets extglob will fail because we // parse the entire script with the same settings. shell.options.extended_globbing = true; // If requested, seed parameters from environment. if !options.do_not_inherit_env { wellknownvars::inherit_env_vars(&mut shell)?; } // If requested, set well-known variables. if !options.skip_well_known_vars { wellknownvars::init_well_known_vars(&mut shell)?; } // Set any provided variables. for (var_name, var_value) in options.vars { shell.env.set_global(var_name, var_value)?; } // Set up history, if relevant. The history file itself is loaded later, once startup // files have run, since they may change HISTFILE. if shell.options.enable_command_history { shell.history = Some(crate::history::History::default()); } Ok(shell) } } impl Shell { /// Increments the interactive line offset in the shell by the indicated number /// of lines. The interactive loop calls this once after each command it runs, so /// it also advances the command number that the `\#` prompt escape shows. /// /// # Arguments /// /// * `delta` - The number of lines to increment the current line offset by. pub fn increment_interactive_line_offset(&mut self, delta: usize) { self.call_stack.increment_current_line_offset(delta); // shortcut: counts every line the loop runs, which bash doesn't for blank, comment-only // or unparsable ones or for a bound key; count where the reader loop parses, once it's // in core. self.command_number += 1; } /// Updates the currently executing command in the shell. pub fn set_current_cmd(&mut self, cmd: &impl brush_parser::ast::Node) { self.call_stack .set_current_pos(cmd.location().map(|span| span.start)); } /// Updates the `$_` shell variable (last-argument of the previous simple /// command). /// /// Passes `Some(last_arg)` to record the last argument of the just-executed /// command, or `None` to clear `$_` (used for assignment-only statements, /// which bash treats as having no "last argument"). /// /// The update is applied in-place so that attributes on `_` (notably /// `readonly`) are preserved: attempting to update a readonly `_` is a /// silent no-op, matching bash's observable stdout behavior. pub(crate) fn update_last_arg_variable(&mut self, last_arg: Option) { // Bash refuses to update a readonly `_`, emitting an error to stderr // on each attempt. We silently skip the update here — the observable // stdout effect ($_ stays unchanged) matches bash; the missing stderr // diagnostics are harmless. if self .env .get_using_policy("_", crate::env::EnvironmentLookup::Anywhere) .is_some_and(|v| v.is_readonly()) { return; } // Replace the variable entirely (fresh, non-exported). This matches // bash, which never exports `_` — even under `set -a` — and always // clears any previously-set attributes (except readonly, handled // above). let value = last_arg.unwrap_or_default(); let _ = self .env .set_global("_", crate::variables::ShellVariable::new(value)); } /// Captures the state the last command left behind; see [`SavedCommandStatus`]. pub fn save_command_status(&self) -> SavedCommandStatus { SavedCommandStatus { exit_status: self.last_exit_status, exit_status_change_count: self.last_exit_status_change_count, pipeline_statuses: self.last_pipeline_statuses.clone(), last_arg: self.env_str("_").map(|value| value.into_owned()), } } /// Reapplies a snapshot, exit-status change counter included. Consumes it; `clone` it to /// put the same one back more than once, e.g. between successive hook functions. /// /// # Arguments /// /// * `saved` - The snapshot to restore. pub fn restore_command_status(&mut self, saved: SavedCommandStatus) { self.last_pipeline_statuses = saved.pipeline_statuses; // Assigned directly rather than through `set_last_exit_status`, which would bump // the change counter we're about to put back. self.last_exit_status = saved.exit_status; self.last_exit_status_change_count = saved.exit_status_change_count; match saved.last_arg { Some(last_arg) => self.update_last_arg_variable(Some(last_arg)), // `_` was unset when the snapshot was taken, so put it back that way. Unsetting // a readonly `_` fails; ignore that, as `update_last_arg_variable` does. None => _ = self.env.unset("_"), } } /// Applies errexit semantics to a result if enabled and appropriate. /// This should be called at "statement boundaries" where errexit should be checked. /// /// # Arguments /// /// * `result` - The execution result to potentially modify. pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) { if self.options.exit_on_nonzero_command_exit && !result.is_success() && result.is_normal_flow() { result.next_control_flow = ExecutionControlFlow::ExitShell; } } /// Returns the keywords that are reserved by the shell, in the order bash lists them. pub(crate) fn get_keywords(&self) -> impl IntoIterator { keywords::keywords(self.options.sh_mode) } /// Returns the launcher that starts the processes the shell runs. pub const fn process_launcher(&self) -> &SE::ProcessLauncher { &self.process_launcher } /// Returns the spawner that starts the tasks the shell runs concurrently with /// itself. pub const fn task_spawner(&self) -> &SE::TaskSpawner { &self.task_spawner } /// Checks if the given string is a keyword reserved in this shell. /// /// # Arguments /// /// * `s` - The string to check. pub fn is_keyword(&self, s: &str) -> bool { keywords::keywords(self.options.sh_mode).any(|keyword| keyword == s) } pub(crate) const fn last_exit_status_change_count(&self) -> usize { self.last_exit_status_change_count } pub(crate) const fn command_number(&self) -> usize { self.command_number } } /// Snapshot of the state the last command left behind: `$?`, `PIPESTATUS`, and `$_`. /// /// Take one with [`Shell::save_command_status`] and put it back with /// [`Shell::restore_command_status`] around anything the user didn't type -- a shell hook, /// `PROMPT_COMMAND`, prompt expansion -- so it stays invisible to the next command. #[derive(Clone, Debug)] pub struct SavedCommandStatus { exit_status: u8, exit_status_change_count: usize, pipeline_statuses: Vec, last_arg: Option, } #[inherent::inherent] impl ShellState for Shell { /// Returns whether or not this shell is a subshell. pub fn is_subshell(&self) -> bool { self.depth > 0 } /// Returns the last "SECONDS" captured time. pub fn last_stopwatch_time(&self) -> std::time::SystemTime { self.last_stopwatch_time } /// Returns the last "SECONDS" offset requested. pub fn last_stopwatch_offset(&self) -> u32 { self.last_stopwatch_offset } /// Returns the shell environment containing variables. pub fn env(&self) -> &ShellEnvironment { &self.env } /// Returns a mutable reference to the shell environment. pub fn env_mut(&mut self) -> &mut ShellEnvironment { &mut self.env } /// Returns the shell's runtime options. pub fn options(&self) -> &RuntimeOptions { &self.options } /// Returns a mutable reference to the shell's runtime options. pub fn options_mut(&mut self) -> &mut RuntimeOptions { &mut self.options } /// Returns the shell's aliases. pub fn aliases(&self) -> &HashMap { &self.aliases } /// Returns a mutable reference to the shell's aliases. pub fn aliases_mut(&mut self) -> &mut HashMap { &mut self.aliases } /// Returns the shell's job manager. pub fn jobs(&self) -> &jobs::JobManager { &self.jobs } /// Returns a mutable reference to the shell's job manager. pub fn jobs_mut(&mut self) -> &mut jobs::JobManager { &mut self.jobs } /// Returns the shell's trap handler configuration. pub fn traps(&self) -> &crate::traps::TrapHandlerConfig { &self.traps } /// Returns a mutable reference to the shell's trap handler configuration. pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig { &mut self.traps } /// Returns the shell's directory stack. pub fn directory_stack(&self) -> &[PathBuf] { &self.directory_stack } /// Returns a mutable reference to the shell's directory stack. pub fn directory_stack_mut(&mut self) -> &mut Vec { &mut self.directory_stack } /// Returns the statuses of commands in the last pipeline. pub fn last_pipeline_statuses(&self) -> &[u8] { &self.last_pipeline_statuses } /// Returns a mutable reference to the statuses of commands in the last pipeline. pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec { &mut self.last_pipeline_statuses } /// Returns the shell's program location cache. pub fn program_location_cache(&self) -> &pathcache::PathCache { &self.program_location_cache } /// Returns a mutable reference to the shell's program location cache. pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache { &mut self.program_location_cache } /// Returns the shell's completion configuration. pub fn completion_config(&self) -> &crate::completion::Config { &self.completion.config } /// Returns a mutable reference to the shell's completion configuration. pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config { &mut self.completion.config } /// Returns the shell's open files. pub fn open_files(&self) -> &openfiles::OpenFiles { &self.open_files } /// Returns a mutable reference to the shell's open files. pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles { &mut self.open_files } /// Returns the *current* name of the shell ($0). /// Influenced by the current call stack. pub fn current_shell_name(&self) -> Option> { for frame in self.call_stack.iter() { // Executed scripts shadow the shell name. if frame.frame_type.is_run_script() { return Some(frame.frame_type.name()); } } self.name.as_deref().map(|name| name.into()) } /// Returns the current subshell depth; 0 is returned if this shell is not a subshell. pub fn depth(&self) -> usize { self.depth } /// Returns the call stack for the shell. pub fn call_stack(&self) -> &crate::callstack::CallStack { &self.call_stack } /// Returns the shell's history, if it exists. pub fn history(&self) -> Option<&crate::history::History> { self.history.as_ref() } /// Returns a mutable reference to the shell's history, if it exists. pub fn history_mut(&mut self) -> Option<&mut crate::history::History> { self.history.as_mut() } /// Returns the shell's official version string (if available). pub fn version(&self) -> Option<&str> { self.version.as_deref() } /// Returns the exit status of the last command executed in this shell. pub fn last_exit_status(&self) -> u8 { self.last_exit_status } /// Updates the last exit status, bumping `last_exit_status_change_count`. To *restore* a /// status rather than set one, use [`Self::restore_command_status`], which puts the /// counter back too. pub fn set_last_exit_status(&mut self, status: u8) { self.last_exit_status = status; self.last_exit_status_change_count += 1; } /// Returns the key bindings helper for the shell. pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> { self.key_bindings.as_ref() } /// Sets the key bindings helper for the shell. pub fn set_key_bindings(&mut self, key_bindings: Option) { self.key_bindings = key_bindings; } /// Returns the shell's current working directory. pub fn working_dir(&self) -> &Path { &self.working_dir } /// Returns a mutable reference to the shell's current working directory. /// This is only accessible within the crate. pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf { &mut self.working_dir } /// Returns the product display name for this shell. pub fn product_display_str(&self) -> Option<&str> { self.product_display_str.as_deref() } } #[cfg(feature = "serde")] fn default_error_formatter() -> EF { EF::default() } #[cfg(feature = "serde")] fn default_process_launcher() -> PL { PL::default() } #[cfg(feature = "serde")] fn default_task_spawner() -> TS { TS::default() } #[cfg(test)] #[allow(clippy::panic_in_result_fn, reason = "assertions in a fallible test")] mod tests { use super::*; /// `$_` round-trips through a snapshot, unset included, so an embedder that saves before /// running commands of its own gets back exactly what was there -- not an empty string. /// (The interactive loop can't reach the unset case: every command resets `$_`.) #[tokio::test] async fn saved_command_status_round_trips_last_arg() -> Result<(), error::Error> { let mut shell = Shell::builder() .profile(ProfileLoadBehavior::Skip) .rc(RcLoadBehavior::Skip) .build() .await?; shell.update_last_arg_variable(Some(String::from("saved"))); let with_value = shell.save_command_status(); shell.env.unset("_")?; let while_unset = shell.save_command_status(); shell.restore_command_status(with_value); assert_eq!(shell.env_str("_").as_deref(), Some("saved")); shell.restore_command_status(while_unset); assert_eq!(shell.env_str("_"), None); Ok(()) } #[tokio::test] async fn unset_env_str_contract_is_independent_of_ifs_default() -> Result<(), error::Error> { let mut shell = Shell::builder() .profile(ProfileLoadBehavior::Skip) .rc(RcLoadBehavior::Skip) .build() .await?; shell.env.set_global( "IFS", crate::ShellVariable::new(crate::ShellValue::Unset( crate::variables::ShellValueUnsetType::Untyped, )), )?; assert_eq!(shell.env_str("IFS").as_deref(), Some("")); assert_eq!(shell.ifs(), " \t\n"); shell.env.set_global("IFS", crate::ShellVariable::new(""))?; assert_eq!(shell.env_str("IFS").as_deref(), Some("")); assert_eq!(shell.ifs(), ""); Ok(()) } }