Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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, ¶ms).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,
|
||||
¶ms,
|
||||
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)?)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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, ¶ms).await?;
|
||||
}
|
||||
if let Some(home_path) = self.home_dir() {
|
||||
if self.options.sh_mode {
|
||||
self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
|
||||
.await?;
|
||||
} else {
|
||||
if !self
|
||||
.source_if_exists(home_path.join(".bash_profile").as_path(), ¶ms)
|
||||
.await?
|
||||
{
|
||||
if !self
|
||||
.source_if_exists(home_path.join(".bash_login").as_path(), ¶ms)
|
||||
.await?
|
||||
{
|
||||
self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
|
||||
.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, ¶ms).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, ¶ms).await?;
|
||||
}
|
||||
if let Some(home_path) = self.home_dir() {
|
||||
self.source_if_exists(home_path.join(".bashrc").as_path(), ¶ms)
|
||||
.await?;
|
||||
self.source_if_exists(home_path.join(".brushrc").as_path(), ¶ms)
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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, ¶ms, 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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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, ¶ms)
|
||||
.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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user