Merge nucleic/sleek-thistle-egret-fyej into dev

This commit is contained in:
2026-07-18 05:19:31 -07:00
commit b6be87b72d
677 changed files with 102939 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
//! Module defining the builder for creating shell instances.
use std::{collections::HashMap, path::PathBuf};
pub use shell_builder::State as ShellBuilderState;
use super::Shell;
use crate::{
ProfileLoadBehavior, RcLoadBehavior, ShellFd, ShellVariable, builtins, callstack, completion,
env, error, extensions, functions, jobs, openfiles, options, pathcache,
shell::KeyBindingsHelper, traps,
};
impl<SE: extensions::ShellExtensions, S: shell_builder::IsComplete> ShellBuilder<SE, S> {
/// Returns a new shell instance created with the options provided. Runs any
/// configuration loading as well.
pub async fn build(self) -> Result<Shell<SE>, error::Error> {
let mut options = self.build_settings();
let profile = std::mem::take(&mut options.profile);
let rc = std::mem::take(&mut options.rc);
// Construct the shell.
let mut shell = Shell::new(options)?;
// Load profiles/configuration, unless skipped.
if !profile.skip() || !rc.skip() {
shell.load_config(&profile, &rc).await?;
}
Ok(shell)
}
}
impl<SE: extensions::ShellExtensions, S: shell_builder::State> ShellBuilder<SE, S> {
/// Add a disabled option
pub fn disable_option(mut self, option: impl Into<String>) -> Self {
self.disabled_options.push(option.into());
self
}
/// Add an enabled option
pub fn enable_option(mut self, option: impl Into<String>) -> Self {
self.enabled_options.push(option.into());
self
}
/// Add many disabled options
pub fn disable_options(mut self, options: impl IntoIterator<Item: Into<String>>) -> Self {
self.disabled_options
.extend(options.into_iter().map(Into::into));
self
}
/// Add many enabled options
pub fn enable_options(mut self, options: impl IntoIterator<Item: Into<String>>) -> Self {
self.enabled_options
.extend(options.into_iter().map(Into::into));
self
}
/// Add a disabled shopt option
pub fn disable_shopt_option(mut self, option: impl Into<String>) -> Self {
self.disabled_shopt_options.push(option.into());
self
}
/// Add an enabled shopt option
pub fn enable_shopt_option(mut self, option: impl Into<String>) -> Self {
self.enabled_shopt_options.push(option.into());
self
}
/// Add many disabled shopt options
pub fn disable_shopt_options(mut self, options: impl IntoIterator<Item: Into<String>>) -> Self {
self.disabled_shopt_options
.extend(options.into_iter().map(Into::into));
self
}
/// Add many enabled shopt options
pub fn enable_shopt_options(mut self, options: impl IntoIterator<Item: Into<String>>) -> Self {
self.enabled_shopt_options
.extend(options.into_iter().map(Into::into));
self
}
/// Add a single builtin registration
pub fn builtin(mut self, name: impl Into<String>, reg: builtins::Registration<SE>) -> Self {
self.builtins.insert(name.into(), reg);
self
}
/// Add many builtin registrations
pub fn builtins(
mut self,
builtins: impl IntoIterator<Item = (String, builtins::Registration<SE>)>,
) -> Self {
self.builtins.extend(builtins);
self
}
/// Adds a single variable to be initialized in the shell.
pub fn var(mut self, name: impl Into<String>, variable: ShellVariable) -> Self {
self.vars.insert(name.into(), variable);
self
}
}
/// Options for creating a new shell.
#[derive(Default, bon::Builder)]
#[builder(
builder_type(
name = ShellBuilder,
doc {
/// Builder for [Shell]
}),
finish_fn(
name = build_settings,
vis = "pub(self)",
),
start_fn(
vis = "pub(self)"
)
)]
pub struct CreateOptions<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
/// Disabled options.
#[builder(field)]
pub disabled_options: Vec<String>,
/// Enabled options.
#[builder(field)]
pub enabled_options: Vec<String>,
/// Disabled shopt options.
#[builder(field)]
pub disabled_shopt_options: Vec<String>,
/// Enabled shopt options.
#[builder(field)]
pub enabled_shopt_options: Vec<String>,
/// Registered builtins.
#[builder(field)]
pub builtins: HashMap<String, builtins::Registration<SE>>,
/// Provides a set of variables to be initialized in the shell. If present, they
/// are assigned *after* inherited or well-known variables are set (when applicable).
#[builder(field)]
pub vars: HashMap<String, ShellVariable>,
/// Error behavior implementation.
#[builder(default)]
pub error_formatter: SE::ErrorFormatter,
/// Disallow overwriting regular files via output redirection.
#[builder(default)]
pub disallow_overwriting_regular_files_via_output_redirection: bool,
/// Do not execute commands.
#[builder(default)]
pub do_not_execute_commands: bool,
/// Exit after one command.
#[builder(default)]
pub exit_after_one_command: bool,
/// Whether the shell is interactive.
#[builder(default)]
pub interactive: bool,
/// Whether the shell is a login shell.
#[builder(default)]
pub login: bool,
/// Whether to skip using a readline-like interface for input.
#[builder(default)]
pub no_editing: bool,
/// System profile loading behavior.
#[builder(default)]
pub profile: ProfileLoadBehavior,
/// Rc file loading behavior.
#[builder(default)]
pub rc: RcLoadBehavior,
/// Whether to skip inheriting environment variables from the calling process.
#[builder(default)]
pub do_not_inherit_env: bool,
/// Whether to skip initializing well-known variables.
#[builder(default)]
pub skip_well_known_vars: bool,
/// Provides a set of initial open files to be tracked by the shell.
#[builder(default)]
pub fds: HashMap<ShellFd, openfiles::OpenFile>,
/// Whether to launch external commands as session leaders.
#[builder(default)]
pub external_cmd_leads_session: bool,
/// Initial working dir for the shell. If left unspecified, will be populated from
/// the host environment.
pub working_dir: Option<PathBuf>,
/// Whether the shell is in POSIX compliance mode.
#[builder(default)]
pub posix: bool,
/// Whether to print commands and arguments as they are read.
#[builder(default)]
pub print_commands_and_arguments: bool,
/// Whether commands are being read from stdin.
#[builder(default)]
pub read_commands_from_stdin: bool,
/// The name of the shell.
pub shell_name: Option<String>,
/// Base positional arguments for the shell (not including the shell name).
pub shell_args: Option<Vec<String>>,
/// Optionally provides a display string describing the version and variant of the shell.
pub shell_product_display_str: Option<String>,
/// Whether to run in maximal POSIX sh compatibility mode.
#[builder(default)]
pub sh_mode: bool,
/// Whether to treat expansion of unset variables as an error.
#[builder(default)]
pub treat_unset_variables_as_error: bool,
/// Whether to enable error-on-exit behavior.
#[builder(default)]
pub exit_on_nonzero_command_exit: bool,
/// Whether to disable pathname expansion.
#[builder(default)]
pub disable_pathname_expansion: bool,
/// Whether to print verbose output.
#[builder(default)]
pub verbose: bool,
/// Parser implementation to use.
#[builder(default)]
pub parser: crate::parser::ParserImpl,
/// Whether the shell is in command string mode (-c).
#[builder(default)]
pub command_string_mode: bool,
/// Maximum function call depth.
pub max_function_call_depth: Option<usize>,
/// Key bindings helper for the shell to use.
pub key_bindings: Option<KeyBindingsHelper>,
/// Brush implementation version.
pub shell_version: Option<String>,
}
impl<SE: extensions::ShellExtensions> Default for Shell<SE> {
fn default() -> Self {
Self {
error_formatter: SE::ErrorFormatter::default(),
traps: traps::TrapHandlerConfig::default(),
open_files: openfiles::OpenFiles::default(),
working_dir: PathBuf::default(),
env: env::ShellEnvironment::default(),
funcs: functions::FunctionEnv::default(),
options: options::RuntimeOptions::default(),
jobs: jobs::JobManager::default(),
aliases: HashMap::default(),
last_exit_status: 0,
last_exit_status_change_count: 0,
last_pipeline_statuses: vec![0],
depth: 0,
name: None,
args: vec![],
version: None,
product_display_str: None,
call_stack: callstack::CallStack::new(),
directory_stack: vec![],
completion_config: completion::Config::default(),
builtins: HashMap::default(),
program_location_cache: pathcache::PathCache::default(),
last_stopwatch_time: std::time::SystemTime::now(),
last_stopwatch_offset: 0,
parser_impl: crate::parser::ParserImpl::default(),
key_bindings: None,
history: None,
}
}
}
impl Shell {
/// Create an instance of [Shell] using the builder syntax
pub fn builder() -> ShellBuilder<extensions::DefaultShellExtensions, shell_builder::Empty> {
CreateOptions::builder()
}
/// Create an instance of [Shell] using the builder syntax, with custom extensions.
pub fn builder_with_extensions<SE: extensions::ShellExtensions>()
-> ShellBuilder<SE, shell_builder::Empty> {
CreateOptions::builder()
}
}
+51
View File
@@ -0,0 +1,51 @@
//! Builtin command management for shell instances.
use std::collections::HashMap;
use crate::{builtins, extensions};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Register a builtin to the shell's environment, replacing any existing
/// registration with the same name.
///
/// # Arguments
///
/// * `name` - The in-shell name of the builtin.
/// * `registration` - The registration handle for the builtin.
pub fn register_builtin<S: Into<String>>(
&mut self,
name: S,
registration: builtins::Registration<SE>,
) {
self.builtins.insert(name.into(), registration);
}
/// Register a builtin only if no builtin with that name is already registered.
///
/// # Arguments
///
/// * `name` - The in-shell name of the builtin.
/// * `registration` - The registration handle for the builtin.
pub fn register_builtin_if_unset<S: Into<String>>(
&mut self,
name: S,
registration: builtins::Registration<SE>,
) {
self.builtins.entry(name.into()).or_insert(registration);
}
/// Tries to retrieve a mutable reference to an existing builtin registration.
/// Returns `None` if no such registration exists.
///
/// # Arguments
///
/// * `name` - The name of the builtin to lookup.
pub fn builtin_mut(&mut self, name: &str) -> Option<&mut builtins::Registration<SE>> {
self.builtins.get_mut(name)
}
/// Returns the registered builtins for the shell.
pub const fn builtins(&self) -> &HashMap<String, builtins::Registration<SE>> {
&self.builtins
}
}
+186
View File
@@ -0,0 +1,186 @@
//! Call stack management for the shell.
use crate::{ExecutionParameters, callstack, env, error, functions, trace_categories};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
/// Returns whether or not the shell is actively executing in a sourced script.
pub fn in_sourced_script(&self) -> bool {
self.call_stack.in_sourced_script()
}
/// Returns whether or not the shell is actively executing in a shell function.
pub fn in_function(&self) -> bool {
self.call_stack.in_function()
}
/// Updates the shell's internal tracking state to reflect that a new interactive
/// session is being started.
pub fn start_interactive_session(&mut self) -> Result<(), error::Error> {
self.call_stack.push_interactive_session();
Ok(())
}
/// Updates the shell's internal tracking state to reflect that the current
/// interactive session is ending.
pub fn end_interactive_session(&mut self) -> Result<(), error::Error> {
if self
.call_stack
.current_frame()
.is_none_or(|frame| !frame.frame_type.is_interactive_session())
{
return Err(error::ErrorKind::NotInInteractiveSession.into());
}
self.call_stack.pop();
Ok(())
}
/// Updates the shell's internal tracking state to reflect that command
/// string mode is being started.
pub fn start_command_string_mode(&mut self) {
self.call_stack.push_command_string();
}
/// Updates the shell's internal tracking state to reflect that command
/// string mode is ending.
pub fn end_command_string_mode(&mut self) -> Result<(), error::Error> {
if self
.call_stack
.current_frame()
.is_none_or(|frame| !frame.frame_type.is_command_string())
{
return Err(error::ErrorKind::NotExecutingCommandString.into());
}
self.call_stack.pop();
Ok(())
}
pub(crate) fn enter_trap_handler(
&mut self,
signal: crate::traps::TrapSignal,
handler: Option<&crate::traps::TrapHandler>,
) {
self.call_stack.push_trap_handler(signal, handler);
}
pub(crate) fn leave_trap_handler(&mut self) {
self.call_stack.pop();
}
/// Acquires a block on trap delivery, preventing traps from being delivered until
/// the block is released. Multiple blocks may be acquired, and trap delivery will
/// remain suppressed until all blocks have been released.
pub(crate) const fn acquire_trap_delivery_block(&mut self) {
self.call_stack.acquire_trap_delivery_block();
}
/// Releases a block on trap delivery; note that trap delivery will remain
/// suppressed until all blocks have been released.
pub(crate) const fn release_trap_delivery_block(&mut self) {
self.call_stack.release_trap_delivery_block();
}
/// Updates the shell's internal tracking state to reflect that a new shell
/// function is being entered.
///
/// # Arguments
///
/// * `name` - The name of the function being entered.
/// * `function` - The function being entered.
/// * `args` - The arguments being passed to the function.
/// * `_params` - Current execution parameters.
pub(crate) fn enter_function(
&mut self,
name: &str,
function: &functions::Registration,
args: impl IntoIterator<Item = String>,
_params: &ExecutionParameters,
) -> Result<(), error::Error> {
if let Some(max_call_depth) = self.options.max_function_call_depth
&& self.call_stack.function_call_depth() >= max_call_depth
{
return Err(error::ErrorKind::MaxFunctionCallDepthExceeded.into());
}
if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
let depth = self.call_stack.function_call_depth();
let prefix = repeated_char_str(' ', depth);
tracing::debug!(target: trace_categories::FUNCTIONS, "Entering func [depth={depth}]: {prefix}{name}");
}
self.call_stack.push_function(name, function, args);
self.env.push_scope(env::EnvironmentScope::Local);
Ok(())
}
/// Updates the shell's internal tracking state to reflect that the shell
/// has exited the top-most function on its call stack.
pub(crate) fn leave_function(&mut self) -> Result<(), error::Error> {
self.env.pop_scope(env::EnvironmentScope::Local)?;
if let Some(exited_call) = self.call_stack.pop() {
if let callstack::FrameType::Function(func_call) = exited_call.frame_type {
if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
let depth = self.call_stack.function_call_depth();
let prefix = repeated_char_str(' ', depth);
tracing::debug!(target: trace_categories::FUNCTIONS, "Exiting func [depth={depth}]: {prefix}{}", func_call.function_name);
}
} else {
let err: error::Error =
error::ErrorKind::InternalError("mismatched call stack state".to_owned())
.into();
return Err(err.into_fatal());
}
}
Ok(())
}
/// Returns the *current* positional arguments for the shell ($1 and beyond).
/// Influenced by the current call stack.
pub fn current_shell_args(&self) -> &[String] {
for frame in self.call_stack.iter() {
match frame.frame_type {
// Function calls always shadow positional parameters.
crate::callstack::FrameType::Function(..) => return &frame.args,
// Executed scripts always shadow positional parameters.
_ if frame.frame_type.is_run_script() => return &frame.args,
// Sourced scripts shadow positional parameters if they have arguments.
_ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
return &frame.args;
}
_ => (),
}
}
self.args.as_slice()
}
/// Returns a mutable reference to *current* positional parameters for the shell
/// ($1 and beyond).
pub fn current_shell_args_mut(&mut self) -> &mut Vec<String> {
for frame in self.call_stack.iter_mut() {
match frame.frame_type {
// Function calls always shadow positional parameters.
crate::callstack::FrameType::Function(..) => return &mut frame.args,
// Executed scripts always shadow positional parameters.
_ if frame.frame_type.is_run_script() => return &mut frame.args,
// Sourced scripts shadow positional parameters if they have arguments.
_ if frame.frame_type.is_sourced_script() && !frame.args.is_empty() => {
return &mut frame.args;
}
_ => (),
}
}
&mut self.args
}
}
fn repeated_char_str(c: char, count: usize) -> String {
(0..count).map(|_| c).collect()
}
+22
View File
@@ -0,0 +1,22 @@
//! Command completion support for shell instances.
use crate::{completion, error, extensions};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Generates command completions for the shell.
///
/// # Arguments
///
/// * `input` - The input string to generate completions for.
/// * `position` - The position in the input string to generate completions at.
pub async fn complete(
&mut self,
input: &str,
position: usize,
) -> Result<completion::Completions, error::Error> {
let completion_config = self.completion_config.clone();
completion_config
.get_completions(self, input, position)
.await
}
}
+36
View File
@@ -0,0 +1,36 @@
//! Environment support for shell.
use std::borrow::Cow;
use crate::{ShellVariable, error};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
/// Tries to retrieve a variable from the shell's environment, converting it into its
/// string form.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn env_str(&self, name: &str) -> Option<Cow<'_, str>> {
self.env.get_str(name, self)
}
/// Tries to retrieve a variable from the shell's environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn env_var(&self, name: &str) -> Option<&ShellVariable> {
self.env.get(name).map(|(_, var)| var)
}
/// Tries to set a global variable in the shell's environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to add.
/// * `var` - The variable contents to add.
pub fn set_env_global(&mut self, name: &str, var: ShellVariable) -> Result<(), error::Error> {
self.env.set_global(name, var)
}
}
+285
View File
@@ -0,0 +1,285 @@
//! Execution support for shell.
use std::{io::Read, path::Path};
use crate::{
ExecutionControlFlow, ExecutionParameters, ExecutionResult, ProcessGroupPolicy, SourceInfo,
arithmetic::Evaluatable as _, callstack, error, interp::Execute as _, openfiles,
trace_categories,
};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
/// Returns the default execution parameters for this shell.
pub fn default_exec_params(&self) -> ExecutionParameters {
let mut params = ExecutionParameters::default();
params.process_group_policy = if self.options.enable_job_control {
ProcessGroupPolicy::NewProcessGroup
} else {
ProcessGroupPolicy::SameProcessGroup
};
params
}
pub(super) async fn source_if_exists(
&mut self,
path: impl AsRef<Path>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
let path = path.as_ref();
if path.exists() {
self.source_script(path, std::iter::empty::<String>(), params)
.await?;
Ok(true)
} else {
tracing::debug!("skipping non-existent file: {}", path.display());
Ok(false)
}
}
/// Source the given file as a shell script, returning the execution result.
///
/// # Arguments
///
/// * `path` - The path to the file to source.
/// * `args` - The arguments to pass to the script as positional parameters.
/// * `params` - Execution parameters.
pub async fn source_script<S: Into<String>, P: AsRef<Path>, I: Iterator<Item = S>>(
&mut self,
path: P,
args: I,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
self.parse_and_execute_script_file(
path.as_ref(),
args,
params,
callstack::ScriptCallType::Source,
)
.await
}
/// Parse and execute the given file as a shell script, returning the execution result.
///
/// # Arguments
///
/// * `path` - The path to the file to source.
/// * `args` - The arguments to pass to the script as positional parameters.
/// * `params` - Execution parameters.
/// * `call_type` - The type of script call being made.
async fn parse_and_execute_script_file<
S: Into<String>,
P: AsRef<Path>,
I: Iterator<Item = S>,
>(
&mut self,
path: P,
args: I,
params: &ExecutionParameters,
call_type: callstack::ScriptCallType,
) -> Result<ExecutionResult, error::Error> {
let path = path.as_ref();
tracing::debug!("sourcing: {}", path.display());
let mut options = std::fs::File::options();
options.read(true);
let opened_file: openfiles::OpenFile = self
.open_file(&options, path, params)
.map_err(|e| error::ErrorKind::FailedSourcingFile(path.to_owned(), e))?;
if opened_file.is_dir() {
return Err(error::ErrorKind::FailedSourcingFile(
path.to_owned(),
std::io::Error::from(std::io::ErrorKind::IsADirectory),
)
.into());
}
let source_info = crate::SourceInfo::from(path.to_owned());
let mut result = self
.source_file(opened_file, &source_info, args, params, call_type)
.await?;
// Handle control flow at script execution boundary. If execution completed
// with a `return`, we need to clear it since it's already been "used". All
// other control flow types are preserved.
if matches!(
result.next_control_flow,
ExecutionControlFlow::ReturnFromFunctionOrScript
) {
result.next_control_flow = ExecutionControlFlow::Normal;
}
Ok(result)
}
/// Source the given file as a shell script, returning the execution result.
///
/// # Arguments
///
/// * `file` - The file to source.
/// * `source_info` - Information about the source of the script.
/// * `args` - The arguments to pass to the script as positional parameters.
/// * `params` - Execution parameters.
/// * `call_type` - The type of script call being made.
async fn source_file<F: Read, S: Into<String>, I: Iterator<Item = S>>(
&mut self,
file: F,
source_info: &crate::SourceInfo,
args: I,
params: &ExecutionParameters,
call_type: callstack::ScriptCallType,
) -> Result<ExecutionResult, error::Error> {
let mut reader = std::io::BufReader::new(file);
let mut parser = brush_parser::Parser::new(&mut reader, &self.parser_options());
tracing::debug!(target: trace_categories::PARSE, "Parsing sourced file: {}", source_info.source);
let parse_result = parser.parse_program();
let script_positional_args = args.map(Into::into);
self.call_stack
.push_script(call_type, source_info, script_positional_args);
let result = self
.run_parsed_result(parse_result, source_info, params)
.await;
self.call_stack.pop();
result
}
/// Executes the given string as a shell program, returning the resulting exit status.
///
/// # Arguments
///
/// * `command` - The command to execute.
/// * `source_info` - Information about the source of the command text.
/// * `params` - Execution parameters.
pub async fn run_string<S: Into<String>>(
&mut self,
command: S,
source_info: &crate::SourceInfo,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
let parse_result = self.parse_string(command);
self.run_parsed_result(parse_result, source_info, params)
.await
}
/// Executes the given command, provided to a shell executable on the command
/// line (i.e., via `-c`).
///
/// It is expected that the shell will not be used for any further execution
/// after this command; this function will perform any necessary shell exit
/// handling.
///
/// # Arguments
///
/// * `command` - The command to execute.
pub async fn run_dash_c_command<S: Into<String>>(
&mut self,
command: S,
) -> Result<ExecutionResult, error::Error> {
self.start_command_string_mode();
// Execute the command string.
let params = self.default_exec_params();
let source_info = SourceInfo::from("-c");
let result = self.run_string(command, &source_info, &params).await?;
self.end_command_string_mode()?;
// Give the shell a chance to run on-exit tasks, but ignore the result.
let _ = self.on_exit().await;
Ok(result)
}
/// Executes the given script file, returning the resulting exit status.
///
/// It is expected that the shell will not be used for any further execution
/// after this command; this function will perform any necessary shell exit
/// handling.
///
/// # Arguments
///
/// * `script_path` - The path to the script file to execute.
/// * `args` - The arguments to pass to the script as positional parameters.
pub async fn run_script<S: Into<String>, P: AsRef<Path>, I: Iterator<Item = S>>(
&mut self,
script_path: P,
args: I,
) -> Result<ExecutionResult, error::Error> {
let params = self.default_exec_params();
let result = self
.parse_and_execute_script_file(
script_path.as_ref(),
args,
&params,
callstack::ScriptCallType::Run,
)
.await?;
// Give the shell a chance to run on-exit tasks, but ignore the result.
let _ = self.on_exit().await;
Ok(result)
}
pub(crate) async fn run_parsed_result(
&mut self,
parse_result: Result<brush_parser::ast::Program, brush_parser::ParseError>,
source_info: &crate::SourceInfo,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
// If parsing succeeded, run the program. If there's a parse error, it's fatal (per spec).
let result = match parse_result {
Ok(prog) => self.run_program(prog, params).await,
Err(parse_err) => Err(error::Error::from(error::ErrorKind::ParseError(
parse_err,
source_info.clone(),
))
.into_fatal()),
};
// Report any errors.
match result {
Ok(result) => Ok(result),
Err(err) => {
let _ = self.display_error(&mut params.stderr(self), &err);
let result = err.into_result(self);
self.set_last_exit_status(result.exit_code.into());
Ok(result)
}
}
}
/// Executes the given parsed shell program, returning the resulting exit status.
///
/// # Arguments
///
/// * `program` - The program to execute.
/// * `params` - Execution parameters.
pub async fn run_program(
&mut self,
program: brush_parser::ast::Program,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
program.execute(self, params).await
}
/// Evaluate the given arithmetic expression, returning the result.
pub fn eval_arithmetic(
&mut self,
expr: &brush_parser::ast::ArithmeticExpr,
) -> Result<i64, error::Error> {
Ok(expr.eval(self)?)
}
}
+46
View File
@@ -0,0 +1,46 @@
//! Expansion support for shell instances.
use std::borrow::Cow;
use crate::{error, expansion, extensions, interp::ExecutionParameters};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Returns the current value of the IFS variable, or the default value if it is not set.
pub fn ifs(&self) -> Cow<'_, str> {
self.env_str("IFS").unwrap_or_else(|| " \t\n".into())
}
/// Returns the first character of the IFS variable, or a space if it is not set.
pub(crate) fn get_ifs_first_char(&self) -> char {
self.ifs().chars().next().unwrap_or(' ')
}
/// Applies basic shell expansion to the provided string.
///
/// # Arguments
///
/// * `s` - The string to expand.
pub async fn basic_expand_string<S: AsRef<str>>(
&mut self,
params: &ExecutionParameters,
s: S,
) -> Result<String, error::Error> {
let result = expansion::basic_expand_word(self, params, s.as_ref()).await?;
Ok(result)
}
/// Applies full shell expansion and field splitting to the provided string; returns
/// a sequence of fields.
///
/// # Arguments
///
/// * `s` - The string to expand and split.
pub async fn full_expand_and_split_string<S: AsRef<str>>(
&mut self,
params: &ExecutionParameters,
s: S,
) -> Result<Vec<String>, error::Error> {
let result = expansion::full_expand_and_split_word(self, params, s.as_ref()).await?;
Ok(result)
}
}
+229
View File
@@ -0,0 +1,229 @@
//! Filesystem interaction in the shell.
use std::path::{Path, PathBuf};
use normalize_path::NormalizePath as _;
use crate::{
ExecutionParameters, ShellFd,
env::{EnvironmentLookup, EnvironmentScope},
error, openfiles, pathsearch,
sys::{fs::PathExt as _, users},
variables,
};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
/// Sets the shell's current working directory to the given path.
///
/// # Arguments
///
/// * `target_dir` - The path to set as the working directory.
pub fn set_working_dir(&mut self, target_dir: impl AsRef<Path>) -> Result<(), error::Error> {
let abs_path = self.absolute_path(target_dir.as_ref());
match std::fs::metadata(&abs_path) {
Ok(m) => {
if !m.is_dir() {
return Err(error::ErrorKind::NotADirectory(abs_path).into());
}
}
Err(e) => {
return Err(e.into());
}
}
// Normalize the path (but don't canonicalize it).
let cleaned_path = abs_path.normalize();
let pwd = cleaned_path.to_string_lossy().to_string();
self.env.update_or_add(
"PWD",
variables::ShellValueLiteral::Scalar(pwd),
|_| Ok(()),
EnvironmentLookup::Anywhere,
EnvironmentScope::Global,
)?;
let oldpwd = std::mem::replace(self.working_dir_mut(), cleaned_path);
self.env.update_or_add(
"OLDPWD",
variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
|_| Ok(()),
EnvironmentLookup::Anywhere,
EnvironmentScope::Global,
)?;
Ok(())
}
/// Tilde-shortens the given string, replacing the user's home directory with a tilde.
///
/// # Arguments
///
/// * `s` - The string to shorten.
pub fn tilde_shorten(&self, s: String) -> String {
if let Some(home_dir) = self.home_dir()
&& let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref())
{
return format!("~{stripped}");
}
s
}
/// Returns the shell's current home directory, if available.
pub(crate) fn home_dir(&self) -> Option<PathBuf> {
if let Some(home) = self.env.get_str("HOME", self) {
Some(PathBuf::from(home.to_string()))
} else {
// HOME isn't set, so let's sort it out ourselves.
users::get_current_user_home_dir()
}
}
/// Finds executables in the shell's current default PATH, matching the given glob pattern.
///
/// # Arguments
///
/// * `required_glob_pattern` - The glob pattern to match against.
pub fn find_executables_in_path<'a>(
&'a self,
filename: &'a str,
) -> impl Iterator<Item = PathBuf> + 'a {
let path_var = self.env.get_str("PATH", self).unwrap_or_default();
let paths = crate::sys::fs::split_paths(path_var.as_ref());
pathsearch::search_for_executable(paths, filename)
}
/// Finds executables in the shell's current default PATH, with filenames matching the
/// given prefix.
///
/// # Arguments
///
/// * `filename_prefix` - The prefix to match against executable filenames.
pub fn find_executables_in_path_with_prefix(
&self,
filename_prefix: &str,
case_insensitive: bool,
) -> impl Iterator<Item = PathBuf> {
let path_var = self.env.get_str("PATH", self).unwrap_or_default();
let paths = crate::sys::fs::split_paths(path_var.as_ref());
pathsearch::search_for_executable_with_prefix(paths, filename_prefix, case_insensitive)
}
/// Determines whether the given filename is the name of an executable in one of the
/// directories in the shell's current PATH. If found, returns the path.
///
/// # Arguments
///
/// * `candidate_name` - The name of the file to look for.
pub fn find_first_executable_in_path<S: AsRef<str>>(
&self,
candidate_name: S,
) -> Option<PathBuf> {
let path = self.env_str("PATH").unwrap_or_default();
for one_dir in crate::sys::fs::split_paths(path.as_ref()) {
let candidate_path = one_dir.join(candidate_name.as_ref());
if candidate_path.executable() {
return Some(candidate_path);
}
}
None
}
/// Uses the shell's hash-based path cache to check whether the given filename is the name
/// of an executable in one of the directories in the shell's current PATH. If found,
/// ensures the path is in the cache and returns it.
///
/// # Arguments
///
/// * `candidate_name` - The name of the file to look for.
pub fn find_first_executable_in_path_using_cache<S: AsRef<str>>(
&mut self,
candidate_name: S,
) -> Option<PathBuf>
where
String: From<S>,
{
if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
Some(cached_path)
} else if let Some(found_path) = self.find_first_executable_in_path(&candidate_name) {
self.program_location_cache
.set(candidate_name, found_path.clone());
Some(found_path)
} else {
None
}
}
/// Gets the absolute form of the given path.
///
/// # Arguments
///
/// * `path` - The path to get the absolute form of.
pub fn absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
if path.as_os_str().is_empty() || path.is_absolute() {
path.to_owned()
} else {
self.working_dir().join(path)
}
}
/// Opens the given file, using the context of this shell and the provided execution parameters.
///
/// # Arguments
///
/// * `options` - The options to use opening the file.
/// * `path` - The path to the file to open; may be relative to the shell's working directory.
/// * `params` - Execution parameters.
pub(crate) fn open_file(
&self,
options: &std::fs::OpenOptions,
path: impl AsRef<Path>,
params: &ExecutionParameters,
) -> Result<openfiles::OpenFile, std::io::Error> {
// Give platform-specific code a chance to handle special files
// (e.g. /dev/null on Windows, which needs to open NUL instead).
// This is checked before absolute_path so that paths like /dev/null
// are intercepted on platforms where they aren't valid native paths.
if let Some(result) = crate::sys::fs::try_open_special_file(path.as_ref()) {
return result.map(openfiles::OpenFile::from);
}
let path_to_open = self.absolute_path(path.as_ref());
// See if this is a reference to a file descriptor, in which case the actual
// /dev/fd* file path for this process may not match with what's in the execution
// parameters.
if let Some(parent) = path_to_open.parent()
&& parent == Path::new("/dev/fd")
&& let Some(filename) = path_to_open.file_name()
&& let Ok(fd_num) = filename.to_string_lossy().to_string().parse::<ShellFd>()
&& let Some(open_file) = params.try_fd(self, fd_num)
{
return open_file.try_clone();
}
Ok(options.open(path_to_open)?.into())
}
/// Replaces the shell's currently configured open files with the given set.
/// Typically only used by exec-like builtins.
///
/// # Arguments
///
/// * `open_files` - The new set of open files to use.
pub fn replace_open_files(
&mut self,
open_fds: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
) {
self.open_files = openfiles::OpenFiles::from(open_fds);
}
pub(crate) const fn persistent_open_files(&self) -> &openfiles::OpenFiles {
&self.open_files
}
}
+129
View File
@@ -0,0 +1,129 @@
//! Function support for shells.
use crate::{
ExecutionParameters, commands, error, extensions, functions, results::ExecutionWaitResult,
};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Returns the function definition environment for this shell.
pub const fn funcs(&self) -> &functions::FunctionEnv {
&self.funcs
}
/// Returns a mutable reference to the function definition environment for this shell.
pub const fn funcs_mut(&mut self) -> &mut functions::FunctionEnv {
&mut self.funcs
}
/// Tries to undefine a function in the shell's environment. Returns whether or
/// not a definition was removed.
///
/// # Arguments
///
/// * `name` - The name of the function to undefine.
pub fn undefine_func(&mut self, name: &str) -> bool {
self.funcs.remove(name).is_some()
}
/// Defines a function in the shell's environment. If a function already exists
/// with the given name, it is replaced with the new definition.
///
/// # Arguments
///
/// * `name` - The name of the function to define.
/// * `definition` - The function's definition.
/// * `source_info` - Source information for the function definition.
pub fn define_func(
&mut self,
name: impl Into<String>,
definition: brush_parser::ast::FunctionDefinition,
source_info: &crate::SourceInfo,
) {
let reg = functions::Registration::new(definition, source_info);
self.funcs.update(name.into(), reg);
}
/// Tries to return a mutable reference to the registration for a named function.
/// Returns `None` if no such function was found.
///
/// # Arguments
///
/// * `name` - The name of the function to lookup
pub fn func_mut(&mut self, name: &str) -> Option<&mut functions::Registration> {
self.funcs.get_mut(name)
}
/// Tries to define a function in the shell's environment using the given
/// string as its body.
///
/// # Arguments
///
/// * `name` - The name of the function
/// * `body_text` - The body of the function, expected to start with "()".
pub fn define_func_from_str(
&mut self,
name: impl Into<String>,
body_text: &str,
) -> Result<(), error::Error> {
let name = name.into();
let mut parser =
super::parsing::create_parser(body_text.as_bytes(), &self.parser_options());
let func_body = parser.parse_function_parens_and_body().map_err(|e| {
error::Error::from(error::ErrorKind::FunctionParseError(name.clone(), e))
})?;
let def = brush_parser::ast::FunctionDefinition {
fname: name.clone().into(),
body: func_body,
};
self.define_func(name, def, &crate::SourceInfo::default());
Ok(())
}
/// Invokes a function defined in this shell, returning the resulting exit status.
///
/// # Arguments
///
/// * `name` - The name of the function to invoke.
/// * `args` - The arguments to pass to the function.
/// * `params` - Execution parameters to use for the invocation.
pub async fn invoke_function<N: AsRef<str>, I: IntoIterator<Item = A>, A: AsRef<str>>(
&mut self,
name: N,
args: I,
params: &ExecutionParameters,
) -> Result<u8, error::Error> {
let name = name.as_ref();
let command_name = String::from(name);
let func_registration = self
.funcs
.get(name)
.ok_or_else(|| error::ErrorKind::FunctionNotFound(name.to_owned()))?
.to_owned();
let context = commands::ExecutionContext {
shell: self,
command_name,
params: params.clone(),
};
let command_args = args
.into_iter()
.map(|s| commands::CommandArg::String(String::from(s.as_ref())))
.collect::<Vec<_>>();
let result =
commands::invoke_shell_function(func_registration, context, &command_args).await?;
match result.wait().await? {
ExecutionWaitResult::Completed(result) => Ok(result.exit_code.into()),
ExecutionWaitResult::Stopped(..) => {
error::unimp("stopped child from function invocation")
}
}
}
}
+96
View File
@@ -0,0 +1,96 @@
//! History management for shells.
use std::path::PathBuf;
use crate::{error, openfiles};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
pub(super) fn load_history(&self) -> Result<Option<crate::history::History>, error::Error> {
const MAX_FILE_SIZE_FOR_HISTORY_IMPORT: u64 = 1024 * 1024 * 1024; // 1 GiB
let Some(history_path) = self.history_file_path() else {
return Ok(None);
};
let mut options = std::fs::File::options();
options.read(true);
let mut history_file =
self.open_file(&options, history_path, &self.default_exec_params())?;
// Check on the file's size.
if let openfiles::OpenFile::File(file) = &mut history_file {
let file_metadata = file.metadata()?;
let file_size = file_metadata.len();
// If the file is empty, no reason to try reading it. Note that this will also
// end up excluding non-regular files that report a 0 file size but appear
// to have contents when read.
if file_size == 0 {
return Ok(None);
}
// Bail if the file is unrealistically large. For now we just refuse to import it.
if file_size > MAX_FILE_SIZE_FOR_HISTORY_IMPORT {
return Err(error::ErrorKind::HistoryFileTooLargeToImport.into());
}
}
Ok(Some(crate::history::History::import(history_file)?))
}
/// Returns the path to the history file used by the shell, if one is set.
pub fn history_file_path(&self) -> Option<PathBuf> {
self.env_str("HISTFILE")
.map(|s| PathBuf::from(s.into_owned()))
}
/// Returns the path to the history file used by the shell, if one is set.
pub fn history_time_format(&self) -> Option<String> {
self.env_str("HISTTIMEFORMAT").map(|s| s.into_owned())
}
/// Saves history back to any backing storage.
pub fn save_history(&mut self) -> Result<(), error::Error> {
if let Some(history_file_path) = self.history_file_path()
&& let Some(history) = &mut self.history
{
// See if there's *any* time format configured. That triggers writing out
// timestamps.
let write_timestamps = self.env.is_set("HISTTIMEFORMAT");
// TODO(history): Observe options.append_to_history_file
history.flush(
history_file_path,
true, /* append? */
true, /* unsaved items only? */
write_timestamps,
)?;
}
Ok(())
}
/// Adds a command to history.
pub fn add_to_history(&mut self, command: &str) -> Result<(), error::Error> {
if let Some(history) = &mut self.history {
// Trim.
let command = command.trim();
// For now, discard empty commands.
if command.is_empty() {
return Ok(());
}
// Add it to history.
history.add(crate::history::Item {
id: 0,
command_line: command.to_owned(),
timestamp: Some(chrono::Utc::now()),
dirty: true,
})?;
}
Ok(())
}
}
+142
View File
@@ -0,0 +1,142 @@
//! Init script support for shells.
use std::path::PathBuf;
use crate::{Shell, error, extensions, interp};
/// Behavior for loading profile files.
#[derive(Default)]
pub enum ProfileLoadBehavior {
/// Load the default profile files.
#[default]
LoadDefault,
/// Skip loading profile files.
Skip,
}
impl ProfileLoadBehavior {
/// Returns whether profile loading should be skipped.
pub const fn skip(&self) -> bool {
matches!(self, Self::Skip)
}
}
/// Behavior for loading rc files.
#[derive(Default)]
pub enum RcLoadBehavior {
/// Load the default rc files.
#[default]
LoadDefault,
/// Load a custom rc file; do not load defaults.
LoadCustom(PathBuf),
/// Skip loading rc files.
Skip,
}
impl RcLoadBehavior {
/// Returns whether rc loading should be skipped.
pub const fn skip(&self) -> bool {
matches!(self, Self::Skip)
}
}
impl<SE: extensions::ShellExtensions> Shell<SE> {
/// Loads and executes standard shell configuration files (i.e., rc and profile).
///
/// # Arguments
///
/// * `profile_behavior` - Behavior for loading profile files.
/// * `rc_behavior` - Behavior for loading rc files.
pub async fn load_config(
&mut self,
profile_behavior: &ProfileLoadBehavior,
rc_behavior: &RcLoadBehavior,
) -> Result<(), error::Error> {
let mut params = self.default_exec_params();
params.process_group_policy = interp::ProcessGroupPolicy::SameProcessGroup;
if self.options.login_shell {
// --noprofile means skip this.
if matches!(profile_behavior, ProfileLoadBehavior::Skip) {
return Ok(());
}
//
// Source the system profile if it exists.
//
// Next source the first of these that exists and is readable (if any):
// * ~/.bash_profile
// * ~/.bash_login
// * ~/.profile
//
if let Some(system_profile) = crate::sys::fs::get_system_profile_path() {
self.source_if_exists(system_profile, &params).await?;
}
if let Some(home_path) = self.home_dir() {
if self.options.sh_mode {
self.source_if_exists(home_path.join(".profile").as_path(), &params)
.await?;
} else {
if !self
.source_if_exists(home_path.join(".bash_profile").as_path(), &params)
.await?
{
if !self
.source_if_exists(home_path.join(".bash_login").as_path(), &params)
.await?
{
self.source_if_exists(home_path.join(".profile").as_path(), &params)
.await?;
}
}
}
}
} else {
if self.options.interactive {
match rc_behavior {
_ if self.options.sh_mode => (),
RcLoadBehavior::Skip => (),
RcLoadBehavior::LoadCustom(rc_file) => {
// If an explicit rc file is provided, source it.
self.source_if_exists(rc_file, &params).await?;
}
RcLoadBehavior::LoadDefault => {
//
// Otherwise, for non-login interactive shells, load in this order:
//
// system rc file (e.g. /etc/bash.bashrc on Unix)
// ~/.bashrc
//
if let Some(system_rc) = crate::sys::fs::get_system_rc_path() {
self.source_if_exists(system_rc, &params).await?;
}
if let Some(home_path) = self.home_dir() {
self.source_if_exists(home_path.join(".bashrc").as_path(), &params)
.await?;
self.source_if_exists(home_path.join(".brushrc").as_path(), &params)
.await?;
}
}
}
} else {
let env_var_name = if self.options.sh_mode {
"ENV"
} else {
"BASH_ENV"
};
if self.env.is_set(env_var_name) {
//
// TODO(well-known-vars): look at $ENV/BASH_ENV; source its expansion if that
// file exists
//
return error::unimp(
"load config from $ENV/BASH_ENV for non-interactive, non-login shell",
);
}
}
}
Ok(())
}
}
+91
View File
@@ -0,0 +1,91 @@
//! I/O support for shell instances.
use std::io::Write;
use crate::{error, extensions, ioutils};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Returns a value that can be used to write to the shell's currently configured
/// standard output stream using `write!` et al.
pub fn stdout(&self) -> impl std::io::Write + 'static {
self.open_files.try_stdout().cloned().unwrap_or_else(|| {
ioutils::FailingReaderWriter::new("standard output not available").into()
})
}
/// Returns a value that can be used to write to the shell's currently configured
/// standard error stream using `write!` et al.
pub fn stderr(&self) -> impl std::io::Write + 'static {
self.open_files.try_stderr().cloned().unwrap_or_else(|| {
ioutils::FailingReaderWriter::new("standard error not available").into()
})
}
/// Outputs `set -x` style trace output for a command. Intentionally does not return
/// a result or error to avoid risk that a caller treats an error as fatal. Tracing
/// failure should generally always be ignored to avoid interfering with execution
/// flows.
///
/// # Arguments
///
/// * `command` - The command to trace.
pub(crate) async fn trace_command<S: AsRef<str>>(
&mut self,
params: &crate::interp::ExecutionParameters,
command: S,
) {
// Expand the PS4 prompt variable to get our prefix.
let mut prefix = self
.as_mut()
.expand_prompt_var("PS4", "")
.await
.unwrap_or_default();
// Add additional depth-based prefixes using the first character of PS4.
let additional_depth = self.call_stack.script_source_depth() + self.depth;
if let Some(c) = prefix.chars().next() {
for _ in 0..additional_depth {
prefix.insert(0, c);
}
}
// Resolve which file descriptor to use for tracing. We default to stderr,
// but if BASH_XTRACEFD is set and refers to a valid file descriptor, use that instead.
let trace_file = if let Some((_, xtracefd_var)) = self.env.get("BASH_XTRACEFD")
&& let Ok(fd) = xtracefd_var
.value()
.to_cow_str(self)
.parse::<super::ShellFd>()
&& let Some(file) = self.open_files.try_fd(fd)
{
Some(file.clone())
} else {
params.try_stderr(self)
};
// If we have a valid trace file, write to it.
if let Some(trace_file) = trace_file
&& let Ok(mut trace_file) = trace_file.try_clone()
{
let _ = writeln!(trace_file, "{prefix}{}", command.as_ref());
}
}
/// Displays the given error to the user, using the shell's error display mechanisms.
///
/// # Arguments
///
/// * `file_table` - The open file table to use for any file descriptor references.
/// * `err` - The error to display.
pub fn display_error(
&self,
file: &mut impl std::io::Write,
err: &error::Error,
) -> Result<(), error::Error> {
use crate::extensions::ErrorFormatter as _;
let str = self.error_formatter.format_error(err, self);
write!(file, "{str}")?;
Ok(())
}
}
+20
View File
@@ -0,0 +1,20 @@
//! Job management for shell instances.
use std::io::Write;
use crate::{error, extensions};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Checks for completed jobs in the shell, reporting any changes found.
pub fn check_for_completed_jobs(&mut self) -> Result<(), error::Error> {
let results = self.jobs.poll()?;
if self.options.enable_job_control {
for (job, _result) in results {
writeln!(self.stderr(), "{job}")?;
}
}
Ok(())
}
}
+64
View File
@@ -0,0 +1,64 @@
//! Parsing for shell instances.
use std::io::Read;
use crate::{Shell, extensions, trace_categories};
impl<SE: extensions::ShellExtensions> Shell<SE> {
/// Parses the given reader as a shell program, returning the resulting Abstract Syntax Tree
/// for the program.
pub fn parse<R: Read>(
&self,
reader: R,
) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
let mut parser = create_parser(reader, &self.parser_options());
tracing::debug!(target: trace_categories::PARSE, "Parsing reader as program...");
parser.parse_program()
}
/// Parses the given string as a shell program, returning the resulting Abstract Syntax Tree
/// for the program.
///
/// # Arguments
///
/// * `s` - The string to parse as a program.
pub fn parse_string<S: Into<String>>(
&self,
s: S,
) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
parse_string_impl(s.into(), self.parser_options())
}
/// Returns the options that should be used for parsing shell programs; reflects
/// the current configuration state of the shell and may change over time.
pub const fn parser_options(&self) -> brush_parser::ParserOptions {
brush_parser::ParserOptions {
enable_extended_globbing: self.options.extended_globbing,
posix_mode: self.options.posix_mode,
sh_mode: self.options.sh_mode,
tilde_expansion_at_word_start: true,
tilde_expansion_after_colon: false,
parser_impl: self.parser_impl,
}
}
}
#[cached::proc_macro::cached(size = 64, result = true)]
fn parse_string_impl(
s: String,
parser_options: brush_parser::ParserOptions,
) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
let mut parser = create_parser(s.as_bytes(), &parser_options);
tracing::debug!(target: trace_categories::PARSE, "Parsing string as program...");
parser.parse_program()
}
pub(super) fn create_parser<R: Read>(
r: R,
parser_options: &brush_parser::ParserOptions,
) -> brush_parser::Parser<std::io::BufReader<R>> {
let reader = std::io::BufReader::new(r);
brush_parser::Parser::new(reader, parser_options)
}
+77
View File
@@ -0,0 +1,77 @@
//! Prompt handling for shell instances.
use std::borrow::Cow;
use crate::{Shell, error, extensions, prompt};
impl<SE: extensions::ShellExtensions> Shell<SE> {
/// Returns the default prompt string for the shell.
const fn default_prompt(&self) -> &'static str {
if self.options.sh_mode {
"$ "
} else {
"brush$ "
}
}
/// Composes the shell's post-input, pre-command prompt, applying all appropriate expansions.
pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
self.expand_prompt_var("PS0", "").await
}
/// Composes the shell's prompt, applying all appropriate expansions.
pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
self.expand_prompt_var("PS1", self.default_prompt()).await
}
/// Composes the shell's alternate-side prompt, applying all appropriate expansions.
pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
// This is a brush extension.
self.expand_prompt_var("BRUSH_PS_ALT", "").await
}
/// Composes the shell's continuation prompt.
pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
self.expand_prompt_var("PS2", "> ").await
}
pub(super) async fn expand_prompt_var(
&mut self,
var_name: &str,
default: &str,
) -> Result<String, error::Error> {
//
// TODO(prompt): bash appears to do this in a subshell; we need to investigate
// if that's required.
//
// Retrieve the spec.
let prompt_spec = self.parameter_or_default(var_name, default);
if prompt_spec.is_empty() {
return Ok(String::new());
}
// Save (and later restore) the last exit status.
let prev_last_result = self.last_exit_status();
let prev_last_pipeline_statuses = self.last_pipeline_statuses.clone();
// Expand it.
let params = self.default_exec_params();
let result = prompt::expand_prompt(self, &params, prompt_spec.into_owned()).await;
// Restore the last exit status.
self.last_pipeline_statuses = prev_last_pipeline_statuses;
self.set_last_exit_status(prev_last_result);
// Strip out special characters that readline would typically drop:
// \001 and \002 (start and end of non-printing sequences).
let mut expanded = result?;
expanded.retain(|c| c != '\x01' && c != '\x02');
Ok(expanded)
}
fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
self.env_str(name).unwrap_or_else(|| default.into())
}
}
+42
View File
@@ -0,0 +1,42 @@
//! Readline edit buffer support for shell instances.
use crate::{error, extensions, variables::ShellVariable};
impl<SE: extensions::ShellExtensions> crate::Shell<SE> {
/// Updates the shell state to reflect the given edit buffer contents.
///
/// # Arguments
///
/// * `contents` - The contents of the edit buffer.
/// * `cursor` - The cursor position in the edit buffer.
pub fn set_edit_buffer(&mut self, contents: String, cursor: usize) -> Result<(), error::Error> {
self.env
.set_global("READLINE_LINE", ShellVariable::new(contents))?;
self.env
.set_global("READLINE_POINT", ShellVariable::new(cursor.to_string()))?;
Ok(())
}
/// Returns the contents of the shell's edit buffer, if any. The buffer
/// state is cleared from the shell.
pub fn pop_edit_buffer(&mut self) -> Result<Option<(String, usize)>, error::Error> {
let line = self
.env
.unset("READLINE_LINE")?
.map(|line| line.value().to_cow_str(self).to_string());
let point = self
.env
.unset("READLINE_POINT")?
.and_then(|point| point.value().to_cow_str(self).parse::<usize>().ok())
.unwrap_or(0);
if let Some(line) = line {
Ok(Some((line, point)))
} else {
Ok(None)
}
}
}
+124
View File
@@ -0,0 +1,124 @@
//! Defines state traits for the shell.
use std::{
borrow::Cow,
collections::HashMap,
path::{Path, PathBuf},
};
use crate::{
completion, env::ShellEnvironment, jobs, openfiles, options::RuntimeOptions, pathcache,
shell::KeyBindingsHelper,
};
/// A dyn-safe trait for constrained access to shell state.
pub trait ShellState {
/// Returns whether or not this shell is a subshell.
fn is_subshell(&self) -> bool;
/// Returns the last "SECONDS" captured time.
fn last_stopwatch_time(&self) -> std::time::SystemTime;
/// Returns the last "SECONDS" offset requested.
fn last_stopwatch_offset(&self) -> u32;
/// Returns the shell environment containing variables.
fn env(&self) -> &ShellEnvironment;
/// Returns a mutable reference to the shell environment.
fn env_mut(&mut self) -> &mut ShellEnvironment;
/// Returns the shell's runtime options.
fn options(&self) -> &RuntimeOptions;
/// Returns a mutable reference to the shell's runtime options.
fn options_mut(&mut self) -> &mut RuntimeOptions;
/// Returns the shell's aliases.
fn aliases(&self) -> &HashMap<String, String>;
/// Returns a mutable reference to the shell's aliases.
fn aliases_mut(&mut self) -> &mut HashMap<String, String>;
/// Returns the shell's job manager.
fn jobs(&self) -> &jobs::JobManager;
/// Returns a mutable reference to the shell's job manager.
fn jobs_mut(&mut self) -> &mut jobs::JobManager;
/// Returns the shell's trap handler configuration.
fn traps(&self) -> &crate::traps::TrapHandlerConfig;
/// Returns a mutable reference to the shell's trap handler configuration.
fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig;
/// Returns the shell's directory stack.
fn directory_stack(&self) -> &[PathBuf];
/// Returns a mutable reference to the shell's directory stack.
fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf>;
/// Returns the statuses of commands in the last pipeline.
fn last_pipeline_statuses(&self) -> &[u8];
/// Returns a mutable reference to the statuses of commands in the last pipeline.
fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8>;
/// Returns the shell's program location cache.
fn program_location_cache(&self) -> &pathcache::PathCache;
/// Returns a mutable reference to the shell's program location cache.
fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache;
/// Returns the shell's completion configuration.
fn completion_config(&self) -> &completion::Config;
/// Returns a mutable reference to the shell's completion configuration.
fn completion_config_mut(&mut self) -> &mut completion::Config;
/// Returns the shell's open files.
fn open_files(&self) -> &openfiles::OpenFiles;
/// Returns a mutable reference to the shell's open files.
fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles;
/// Returns the *current* name of the shell ($0).
fn current_shell_name(&self) -> Option<Cow<'_, str>>;
/// Returns the current subshell depth; 0 is returned if this shell is not a subshell.
fn depth(&self) -> usize;
/// Returns the call stack for the shell.
fn call_stack(&self) -> &crate::callstack::CallStack;
/// Returns the shell's history, if it exists.
fn history(&self) -> Option<&crate::history::History>;
/// Returns a mutable reference to the shell's history, if it exists.
fn history_mut(&mut self) -> Option<&mut crate::history::History>;
/// Returns the shell's official version string (if available).
fn version(&self) -> Option<&str>;
/// Returns the exit status of the last command executed in this shell.
fn last_exit_status(&self) -> u8;
/// Updates the last exit status.
fn set_last_exit_status(&mut self, status: u8);
/// Returns the key bindings helper for the shell.
fn key_bindings(&self) -> Option<&KeyBindingsHelper>;
/// Sets the key bindings helper for the shell.
fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>);
/// Returns the shell's current working directory.
fn working_dir(&self) -> &Path;
/// Returns a mutable reference to the shell's current working directory.
/// This is only accessible within the crate.
fn working_dir_mut(&mut self) -> &mut PathBuf;
/// Returns the product display name for this shell.
fn product_display_str(&self) -> Option<&str>;
}
+105
View File
@@ -0,0 +1,105 @@
//! Trap handling for the shell.
use crate::{ExecutionParameters, ExecutionResult, ProcessGroupPolicy, error, traps::TrapSignal};
impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
/// Runs any exit steps for the shell.
///
/// This currently includes invoking the `EXIT` trap handler, if any.
pub async fn on_exit(&mut self) -> Result<(), error::Error> {
if self.traps.handles(TrapSignal::Exit) {
self.invoke_trap_handler(TrapSignal::Exit, &self.default_exec_params())
.await?;
}
Ok(())
}
/// Invokes the handler registered for `signal`, if any.
///
/// Behavior varies by signal type:
///
/// * **Per-signal recursion guard** — each trap guards against its own self-recursion, but
/// different traps *can* fire from within each other's handlers (matching bash semantics).
///
/// * **Inheritance** — in functions and subshells, some traps are only inherited when the
/// corresponding shell option is enabled (e.g. `errtrace` / `set -E` for `ERR`, `functrace` /
/// `set -T` for `DEBUG`/`RETURN`).
///
/// * **`$?` preservation** — `last_exit_status` is saved before and restored after the handler
/// runs so the trap does not clobber the status that triggered it.
///
/// # Arguments
///
/// * `signal`: Signal to run handler for.
///
/// * `params`: Execution parameters to use for handler.
pub(crate) async fn invoke_trap_handler(
&mut self,
signal: TrapSignal,
params: &ExecutionParameters,
) -> Result<ExecutionResult, error::Error> {
// Per-signal self-recursion guard: don't re-enter a trap that is
// already being handled. Different traps *can* fire from each
// other's handlers (e.g. ERR inside EXIT, EXIT inside ERR).
if self.call_stack().is_trap_signal_active(signal) {
return Ok(ExecutionResult::success());
}
// Don't fire traps that have been explicitly suppressed (e.g. DEBUG
// during programmable completion).
if self.call_stack().is_trap_delivery_suppressed() {
return Ok(ExecutionResult::success());
}
// In functions and subshells, some traps are only inherited when the
// corresponding option is enabled.
if (self.in_function() || self.is_subshell())
&& !self.is_trap_inherited_in_current_scope(signal)
{
return Ok(ExecutionResult::success());
}
let Some(handler) = self.traps.get_handler(signal).cloned() else {
return Ok(ExecutionResult::success());
};
let mut params = params.clone();
params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
// Preserve $? across trap handler execution so the handler doesn't
// clobber the status that triggered it.
let orig_last_exit_status = self.last_exit_status;
// N.B. We use manual enter/leave rather than an RAII guard because a guard
// would need to hold `&mut Shell`, preventing the mutable borrow required by
// `run_string()`. This is safe because `result` is captured into a variable
// (never early-returned with `?`), so `leave_trap_handler()` always runs.
self.enter_trap_handler(signal, Some(&handler));
let result = self
.run_string(&handler.command, &handler.source_info, &params)
.await;
self.leave_trap_handler();
self.last_exit_status = orig_last_exit_status;
result
}
/// Returns whether the given trap signal is inherited in the current
/// function or subshell scope.
fn is_trap_inherited_in_current_scope(&self, signal: TrapSignal) -> bool {
match signal {
TrapSignal::Err => self.options().shell_functions_inherit_err_trap,
TrapSignal::Debug | TrapSignal::Return => {
self.options()
.shell_functions_inherit_debug_and_return_traps
}
// EXIT and system signals are always inherited — i.e. their visibility is
// not gated by errtrace/functrace options. (The actual trap *state* for
// subshells is managed separately via `Shell::clone`.)
TrapSignal::Exit | TrapSignal::Signal(_) => true,
}
}
}