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
+423
View File
@@ -0,0 +1,423 @@
//! Arithmetic evaluation
use std::borrow::Cow;
use crate::{ExecutionParameters, Shell, env, expansion, extensions, variables};
use brush_parser::ast;
/// Maximum recursion depth for arithmetic variable dereference chains
/// (e.g., a=b, b=c, c=a would cycle through variable dereferences).
const MAX_VARIABLE_DEREF_DEPTH: u32 = 1024;
/// Represents an error that occurs during evaluation of an arithmetic expression.
#[derive(Debug, thiserror::Error)]
pub enum EvalError {
/// Division by zero.
#[error("division by zero")]
DivideByZero,
/// Negative exponent.
#[error("exponent less than 0")]
NegativeExponent,
/// Failed to tokenize an arithmetic expression.
#[error("failed to tokenize expression")]
FailedToTokenizeExpression,
/// Failed to expand an arithmetic expression.
#[error("failed to expand expression: {0}")]
FailedToExpandExpression(String),
/// Failed to access an element of an array.
#[error("failed to access array")]
FailedToAccessArray,
/// Failed to update the shell environment in an assignment operator.
#[error("failed to update environment")]
FailedToUpdateEnvironment,
/// Failed to parse an arithmetic expression.
#[error("failed to parse expression: {0}")]
ParseError(String),
/// Error expanding an unset variable.
#[error("expanding unset variable: {0}")]
ExpandingUnsetVariable(String),
/// Expression recursion level exceeded.
#[error("expression recursion level exceeded")]
RecursionLimitExceeded,
}
/// Trait implemented by arithmetic expressions that can be evaluated.
pub(crate) trait ExpandAndEvaluate {
/// Evaluate the given expression, returning the resulting numeric value.
///
/// # Arguments
///
/// * `shell` - The shell to use for evaluation.
/// * `trace_if_needed` - Whether to trace the evaluation.
async fn eval(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
trace_if_needed: bool,
) -> Result<i64, EvalError>;
}
impl ExpandAndEvaluate for ast::UnexpandedArithmeticExpr {
async fn eval(
&self,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
trace_if_needed: bool,
) -> Result<i64, EvalError> {
expand_and_eval(shell, params, self.value.as_str(), trace_if_needed).await
}
}
/// Evaluate the given arithmetic expression, returning the resulting numeric value.
///
/// # Arguments
///
/// * `shell` - The shell to use for evaluation.
/// * `expr` - The unexpanded arithmetic expression to evaluate.
/// * `trace_if_needed` - Whether to trace the evaluation.
pub(crate) async fn expand_and_eval(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
expr: &str,
trace_if_needed: bool,
) -> Result<i64, EvalError> {
// Per documentation, first shell-expand it.
let options = expansion::ExpanderOptions {
tilde_expand: false,
..Default::default()
};
let expanded_self = expansion::basic_expand_word_with_options(shell, params, expr, &options)
.await
.map_err(|_e| EvalError::FailedToExpandExpression(expr.to_owned()))?;
// Now parse.
let expr = brush_parser::arithmetic::parse(&expanded_self)
.map_err(|_e| EvalError::ParseError(expanded_self))?;
// Trace if applicable.
if trace_if_needed && shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("(( {expr} ))"))
.await;
}
// Now evaluate.
expr.eval(shell)
}
/// Trait implemented by evaluatable arithmetic expressions.
pub trait Evaluatable {
/// Evaluate the given arithmetic expression, returning the resulting numeric value.
///
/// # Arguments
///
/// * `shell` - The shell to use for evaluation.
fn eval(&self, shell: &mut Shell<impl extensions::ShellExtensions>) -> Result<i64, EvalError>;
}
impl Evaluatable for ast::ArithmeticExpr {
fn eval(&self, shell: &mut Shell<impl extensions::ShellExtensions>) -> Result<i64, EvalError> {
eval_expr_impl(self, shell, 0)
}
}
fn eval_expr_impl(
expr: &ast::ArithmeticExpr,
shell: &mut Shell<impl extensions::ShellExtensions>,
depth: u32,
) -> Result<i64, EvalError> {
let value = match expr {
ast::ArithmeticExpr::Literal(l) => *l,
ast::ArithmeticExpr::Reference(lvalue) => deref_lvalue(shell, lvalue, depth)?,
ast::ArithmeticExpr::UnaryOp(op, operand) => apply_unary_op(shell, *op, operand, depth)?,
ast::ArithmeticExpr::BinaryOp(op, left, right) => {
apply_binary_op(shell, *op, left, right, depth)?
}
ast::ArithmeticExpr::Conditional(condition, then_expr, else_expr) => {
let conditional_eval = eval_expr_impl(condition, shell, depth)?;
// Ensure we only evaluate the branch indicated by the condition.
if conditional_eval != 0 {
eval_expr_impl(then_expr, shell, depth)?
} else {
eval_expr_impl(else_expr, shell, depth)?
}
}
ast::ArithmeticExpr::Assignment(lvalue, rhs) => {
let expr_eval = eval_expr_impl(rhs, shell, depth)?;
assign(shell, lvalue, expr_eval, depth)?
}
ast::ArithmeticExpr::UnaryAssignment(op, lvalue) => {
apply_unary_assignment_op(shell, lvalue, *op, depth)?
}
ast::ArithmeticExpr::BinaryAssignment(op, lvalue, operand) => {
let value = apply_binary_op(
shell,
*op,
&ast::ArithmeticExpr::Reference(lvalue.clone()),
operand,
depth,
)?;
assign(shell, lvalue, value, depth)?
}
};
Ok(value)
}
fn get_var_value<'a>(
shell: &'a Shell<impl extensions::ShellExtensions>,
name: &str,
) -> Result<Cow<'a, str>, EvalError> {
let value = shell.env_var(name).map(|var| var.resolve_value(shell));
if let Some(value) = value
&& value.is_set()
{
return Ok(value.to_cow_str(shell).to_string().into());
}
if shell.options().treat_unset_variables_as_error {
return Err(EvalError::ExpandingUnsetVariable(name.into()));
}
Ok("".into())
}
fn deref_lvalue(
shell: &mut Shell<impl extensions::ShellExtensions>,
lvalue: &ast::ArithmeticTarget,
depth: u32,
) -> Result<i64, EvalError> {
let value_str: Cow<'_, str> = match lvalue {
ast::ArithmeticTarget::Variable(name) => get_var_value(shell, name.as_str())?,
ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
let index_str = eval_expr_impl(index_expr, shell, depth)?.to_string();
shell
.env()
.get(name)
.map_or_else(
|| Ok(None),
|(_, v)| v.value().get_at(index_str.as_str(), shell),
)
.map_err(|_err| EvalError::FailedToAccessArray)?
.unwrap_or(Cow::Borrowed(""))
}
};
let parsed_value = brush_parser::arithmetic::parse(value_str.as_ref())
.map_err(|_err| EvalError::ParseError(value_str.to_string()))?;
// Literals don't need depth tracking — they can't cause recursion.
// Only increment depth when the parsed value requires further evaluation
// (i.e., it references other variables), matching bash's behavior.
if matches!(parsed_value, ast::ArithmeticExpr::Literal(_)) {
return eval_expr_impl(&parsed_value, shell, depth);
}
let new_depth = depth + 1;
if new_depth > MAX_VARIABLE_DEREF_DEPTH {
return Err(EvalError::RecursionLimitExceeded);
}
eval_expr_impl(&parsed_value, shell, new_depth)
}
fn apply_unary_op(
shell: &mut Shell<impl extensions::ShellExtensions>,
op: ast::UnaryOperator,
operand: &ast::ArithmeticExpr,
depth: u32,
) -> Result<i64, EvalError> {
let operand_eval = eval_expr_impl(operand, shell, depth)?;
match op {
ast::UnaryOperator::UnaryPlus => Ok(operand_eval),
ast::UnaryOperator::UnaryMinus => Ok(operand_eval.wrapping_neg()),
ast::UnaryOperator::BitwiseNot => Ok(!operand_eval),
ast::UnaryOperator::LogicalNot => Ok(bool_to_i64(operand_eval == 0)),
}
}
fn apply_binary_op(
shell: &mut Shell<impl extensions::ShellExtensions>,
op: ast::BinaryOperator,
left: &ast::ArithmeticExpr,
right: &ast::ArithmeticExpr,
depth: u32,
) -> Result<i64, EvalError> {
// First, special-case short-circuiting operators. For those, we need
// to ensure we don't eagerly evaluate both operands. After we
// get these out of the way, we can easily just evaluate operands
// for the other operators.
match op {
ast::BinaryOperator::LogicalAnd => {
let left = eval_expr_impl(left, shell, depth)?;
if left == 0 {
return Ok(bool_to_i64(false));
}
let right = eval_expr_impl(right, shell, depth)?;
return Ok(bool_to_i64(right != 0));
}
ast::BinaryOperator::LogicalOr => {
let left = eval_expr_impl(left, shell, depth)?;
if left != 0 {
return Ok(bool_to_i64(true));
}
let right = eval_expr_impl(right, shell, depth)?;
return Ok(bool_to_i64(right != 0));
}
_ => (),
}
// The remaining operators unconditionally operate both operands.
let left = eval_expr_impl(left, shell, depth)?;
let right = eval_expr_impl(right, shell, depth)?;
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_sign_loss)]
match op {
ast::BinaryOperator::Power => {
if right >= 0 {
Ok(wrapping_pow_u64(left, right as u64))
} else {
Err(EvalError::NegativeExponent)
}
}
ast::BinaryOperator::Multiply => Ok(left.wrapping_mul(right)),
ast::BinaryOperator::Divide => {
if right == 0 {
Err(EvalError::DivideByZero)
} else {
Ok(left.wrapping_div(right))
}
}
ast::BinaryOperator::Modulo => {
if right == 0 {
Err(EvalError::DivideByZero)
} else {
Ok(left.wrapping_rem(right))
}
}
ast::BinaryOperator::Comma => Ok(right),
ast::BinaryOperator::Add => Ok(left.wrapping_add(right)),
ast::BinaryOperator::Subtract => Ok(left.wrapping_sub(right)),
ast::BinaryOperator::ShiftLeft => Ok(left.wrapping_shl(right as u32)),
ast::BinaryOperator::ShiftRight => Ok(left.wrapping_shr(right as u32)),
ast::BinaryOperator::LessThan => Ok(bool_to_i64(left < right)),
ast::BinaryOperator::LessThanOrEqualTo => Ok(bool_to_i64(left <= right)),
ast::BinaryOperator::GreaterThan => Ok(bool_to_i64(left > right)),
ast::BinaryOperator::GreaterThanOrEqualTo => Ok(bool_to_i64(left >= right)),
ast::BinaryOperator::Equals => Ok(bool_to_i64(left == right)),
ast::BinaryOperator::NotEquals => Ok(bool_to_i64(left != right)),
ast::BinaryOperator::BitwiseAnd => Ok(left & right),
ast::BinaryOperator::BitwiseXor => Ok(left ^ right),
ast::BinaryOperator::BitwiseOr => Ok(left | right),
ast::BinaryOperator::LogicalAnd => unreachable!("LogicalAnd covered above"),
ast::BinaryOperator::LogicalOr => unreachable!("LogicalOr covered above"),
}
}
fn apply_unary_assignment_op(
shell: &mut Shell<impl extensions::ShellExtensions>,
lvalue: &ast::ArithmeticTarget,
op: ast::UnaryAssignmentOperator,
depth: u32,
) -> Result<i64, EvalError> {
let value = deref_lvalue(shell, lvalue, depth)?;
match op {
ast::UnaryAssignmentOperator::PrefixIncrement => {
let new_value = value.wrapping_add(1);
assign(shell, lvalue, new_value, depth)?;
Ok(new_value)
}
ast::UnaryAssignmentOperator::PrefixDecrement => {
let new_value = value.wrapping_sub(1);
assign(shell, lvalue, new_value, depth)?;
Ok(new_value)
}
ast::UnaryAssignmentOperator::PostfixIncrement => {
let new_value = value.wrapping_add(1);
assign(shell, lvalue, new_value, depth)?;
Ok(value)
}
ast::UnaryAssignmentOperator::PostfixDecrement => {
let new_value = value.wrapping_sub(1);
assign(shell, lvalue, new_value, depth)?;
Ok(value)
}
}
}
fn assign(
shell: &mut Shell<impl extensions::ShellExtensions>,
lvalue: &ast::ArithmeticTarget,
value: i64,
depth: u32,
) -> Result<i64, EvalError> {
match lvalue {
ast::ArithmeticTarget::Variable(name) => {
shell
.env_mut()
.update_or_add(
name.as_str(),
variables::ShellValueLiteral::Scalar(value.to_string()),
|_| Ok(()),
env::EnvironmentLookup::Anywhere,
env::EnvironmentScope::Global,
)
.map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
}
ast::ArithmeticTarget::ArrayElement(name, index_expr) => {
let index_str = eval_expr_impl(index_expr, shell, depth)?.to_string();
shell
.env_mut()
.update_or_add_array_element(
name.as_str(),
index_str,
value.to_string(),
|_| Ok(()),
env::EnvironmentLookup::Anywhere,
env::EnvironmentScope::Global,
)
.map_err(|_err| EvalError::FailedToUpdateEnvironment)?;
}
}
Ok(value)
}
const fn bool_to_i64(value: bool) -> i64 {
if value { 1 } else { 0 }
}
// N.B. We implement our own version of wrapping_pow that takes a 64-bit exponent.
// This seems to be the best way to guarantee that we handle overflow cases
// with exponents correctly.
const fn wrapping_pow_u64(mut base: i64, mut exponent: u64) -> i64 {
let mut result: i64 = 1;
while exponent > 0 {
if exponent % 2 == 1 {
result = result.wrapping_mul(base);
}
base = base.wrapping_mul(base);
exponent /= 2;
}
result
}
+89
View File
@@ -0,0 +1,89 @@
use brush_parser::word;
use itertools::Itertools;
pub(crate) fn generate_and_combine_brace_expansions(
pieces: Vec<brush_parser::word::BraceExpressionOrText>,
) -> impl IntoIterator<Item = String> {
let expansions: Vec<Vec<String>> = pieces
.into_iter()
.map(|piece| expand_brace_expr_or_text(piece).collect())
.collect();
expansions
.into_iter()
.multi_cartesian_product()
.map(|v| v.join(""))
}
fn expand_brace_expr_or_text(
beot: word::BraceExpressionOrText,
) -> Box<dyn Iterator<Item = String>> {
match beot {
word::BraceExpressionOrText::Expr(members) => {
// Chain all member iterators together
Box::new(members.into_iter().flat_map(expand_brace_expr_member))
}
word::BraceExpressionOrText::Text(text) => Box::new(std::iter::once(text)),
}
}
#[expect(clippy::cast_possible_truncation)]
fn expand_brace_expr_member(bem: word::BraceExpressionMember) -> Box<dyn Iterator<Item = String>> {
match bem {
word::BraceExpressionMember::NumberSequence {
start,
end,
increment,
} => {
let mut increment = increment.unsigned_abs() as usize;
if increment == 0 {
increment = 1;
}
if start <= end {
Box::new((start..=end).step_by(increment).map(|n| n.to_string()))
} else {
// Iterate from start down to end by decrementing.
#[allow(clippy::cast_possible_wrap)]
let increment = increment as i64;
Box::new(
std::iter::successors(Some(start), move |&n| {
let next = n - increment;
(next >= end).then_some(next)
})
.map(|n| n.to_string()),
)
}
}
word::BraceExpressionMember::CharSequence {
start,
end,
increment,
} => {
let mut increment = increment.unsigned_abs() as usize;
if increment == 0 {
increment = 1;
}
if start <= end {
Box::new((start..=end).step_by(increment).map(|c| c.to_string()))
} else {
// Iterate from start down to end by decrementing.
let increment = increment as u32;
Box::new(
std::iter::successors(Some(start), move |&c| {
let next = char::from_u32(c as u32 - increment)?;
(next >= end).then_some(next)
})
.map(|c| c.to_string()),
)
}
}
word::BraceExpressionMember::Child(elements) => {
// Chain all element iterators together
Box::new(generate_and_combine_brace_expansions(elements).into_iter())
}
}
}
+570
View File
@@ -0,0 +1,570 @@
//! Facilities for implementing and managing builtins
use clap::builder::styling;
pub use futures::future::BoxFuture;
use std::io::Write;
use crate::{BuiltinError, CommandArg, commands, error, extensions, results};
/// Type of a function implementing a built-in command.
///
/// # Arguments
///
/// * The context in which the command is being executed.
/// * The arguments to the command.
#[allow(type_alias_bounds)]
pub type CommandExecuteFunc<SE: extensions::ShellExtensions> =
fn(
commands::ExecutionContext<'_, SE>,
Vec<commands::CommandArg>,
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>>;
/// Type of a function to retrieve help content for a built-in command.
///
/// # Arguments
///
/// * `name` - The name of the command.
/// * `content_type` - The type of content to retrieve.
/// * `options` - Additional options for content retrieval.
pub type CommandContentFunc =
fn(&str, ContentType, &ContentOptions) -> Result<String, error::Error>;
/// Trait implemented by built-in shell commands.
pub trait Command: clap::Parser {
/// The error type returned by the command.
type Error: BuiltinError + 'static;
/// Instantiates the built-in command with the given arguments.
///
/// # Arguments
///
/// * `args` - The arguments to the command.
fn new<I>(args: I) -> Result<Self, clap::Error>
where
I: IntoIterator<Item = String>,
{
if !Self::takes_plus_options() {
Self::try_parse_from(args)
} else {
// N.B. clap doesn't support named options like '+x'. To work around this, we
// establish a pattern of renaming them.
let mut updated_args = vec![];
for arg in args {
if let Some(plus_options) = arg.strip_prefix("+") {
for c in plus_options.chars() {
updated_args.push(format!("--+{c}"));
}
} else {
updated_args.push(arg);
}
}
Self::try_parse_from(updated_args)
}
}
/// Returns whether or not the command takes options with a leading '+' or '-' character.
fn takes_plus_options() -> bool {
false
}
/// Executes the built-in command in the provided context.
///
/// # Arguments
///
/// * `context` - The context in which the command is being executed.
// NOTE: we use desugared async here because we need a Send marker
fn execute<SE: extensions::ShellExtensions>(
&self,
context: commands::ExecutionContext<'_, SE>,
) -> impl std::future::Future<Output = Result<results::ExecutionResult, Self::Error>>
+ std::marker::Send;
/// Returns the textual help content associated with the command.
///
/// # Arguments
///
/// * `name` - The name of the command.
/// * `content_type` - The type of content to retrieve.
/// * `options` - Additional options for content retrieval.
fn get_content(
name: &str,
content_type: ContentType,
options: &ContentOptions,
) -> Result<String, error::Error> {
let mut clap_command = Self::command()
.styles(brush_help_styles())
.next_line_help(false);
clap_command.set_bin_name(name);
let s = match content_type {
ContentType::DetailedHelp => {
let rendered = clap_command.render_help();
if options.colorized {
rendered.ansi().to_string()
} else {
rendered.to_string()
}
}
ContentType::ShortUsage => get_builtin_short_usage(name, &clap_command),
ContentType::ShortDescription => get_builtin_short_description(name, &clap_command),
ContentType::ManPage => get_builtin_man_page(name, &clap_command)?,
};
Ok(s)
}
}
/// Trait implemented by built-in shell commands that take specially handled declarations
/// as arguments.
pub trait DeclarationCommand: Command {
/// Stores the declarations within the command instance.
///
/// # Arguments
///
/// * `declarations` - The declarations to store.
fn set_declarations(&mut self, declarations: Vec<commands::CommandArg>);
}
/// Type of help content, typically associated with a built-in command.
pub enum ContentType {
/// Detailed help content for the command.
DetailedHelp,
/// Short usage information for the command.
ShortUsage,
/// Short description for the command.
ShortDescription,
/// man-style help page.
ManPage,
}
/// Options for retrieving built-in command content.
#[derive(Default)]
pub struct ContentOptions {
/// Whether or not the content should be colorized.
pub colorized: bool,
}
/// Encapsulates a registration for a built-in command.
#[derive(Clone)]
pub struct Registration<SE: extensions::ShellExtensions> {
/// Function to execute the builtin.
pub execute_func: CommandExecuteFunc<SE>,
/// Function to retrieve the builtin's content/help text.
pub content_func: CommandContentFunc,
/// Has this registration been disabled?
pub disabled: bool,
/// Is the builtin classified as "special" by specification?
pub special_builtin: bool,
/// Is this builtin one that takes specially handled declarations?
pub declaration_builtin: bool,
}
impl<SE: extensions::ShellExtensions> Registration<SE> {
/// Updates the given registration to mark it for a special builtin.
#[must_use]
pub const fn special(self) -> Self {
Self {
special_builtin: true,
..self
}
}
}
fn get_builtin_man_page(_name: &str, _command: &clap::Command) -> Result<String, error::Error> {
error::unimp("man page rendering is not yet implemented")
}
fn get_builtin_short_description(name: &str, command: &clap::Command) -> String {
let about = command
.get_about()
.map_or_else(String::new, |s| s.to_string());
std::format!("{name} - {about}\n")
}
fn get_builtin_short_usage(name: &str, command: &clap::Command) -> String {
let mut usage = String::new();
let mut needs_space = false;
let mut optional_short_opts = vec![];
let mut required_short_opts = vec![];
for opt in command.get_opts() {
if opt.is_hide_set() {
continue;
}
if let Some(c) = opt.get_short() {
if !opt.is_required_set() {
optional_short_opts.push(c);
} else {
required_short_opts.push(c);
}
}
}
if !optional_short_opts.is_empty() {
if needs_space {
usage.push(' ');
}
usage.push('[');
usage.push('-');
for c in optional_short_opts {
usage.push(c);
}
usage.push(']');
needs_space = true;
}
if !required_short_opts.is_empty() {
if needs_space {
usage.push(' ');
}
usage.push('-');
for c in required_short_opts {
usage.push(c);
}
needs_space = true;
}
for pos in command.get_positionals() {
if pos.is_hide_set() {
continue;
}
if !pos.is_required_set() {
if needs_space {
usage.push(' ');
}
usage.push('[');
needs_space = false;
}
if let Some(names) = pos.get_value_names() {
for name in names {
if needs_space {
usage.push(' ');
}
usage.push_str(name);
needs_space = true;
}
}
if !pos.is_required_set() {
usage.push(']');
needs_space = true;
}
}
std::format!("{name}: {name} {usage}\n")
}
fn brush_help_styles() -> clap::builder::Styles {
styling::Styles::styled()
.header(
styling::AnsiColor::Yellow.on_default()
| styling::Effects::BOLD
| styling::Effects::UNDERLINE,
)
.usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
.literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
.placeholder(styling::AnsiColor::Cyan.on_default())
}
/// This function and the [`try_parse_known`] exists to deal with
/// the Clap's limitation of treating `--` like a regular value
/// `https://github.com/clap-rs/clap/issues/5055`
///
/// # Arguments
///
/// * `args` - An Iterator from [`std::env::args`]
///
/// # Returns
///
/// * a parsed struct T from [`clap::Parser::parse_from`]
/// * the remain iterator `args` with `--` and the rest arguments if they present otherwise None
///
/// # Examples
/// ```
/// use clap::{builder::styling, Parser};
/// #[derive(Parser)]
/// struct CommandLineArgs {
/// #[clap(allow_hyphen_values = true, num_args=1..)]
/// script_args: Vec<String>,
/// }
///
/// let (mut parsed_args, raw_args) =
/// brush_core::builtins::parse_known::<CommandLineArgs, _>(std::env::args());
/// if raw_args.is_some() {
/// parsed_args.script_args = raw_args.unwrap().collect();
/// }
/// ```
pub fn parse_known<T: clap::Parser, S>(
args: impl IntoIterator<Item = S>,
) -> (T, Option<impl Iterator<Item = S>>)
where
S: Into<std::ffi::OsString> + Clone + PartialEq<&'static str>,
{
let mut args = args.into_iter();
// the best way to save `--` is to get it out with a side effect while `clap` iterates over the
// args this way we can be 100% sure that we have '--' and the remaining args
// and we will iterate only once
let mut hyphen = None;
let args_before_hyphen = args.by_ref().take_while(|a| {
let is_hyphen = *a == "--";
if is_hyphen {
hyphen = Some(a.clone());
}
!is_hyphen
});
let parsed_args = T::parse_from(args_before_hyphen);
let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
(parsed_args, raw_args)
}
/// Similar to [`parse_known`] but with [`clap::Parser::try_parse_from`]
/// This function is used to parse arguments in builtins such as
/// `crate::echo::EchoCommand`
pub fn try_parse_known<T: clap::Parser>(
args: impl IntoIterator<Item = String>,
) -> Result<(T, Option<impl Iterator<Item = String>>), clap::Error> {
let mut args = args.into_iter();
let mut hyphen = None;
let args_before_hyphen = args.by_ref().take_while(|a| {
let is_hyphen = a == "--";
if is_hyphen {
hyphen = Some(a.clone());
}
!is_hyphen
});
let parsed_args = T::try_parse_from(args_before_hyphen)?;
let raw_args = hyphen.map(|hyphen| std::iter::once(hyphen).chain(args));
Ok((parsed_args, raw_args))
}
/// A simple command that can be registered as a built-in.
pub trait SimpleCommand {
/// Returns the content of the built-in command.
fn get_content(
name: &str,
content_type: ContentType,
options: &ContentOptions,
) -> Result<String, error::Error>;
/// Executes the built-in command.
fn execute<SE: extensions::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
context: commands::ExecutionContext<'_, SE>,
args: I,
) -> Result<results::ExecutionResult, error::Error>;
}
/// Returns a built-in command registration, given an implementation of the
/// `SimpleCommand` trait.
pub fn simple_builtin<B: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>()
-> Registration<SE> {
Registration {
execute_func: exec_simple_builtin::<B, SE>,
content_func: B::get_content,
disabled: false,
special_builtin: false,
declaration_builtin: false,
}
}
/// Returns a built-in command registration, given an implementation of the
/// `Command` trait.
pub fn builtin<B: Command + Send + Sync, SE: extensions::ShellExtensions>() -> Registration<SE> {
Registration {
execute_func: exec_builtin::<B, SE>,
content_func: get_builtin_content::<B>,
disabled: false,
special_builtin: false,
declaration_builtin: false,
}
}
/// Returns a built-in command registration, given an implementation of the
/// `DeclarationCommand` trait. Used for select commands that can take parsed
/// declarations as arguments.
pub fn decl_builtin<B: DeclarationCommand + Send + Sync, SE: extensions::ShellExtensions>()
-> Registration<SE> {
Registration {
execute_func: exec_declaration_builtin::<B, SE>,
content_func: get_builtin_content::<B>,
disabled: false,
special_builtin: false,
declaration_builtin: true,
}
}
#[allow(clippy::too_long_first_doc_paragraph)]
/// Returns a built-in command registration, given an implementation of the
/// `DeclarationCommand` trait that can be default-constructed. The command
/// implementation is expected to implement clap's `Parser` trait solely
/// for help/usage information. Arguments are passed directly to the command
/// via `set_declarations`. This is primarily only expected to be used with
/// select builtin commands that wrap other builtins (e.g., "builtin").
pub fn raw_arg_builtin<
B: DeclarationCommand + Default + Send + Sync,
SE: extensions::ShellExtensions,
>() -> Registration<SE> {
Registration {
execute_func: exec_raw_arg_builtin::<B, SE>,
content_func: get_builtin_content::<B>,
disabled: false,
special_builtin: false,
declaration_builtin: true,
}
}
fn get_builtin_content<T: Command + Send + Sync>(
name: &str,
content_type: ContentType,
options: &ContentOptions,
) -> Result<String, error::Error> {
T::get_content(name, content_type, options)
}
fn exec_simple_builtin<T: SimpleCommand + Send + Sync, SE: extensions::ShellExtensions>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
Box::pin(async move { exec_simple_builtin_impl::<T, SE>(context, args).await })
}
#[expect(clippy::unused_async)]
async fn exec_simple_builtin_impl<
T: SimpleCommand + Send + Sync,
SE: extensions::ShellExtensions,
>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> Result<results::ExecutionResult, error::Error> {
let plain_args = args.into_iter().map(|arg| match arg {
CommandArg::String(s) => s,
CommandArg::Assignment(a) => a.to_string(),
});
T::execute(context, plain_args)
}
fn exec_builtin<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
Box::pin(async move { exec_builtin_impl::<T, SE>(context, args).await })
}
async fn exec_builtin_impl<T: Command + Send + Sync, SE: extensions::ShellExtensions>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> Result<results::ExecutionResult, error::Error> {
let plain_args = args.into_iter().map(|arg| match arg {
CommandArg::String(s) => s,
CommandArg::Assignment(a) => a.to_string(),
});
let result = T::new(plain_args);
let command = match result {
Ok(command) => command,
Err(e) => {
let _ = writeln!(context.stderr(), "{e}");
return Ok(results::ExecutionExitCode::InvalidUsage.into());
}
};
call_builtin(command, context).await
}
fn exec_declaration_builtin<
T: DeclarationCommand + Send + Sync,
SE: extensions::ShellExtensions,
>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
Box::pin(async move { exec_declaration_builtin_impl::<T, SE>(context, args).await })
}
async fn exec_declaration_builtin_impl<
T: DeclarationCommand + Send + Sync,
SE: extensions::ShellExtensions,
>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> Result<results::ExecutionResult, error::Error> {
let mut options = vec![];
let mut declarations = vec![];
for (i, arg) in args.into_iter().enumerate() {
match arg {
CommandArg::String(s)
if i == 0 || (s.len() > 1 && (s.starts_with('-') || s.starts_with('+'))) =>
{
options.push(s);
}
_ => declarations.push(arg),
}
}
let result = T::new(options);
let mut command = match result {
Ok(command) => command,
Err(e) => {
let _ = writeln!(context.stderr(), "{e}");
return Ok(results::ExecutionExitCode::InvalidUsage.into());
}
};
command.set_declarations(declarations);
call_builtin(command, context).await
}
fn exec_raw_arg_builtin<
T: DeclarationCommand + Default + Send + Sync,
SE: extensions::ShellExtensions,
>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> BoxFuture<'_, Result<results::ExecutionResult, error::Error>> {
Box::pin(async move { exec_raw_arg_builtin_impl::<T, SE>(context, args).await })
}
async fn exec_raw_arg_builtin_impl<
T: DeclarationCommand + Default + Send + Sync,
SE: extensions::ShellExtensions,
>(
context: commands::ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> Result<results::ExecutionResult, error::Error> {
let mut command = T::default();
command.set_declarations(args);
call_builtin(command, context).await
}
async fn call_builtin(
command: impl Command,
context: commands::ExecutionContext<'_, impl extensions::ShellExtensions>,
) -> Result<results::ExecutionResult, error::Error> {
let builtin_name = context.command_name.clone();
let result = command
.execute(context)
.await
.map_err(|e| error::ErrorKind::BuiltinError(Box::new(e), builtin_name))?;
Ok(result)
}
+787
View File
@@ -0,0 +1,787 @@
//! Call stack representations.
use crate::{functions, traps};
use std::{
borrow::Cow,
collections::{HashSet, VecDeque},
sync::Arc,
};
use brush_parser::ast::SourceLocation;
/// Encapsulates info regarding a script call.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScriptCall {
/// The type of script call.
pub call_type: ScriptCallType,
/// The source info for the script called.
pub source_info: crate::SourceInfo,
}
impl ScriptCall {
/// Returns the name of the script that was called.
pub fn name(&self) -> Cow<'_, str> {
self.source_info.source.as_str().into()
}
}
/// The type of script call.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScriptCallType {
/// A script was sourced.
Source,
/// A script was executed.
Run,
}
impl std::fmt::Display for ScriptCall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.call_type {
ScriptCallType::Source => write!(f, "source({})", self.source_info),
ScriptCallType::Run => write!(f, "script({})", self.source_info),
}
}
}
/// Represents the type of a frame, indicating how it was invoked from
/// a different source context.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FrameType {
/// A script was called (sourced or executed).
Script(ScriptCall),
/// A function was called.
Function(FunctionCall),
/// A trap handler was invoked.
TrapHandler(traps::TrapSignal),
/// A string was eval'd.
Eval,
/// A command-line string (i.e., -c) was executed.
CommandString,
/// An interactive command session was started.
InteractiveSession,
}
impl FrameType {
/// Returns a name for the frame (i.e., script path or function name).
pub fn name(&self) -> Cow<'_, str> {
match self {
Self::Script(call) => call.name(),
Self::Function(call) => call.name(),
Self::TrapHandler(_) => "trap".into(),
Self::Eval => "eval".into(),
Self::CommandString => "-c".into(),
Self::InteractiveSession => "interactive".into(),
}
}
/// Returns `true` if the frame is for a function call.
pub const fn is_function(&self) -> bool {
matches!(self, Self::Function(..))
}
/// Returns `true` if the frame is for a script call.
pub const fn is_script(&self) -> bool {
matches!(self, Self::Script(..))
}
/// Returns `true` if the frame is for a trap handler.
pub const fn is_trap_handler(&self) -> bool {
matches!(self, Self::TrapHandler(_))
}
/// Returns `true` if the frame is for an interactive session.
pub const fn is_interactive_session(&self) -> bool {
matches!(self, Self::InteractiveSession)
}
/// Returns `true` if the frame is for a command string being executed.
pub const fn is_command_string(&self) -> bool {
matches!(self, Self::CommandString)
}
/// Returns `true` if the frame is for a sourced script.
pub const fn is_sourced_script(&self) -> bool {
matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Source))
}
/// Returns `true` if the frame is for a run script.
pub const fn is_run_script(&self) -> bool {
matches!(self, Self::Script(call) if matches!(call.call_type, ScriptCallType::Run))
}
}
impl std::fmt::Display for FrameType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Script(call) => call.fmt(f),
Self::Function(call) => call.fmt(f),
Self::TrapHandler(_) => write!(f, "trap"),
Self::Eval => write!(f, "eval"),
Self::CommandString => write!(f, "-c"),
Self::InteractiveSession => write!(f, "interactive"),
}
}
}
/// Describes the target of a function call.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FunctionCall {
/// The name of the function invoked.
pub function_name: String,
/// The invoked function.
pub function: functions::Registration,
}
impl FunctionCall {
/// Returns the name of the function that was called.
pub fn name(&self) -> Cow<'_, str> {
self.function_name.as_str().into()
}
}
impl std::fmt::Display for FunctionCall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "func({})", self.function_name)
}
}
/// Represents a single frame in a `CallStack`.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Frame {
/// The type of frame.
pub frame_type: FrameType,
/// The source information for the frame. The locations associated with AST nodes
/// executed in this frame should be interpreted as being relative to this
/// source info.
pub source_info: crate::SourceInfo,
/// The location of the entry point into this frame, within the frame of
/// reference of `source_info`. May be `None` if the entry point is not known.
pub entry: Option<Arc<crate::SourcePosition>>,
/// Information about the currently executing location. For the topmost frame on
/// the stack, this represents the current execution location. For older frames,
/// this represents the site from which a control transfer was made to the next
/// younger frame. May be `None` if the current location is not known. When present,
/// it is relative to the frame of reference of `source_info`.
pub current: Option<Arc<crate::SourcePosition>>,
/// Positional arguments (not including $0). May not be present for all frames.
pub args: Vec<String>,
/// Optionally, indicates an additional line offset within the current source context.
pub current_line_offset: usize,
}
impl Frame {
/// Returns the adjusted source info for this frame, combining the
/// frame's `source_info` and `current_line_offset`, if present.
pub fn adjusted_source_info(&self) -> crate::SourceInfo {
self.pos_as_source_info(None)
}
/// Returns the current position as a new `SourceInfo`, combining the
/// frame's `source_info` and `current` position.
pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
self.pos_as_source_info(self.current.as_ref())
}
fn pos_as_source_info(&self, pos: Option<&Arc<crate::SourcePosition>>) -> crate::SourceInfo {
let mut new_start = if let Some(existing_start) = &self.source_info.start {
if let Some(current) = pos {
Some(Arc::new(crate::SourcePosition {
index: existing_start.index + current.index,
line: existing_start.line + (current.line - 1),
column: if current.line <= 1 {
existing_start.column + (current.column - 1)
} else {
current.column
},
}))
} else {
Some(existing_start.clone())
}
} else {
pos.cloned()
};
if self.current_line_offset > 0 {
new_start = if let Some(new_start) = new_start {
let mut pos = (*new_start).clone();
pos.line += self.current_line_offset;
Some(Arc::new(pos))
} else {
Some(Arc::new(crate::SourcePosition {
index: 0,
line: self.current_line_offset + 1,
column: 1,
}))
};
}
crate::SourceInfo {
source: self.source_info.source.clone(),
start: new_start,
}
}
/// Returns the current line number.
pub fn current_line(&self) -> Option<usize> {
let start_line = self.source_info.start.as_ref().map_or(1, |pos| pos.line);
let current_line = self.current.as_ref().map(|pos| pos.line)?;
Some(start_line.saturating_sub(1) + current_line + self.current_line_offset)
}
/// Returns the current line number, relative to the frame's entry.
pub fn current_frame_relative_line(&self) -> Option<usize> {
let current_line = self.current.as_ref().map(|pos| pos.line)?;
let entry_line = self.entry.as_ref().map_or(1, |pos| pos.line);
Some(current_line.saturating_sub(entry_line) + self.current_line_offset + 1)
}
}
/// Options for formatting a call stack.
#[derive(Default)]
pub struct FormatOptions {
/// Whether or not to show args.
pub show_args: bool,
/// Whether or not to show frame entry points.
pub show_entry_points: bool,
}
/// Helper struct for formatting a call stack with custom options.
///
/// This struct implements `Display` and can be used to write a formatted
/// call stack to any type that implements `io::Write`.
pub struct FormatCallStack<'a> {
stack: &'a CallStack,
options: &'a FormatOptions,
}
impl<'a> FormatCallStack<'a> {
/// Creates a new formatter for the given call stack with the specified options.
///
/// # Arguments
///
/// * `stack` - The call stack to format.
/// * `options` - The formatting options to use.
pub const fn new(stack: &'a CallStack, options: &'a FormatOptions) -> Self {
Self { stack, options }
}
}
impl std::fmt::Display for FormatCallStack<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.stack.fmt_with_options(f, self.options)
}
}
/// Encapsulates a script call stack.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CallStack {
frames: VecDeque<Frame>,
func_call_depth: usize,
script_source_depth: usize,
active_trap_signals: HashSet<traps::TrapSignal>,
trap_delivery_suppress_count: usize,
}
impl CallStack {
/// Creates a formatter for this call stack with the given options.
///
/// # Arguments
///
/// * `options` - The formatting options to use.
pub const fn format<'a>(&'a self, options: &'a FormatOptions) -> FormatCallStack<'a> {
FormatCallStack::new(self, options)
}
/// Formats the call stack with the given options.
///
/// # Arguments
///
/// * `f` - The formatter to write to.
/// * `options` - The formatting options.
fn fmt_with_options(
&self,
f: &mut std::fmt::Formatter<'_>,
options: &FormatOptions,
) -> std::fmt::Result {
if self.is_empty() {
return Ok(());
}
color_print::cwriteln!(f, "<underline>Call stack (most recent first):</underline>")?;
for (index, frame) in self.iter().enumerate() {
let si = frame.current_pos_as_source_info();
color_print::cwrite!(
f,
" <dim>#{index}</dim><yellow>|</yellow> <strong>{}</strong>",
si.source
)?;
if let Some(pos) = &si.start {
color_print::cwrite!(f, ":<cyan>{}</cyan>,<cyan>{}</cyan>", pos.line, pos.column)?;
}
color_print::cwrite!(f, " (<dim>{}</dim>", frame.frame_type)?;
if options.show_entry_points {
if let Some(entry) = &frame.entry {
let entry_si = frame.pos_as_source_info(Some(entry));
if let Some(entry_start) = &entry_si.start {
color_print::cwrite!(
f,
" <dim>entered at {}:{}</dim>",
entry_si.source,
entry_start
)?;
}
}
}
color_print::cwriteln!(f, ")")?;
if !frame.args.is_empty() && options.show_args {
for (i, arg) in frame.args.iter().enumerate() {
color_print::cwriteln!(
f,
" <yellow>${}</yellow>: <blue>{}</blue>",
i + 1,
arg
)?;
}
}
}
Ok(())
}
}
impl std::fmt::Display for CallStack {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.fmt_with_options(f, &FormatOptions::default())
}
}
impl std::ops::Index<usize> for CallStack {
type Output = Frame;
fn index(&self, index: usize) -> &Self::Output {
&self.frames[index]
}
}
impl CallStack {
/// Creates a new empty script call stack.
pub fn new() -> Self {
Self::default()
}
/// Removes the top from from the stack. If the stack is empty, does nothing and
/// returns `None`; otherwise, returns the removed call frame.
pub fn pop(&mut self) -> Option<Frame> {
let frame = self.frames.pop_front()?;
if frame.frame_type.is_function() {
self.func_call_depth = self.func_call_depth.saturating_sub(1);
}
if frame.frame_type.is_sourced_script() {
self.script_source_depth = self.script_source_depth.saturating_sub(1);
}
if let FrameType::TrapHandler(signal) = &frame.frame_type {
self.active_trap_signals.remove(signal);
}
Some(frame)
}
/// Returns a reference to the current (topmost) call frame in the stack.
/// Returns `None` if the stack is empty.
pub fn current_frame(&self) -> Option<&Frame> {
self.frames.front()
}
/// Returns the position in the current (topmost) call frame in the stack,
/// expressed as a new `SourceInfo`. Note that this may not be identical
/// to that frame's `SourceInfo` since it may include an offset representing
/// the current execution position within that source.
pub fn current_pos_as_source_info(&self) -> crate::SourceInfo {
let Some(frame) = self.frames.front() else {
return crate::SourceInfo::default();
};
frame.current_pos_as_source_info()
}
/// Updates the currently executing position in the top stack frame.
pub fn set_current_pos(&mut self, position: Option<Arc<crate::SourcePosition>>) {
if let Some(frame) = self.frames.front_mut() {
frame.current = position;
}
}
/// Increments the current line offset in the top stack frame by the given delta.
///
/// # Arguments
///
/// * `delta` - The number of lines to increment the current line offset by.
pub(crate) fn increment_current_line_offset(&mut self, delta: usize) {
let Some(frame) = self.frames.front_mut() else {
return;
};
frame.current_line_offset += delta;
}
/// Pushes a new script call frame onto the stack.
///
/// # Arguments
///
/// * `call_type` - The type of script call (sourced or executed).
/// * `source_info` - The source of the script.
/// * `args` - The positional arguments for the script call.
pub fn push_script(
&mut self,
call_type: ScriptCallType,
source_info: &crate::SourceInfo,
args: impl IntoIterator<Item = String>,
) {
self.frames.push_front(Frame {
frame_type: FrameType::Script(ScriptCall {
call_type,
source_info: source_info.to_owned(),
}),
args: args.into_iter().collect(),
source_info: source_info.to_owned(),
current_line_offset: 0,
current: None, // TODO(source-info): fill this out
entry: None, // TODO(source-info): fill this out
});
if matches!(call_type, ScriptCallType::Source) {
self.script_source_depth += 1;
}
}
/// Pushes a new trap handler frame onto the stack.
///
/// # Arguments
///
/// * `signal` - The signal being handled.
/// * `handler` - The trap handler being invoked, if any.
pub fn push_trap_handler(
&mut self,
signal: traps::TrapSignal,
handler: Option<&traps::TrapHandler>,
) {
let source_info =
handler.map_or_else(crate::SourceInfo::default, |h| h.source_info.clone());
self.frames.push_front(Frame {
frame_type: FrameType::TrapHandler(signal),
args: vec![],
source_info,
current_line_offset: 0,
current: None, // TODO(source-info): fill this out
entry: None, // TODO(source-info): fill this out
});
self.active_trap_signals.insert(signal);
}
/// Pushes a new eval frame onto the stack.
pub fn push_eval(&mut self) {
self.frames.push_front(Frame {
frame_type: FrameType::Eval,
args: vec![],
source_info: crate::SourceInfo::from("eval"), // TODO(source-info): fill this out
current_line_offset: 0,
current: None, // TODO(source-info): fill this out
entry: None, // TODO(source-info): fill this out
});
}
/// Pushes a new command string frame onto the stack.
pub fn push_command_string(&mut self) {
self.frames.push_front(Frame {
frame_type: FrameType::CommandString,
args: vec![],
source_info: crate::SourceInfo::from("environment"),
current_line_offset: 0,
current: None, // TODO(source-info): fill this out
entry: None, // TODO(source-info): fill this out
});
}
/// Pushes a new interactive session frame onto the stack.
pub fn push_interactive_session(&mut self) {
self.frames.push_front(Frame {
frame_type: FrameType::InteractiveSession,
args: vec![],
current_line_offset: 0,
source_info: crate::SourceInfo::from("main"),
current: None, // TODO(source-info): fill this out
entry: None, // TODO(source-info): fill this out
});
}
/// Pushes a new function call frame onto the stack.
///
/// # Arguments
///
/// * `name` - The name of the function being called.
/// * `function` - The function being called.
/// * `args` - The positional arguments for the function call.
pub fn push_function(
&mut self,
name: impl Into<String>,
function: &functions::Registration,
args: impl IntoIterator<Item = String>,
) {
self.frames.push_front(Frame {
frame_type: FrameType::Function(FunctionCall {
function_name: name.into(),
function: function.to_owned(),
}),
args: args.into_iter().collect(),
source_info: function.source().clone(),
entry: function.definition().location().map(|span| span.start),
current: None, // TODO(source-info): fill this out
current_line_offset: 0,
});
self.func_call_depth += 1;
}
/// Iterates through the function calls on the stack.
pub fn iter_function_calls(&self) -> impl Iterator<Item = &FunctionCall> {
self.iter().filter_map(|frame| {
if let FrameType::Function(call) = &frame.frame_type {
Some(call)
} else {
None
}
})
}
/// Iterates through the script calls on the stack.
pub fn iter_script_calls(&self) -> impl Iterator<Item = &ScriptCall> {
self.iter().filter_map(|frame| {
if let FrameType::Script(call) = &frame.frame_type {
Some(call)
} else {
None
}
})
}
/// Returns whether or not the current script stack frame is a sourced script.
pub fn in_sourced_script(&self) -> bool {
self.iter_script_calls()
.next()
.is_some_and(|call| matches!(call.call_type, ScriptCallType::Source))
}
/// Returns the current depth of function calls in the call stack.
pub const fn function_call_depth(&self) -> usize {
self.func_call_depth
}
/// Returns the current depth of sourced script calls in the call stack.
pub const fn script_source_depth(&self) -> usize {
self.script_source_depth
}
/// Returns whether the given trap signal is currently being handled
/// (i.e., there is an active frame on the stack for this signal).
pub fn is_trap_signal_active(&self, signal: traps::TrapSignal) -> bool {
self.active_trap_signals.contains(&signal)
}
/// Clears the set of active trap signals. This should be called when
/// creating subshells so they start with fresh trap execution state
/// independent of the parent shell's currently-executing traps.
pub fn clear_active_trap_signals(&mut self) {
self.active_trap_signals.clear();
}
/// Returns whether the given trap signal is currently suppressed.
pub const fn is_trap_delivery_suppressed(&self) -> bool {
self.trap_delivery_suppress_count > 0
}
/// 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 const fn acquire_trap_delivery_block(&mut self) {
self.trap_delivery_suppress_count += 1;
}
/// Releases a block on trap delivery; note that trap delivery will remain
/// suppressed until all blocks have been released.
pub const fn release_trap_delivery_block(&mut self) {
self.trap_delivery_suppress_count = self.trap_delivery_suppress_count.saturating_sub(1);
}
/// Returns whether or not the shell is actively executing in a shell function.
pub fn in_function(&self) -> bool {
self.iter_function_calls().next().is_some()
}
/// Returns the current depth of the call stack.
pub fn depth(&self) -> usize {
self.frames.len()
}
/// Returns whether or not the call stack is empty.
pub fn is_empty(&self) -> bool {
self.frames.is_empty()
}
/// Returns an iterator over the call frames, starting from the most
/// recent.
pub fn iter(&self) -> impl Iterator<Item = &Frame> {
self.frames.iter()
}
/// Returns a mutable iterator over the call frames, starting from the most
/// recent.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Frame> {
self.frames.iter_mut()
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::SourceInfo;
use pretty_assertions::assert_matches;
#[test]
fn test_call_stack_new() {
let stack = CallStack::new();
assert!(stack.is_empty());
assert_eq!(stack.depth(), 0);
}
#[test]
fn test_call_stack_default() {
let stack = CallStack::default();
assert!(stack.is_empty());
assert_eq!(stack.depth(), 0);
}
#[test]
fn test_call_stack_push_pop() {
let mut stack = CallStack::new();
stack.push_script(
ScriptCallType::Source,
&SourceInfo::from(PathBuf::from("script1.sh")),
vec![],
);
assert!(!stack.is_empty());
assert_eq!(stack.depth(), 1);
stack.push_script(
ScriptCallType::Run,
&SourceInfo::from(PathBuf::from("script2.sh")),
vec![],
);
assert_eq!(stack.depth(), 2);
let frame = stack.pop().unwrap();
assert_matches!(
frame.frame_type,
FrameType::Script(ScriptCall {
call_type: ScriptCallType::Run,
source_info: SourceInfo {
source: file_path,
..
},
}) if &file_path == "script2.sh"
);
assert_eq!(stack.depth(), 1);
let frame = stack.pop().unwrap();
assert_matches!(
frame.frame_type,
FrameType::Script(ScriptCall {
call_type: ScriptCallType::Source,
source_info: SourceInfo {
source: file_path,
..
},
}) if &file_path == "script1.sh"
);
assert_eq!(stack.depth(), 0);
assert!(stack.is_empty());
}
#[test]
fn test_call_stack_pop_empty() {
let mut stack = CallStack::new();
assert!(stack.pop().is_none());
}
#[test]
fn test_in_sourced_script() {
let mut stack = CallStack::new();
assert!(!stack.in_sourced_script());
stack.push_script(
ScriptCallType::Run,
&SourceInfo::from(PathBuf::from("script1.sh")),
vec![],
);
assert!(!stack.in_sourced_script());
stack.push_script(
ScriptCallType::Source,
&SourceInfo::from(PathBuf::from("script2.sh")),
vec![],
);
assert!(stack.in_sourced_script());
stack.pop();
assert!(!stack.in_sourced_script());
}
#[test]
fn test_call_stack_iter() {
let mut stack = CallStack::new();
stack.push_script(
ScriptCallType::Source,
&SourceInfo::from(PathBuf::from("script1.sh")),
vec![],
);
stack.push_script(
ScriptCallType::Run,
&SourceInfo::from(PathBuf::from("script2.sh")),
vec![],
);
stack.push_script(
ScriptCallType::Source,
&SourceInfo::from(PathBuf::from("script3.sh")),
vec![],
);
let frames: Vec<_> = stack.iter().collect();
assert_eq!(frames.len(), 3);
assert_matches!(&frames[0].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script3.sh");
assert_matches!(&frames[1].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script2.sh");
assert_matches!(&frames[2].frame_type, FrameType::Script(ScriptCall { source_info: SourceInfo { source: file_path, .. }, .. }) if file_path == "script1.sh");
}
}
+873
View File
@@ -0,0 +1,873 @@
//! Command execution
use std::{
borrow::Cow,
ffi::OsStr,
fmt::Display,
path::{Path, PathBuf},
process::Stdio,
};
use brush_parser::ast;
use itertools::Itertools;
use sys::commands::{CommandExt, CommandFdInjectionExt, CommandFgControlExt};
use crate::{
ErrorKind, ExecutionControlFlow, ExecutionExitCode, ExecutionParameters, ExecutionResult,
Shell, ShellFd, builtins, commands, env, error, escape,
extensions::{self, ShellExtensions},
functions,
interp::{self, Execute, ProcessGroupPolicy},
openfiles::{self, OpenFile, OpenFiles},
pathsearch, processes,
results::ExecutionSpawnResult,
sys, trace_categories, traps, variables,
};
/// Encapsulates the result of waiting for a command to complete.
pub enum CommandWaitResult {
/// The command completed.
CommandCompleted(ExecutionResult),
/// The command was stopped before it completed.
CommandStopped(ExecutionResult, processes::ChildProcess),
}
/// Represents the context for executing a command.
pub struct ExecutionContext<'a, SE: ShellExtensions = extensions::DefaultShellExtensions> {
/// The shell in which the command is being executed.
pub shell: &'a mut Shell<SE>,
/// The name of the command being executed.
pub command_name: String,
/// The parameters for the execution.
pub params: ExecutionParameters,
}
impl<SE: ShellExtensions> ExecutionContext<'_, SE> {
/// Returns the standard input file; usable with `write!` et al.
pub fn stdin(&self) -> impl std::io::Read + 'static {
self.params.stdin(self.shell)
}
/// Returns the standard output file; usable with `write!` et al.
pub fn stdout(&self) -> impl std::io::Write + 'static {
self.params.stdout(self.shell)
}
/// Returns the standard error file; usable with `write!` et al.
pub fn stderr(&self) -> impl std::io::Write + 'static {
self.params.stderr(self.shell)
}
/// Returns the file descriptor with the given number. Returns `None`
/// if the file descriptor is not open.
///
/// # Arguments
///
/// * `fd` - The file descriptor number to retrieve.
pub fn try_fd(&self, fd: ShellFd) -> Option<openfiles::OpenFile> {
self.params.try_fd(self.shell, fd)
}
/// Iterates over all open file descriptors.
pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
self.params.iter_fds(self.shell)
}
}
/// An argument to a command.
#[derive(Clone, Debug)]
pub enum CommandArg {
/// A simple string argument.
String(String),
/// An assignment/declaration; typically treated as a string, but will
/// be specially handled by a limited set of built-in commands.
Assignment(ast::Assignment),
}
impl Display for CommandArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::String(s) => f.write_str(s),
Self::Assignment(a) => write!(f, "{a}"),
}
}
}
impl From<String> for CommandArg {
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<&String> for CommandArg {
fn from(value: &String) -> Self {
Self::String(value.clone())
}
}
impl CommandArg {
pub(crate) fn quote_for_tracing(&self) -> Cow<'_, str> {
match self {
Self::String(s) => escape::quote_if_needed(s, escape::QuoteMode::SingleQuote),
Self::Assignment(a) => {
let mut s = a.name.to_string();
let op = if a.append { "+=" } else { "=" };
s.push_str(op);
s.push_str(&escape::quote_if_needed(
a.value.to_string().as_str(),
escape::QuoteMode::SingleQuote,
));
s.into()
}
}
}
}
/// Encapsulates a possibly-owned reference to a `Shell` for command execution.
pub enum ShellForCommand<'a, SE: extensions::ShellExtensions> {
/// The command is run in the same shell as its parent; the provided
/// mutable reference allows modifying the parent shell.
ParentShell(&'a mut Shell<SE>),
/// The command is run in its own owned shell (which is also provided).
OwnedShell {
/// The owned shell.
target: Box<Shell<SE>>,
/// The parent shell.
parent: &'a mut Shell<SE>,
},
}
impl<SE: extensions::ShellExtensions> std::ops::Deref for ShellForCommand<'_, SE> {
type Target = Shell<SE>;
fn deref(&self) -> &Self::Target {
match self {
ShellForCommand::ParentShell(shell) => shell,
ShellForCommand::OwnedShell { target, .. } => target,
}
}
}
impl<SE: extensions::ShellExtensions> std::ops::DerefMut for ShellForCommand<'_, SE> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
ShellForCommand::ParentShell(shell) => shell,
ShellForCommand::OwnedShell { target, .. } => target,
}
}
}
/// Composes a `std::process::Command` to execute the given command. Appropriately
/// configures the command name and arguments, redirections, injected file
/// descriptors, environment variables, etc.
///
/// # Arguments
///
/// * `context` - The execution context in which the command is being composed.
/// * `command_name` - The name of the command to execute.
/// * `argv0` - The value to use for `argv[0]` (may be different from the command).
/// * `args` - The arguments to pass to the command.
/// * `empty_env` - If true, the command will be executed with an empty environment; if false, the
/// command will inherit environment variables marked as exported in the provided `Shell`.
#[allow(unused_variables, reason = "argv0 is only used on unix platforms")]
pub fn compose_std_command<S: AsRef<OsStr>, SE: extensions::ShellExtensions>(
context: &ExecutionContext<'_, SE>,
command_name: &str,
argv0: &str,
args: &[S],
empty_env: bool,
) -> Result<std::process::Command, error::Error> {
let mut cmd = std::process::Command::new(command_name);
// Override argv[0].
// NOTE: Not supported on all platforms.
cmd.arg0(argv0);
// Pass through args.
cmd.args(args);
// Use the shell's current working dir.
cmd.current_dir(context.shell.working_dir());
// Start with a clear environment.
cmd.env_clear();
// Add in exported variables.
if !empty_env {
for (k, v) in context.shell.env().iter_exported() {
// NOTE: To match bash behavior, we only include exported variables
// that are set (i.e., have a value). This means a variable that
// shows up in `declare -p` but has no *set* value will be omitted.
if v.value().is_set() {
cmd.env(k.as_str(), v.value().to_cow_str(context.shell).as_ref());
}
}
// Set _ to the resolved command path for external commands.
cmd.env("_", command_name);
}
// Add in exported functions.
if !empty_env {
for (func_name, registration) in context.shell.funcs().iter() {
if registration.is_exported() {
let var_name = std::format!("BASH_FUNC_{func_name}%%");
let value = std::format!("() {}", registration.definition().body);
cmd.env(var_name, value);
}
}
}
// Redirect stdin, if applicable.
match context.try_fd(OpenFiles::STDIN_FD) {
Some(OpenFile::Stdin(_)) | None => (),
Some(stdin_file) => {
let as_stdio: Stdio = stdin_file.into();
cmd.stdin(as_stdio);
}
}
// Redirect stdout, if applicable.
match context.try_fd(OpenFiles::STDOUT_FD) {
Some(OpenFile::Stdout(_)) | None => (),
Some(stdout_file) => {
let as_stdio: Stdio = stdout_file.into();
cmd.stdout(as_stdio);
}
}
// Redirect stderr, if applicable.
match context.try_fd(OpenFiles::STDERR_FD) {
Some(OpenFile::Stderr(_)) | None => {}
Some(stderr_file) => {
let as_stdio: Stdio = stderr_file.into();
cmd.stderr(as_stdio);
}
}
// Inject any other fds.
let other_files = context.iter_fds().filter(|(fd, _)| {
*fd != OpenFiles::STDIN_FD && *fd != OpenFiles::STDOUT_FD && *fd != OpenFiles::STDERR_FD
});
cmd.inject_fds(other_files)?;
Ok(cmd)
}
pub(crate) async fn on_preexecute(
cmd: &mut commands::SimpleCommand<'_, impl extensions::ShellExtensions>,
) -> Result<(), error::Error> {
// Set BASH_COMMAND before invoking the DEBUG trap (and generally before
// executing commands).
let full_cmd = cmd.args.iter().map(|arg| arg.to_string()).join(" ");
cmd.shell.env_mut().update_or_add(
"BASH_COMMAND",
variables::ShellValueLiteral::Scalar(full_cmd),
|_| Ok(()),
env::EnvironmentLookup::Anywhere,
env::EnvironmentScope::Global,
)?;
// Fire the DEBUG trap if one is registered.
if cmd.shell.traps().handles(traps::TrapSignal::Debug) {
let _ = cmd
.shell
.invoke_trap_handler(traps::TrapSignal::Debug, &cmd.params)
.await?;
}
Ok(())
}
/// Represents a simple command to be executed.
pub struct SimpleCommand<'a, SE: extensions::ShellExtensions> {
/// The shell to run the command in.
shell: ShellForCommand<'a, SE>,
/// The execution parameters for the command.
pub params: ExecutionParameters,
/// The name of the command to execute.
pub command_name: String,
/// The arguments to the command, including the command itself.
pub args: Vec<CommandArg>,
/// Whether to consider shell functions when looking up the command name.
/// If true, shell functions will be checked; if false, they will be ignored.
pub use_functions: bool,
/// Optional list of directories to search for external commands. If left
/// `None`, the default search logic will be used.
pub path_dirs: Option<Vec<PathBuf>>,
/// The process group ID to use for externally executed commands. This may be
/// `None`, in which case the default behavior will be used.
pub process_group_id: Option<i32>,
/// Optional override for the `argv[0]` value presented to an externally
/// spawned process. When `None`, `command_name` is used.
pub argv0: Option<String>,
/// Optionally provides a function that can run after execution occurs. Note
/// that it is *not* invoked if the shell is discarded during the execution
/// process.
#[allow(clippy::type_complexity)]
pub post_execute: Option<fn(&mut Shell<SE>) -> Result<(), error::Error>>,
}
impl<'a, SE: extensions::ShellExtensions> SimpleCommand<'a, SE> {
/// Creates a new `SimpleCommand` instance.
///
/// # Arguments
///
/// * `shell` - The shell in which to execute the command.
/// * `params` - The execution parameters for the command.
/// * `command_name` - The name of the command to execute.
/// * `args` - The arguments to the command, including the command itself.
pub const fn new(
shell: ShellForCommand<'a, SE>,
params: ExecutionParameters,
command_name: String,
args: Vec<CommandArg>,
) -> Self {
Self {
shell,
params,
command_name,
args,
use_functions: true,
path_dirs: None,
process_group_id: None,
argv0: None,
post_execute: None,
}
}
/// Executes the simple command.
///
/// The command may be a builtin, a shell function, or an externally
/// executed command. This function's implementation is responsible for
/// dispatching it appropriately according to the context provided.
#[allow(
clippy::missing_panics_doc,
reason = "these unwrap calls should not panic"
)]
pub async fn execute(mut self) -> Result<ExecutionSpawnResult, error::Error> {
// First see if it's the name of a builtin.
let builtin = self.shell.builtins().get(&self.command_name).cloned();
// If we're in POSIX mode and found a special builtin (that's not disabled), then invoke it
// without considering functions.
if self.shell.options().posix_mode
&& builtin
.as_ref()
.is_some_and(|r| !r.disabled && r.special_builtin)
{
#[allow(clippy::unwrap_used, reason = "we just checked that builtin is Some")]
let builtin = builtin.unwrap();
return self.execute_via_builtin(builtin).await;
}
// Assuming we weren't requested not to do so, check if it's the name of
// a shell function.
if self.use_functions {
if let Some(func_registration) =
self.shell.funcs().get(self.command_name.as_str()).cloned()
{
return self.execute_via_function(func_registration).await;
}
}
// If we haven't yet resolved the command name and found a builtin that's not disabled,
// then invoke it.
if let Some(builtin) = builtin {
if !builtin.disabled {
return self.execute_via_builtin(builtin).await;
}
}
// We still haven't found a command to invoke. We'll need to look for an external command.
if !sys::fs::contains_path_separator(&self.command_name) {
// All else failed; if we were given path directories to search, try to look through
// them for a matching executable. Otherwise, use our default search logic.
let path = if let Some(path_dirs) = &self.path_dirs {
pathsearch::search_for_executable(path_dirs.iter(), self.command_name.as_str())
.next()
} else {
self.shell
.find_first_executable_in_path_using_cache(&self.command_name)
};
if let Some(path) = path {
self.execute_via_external(&path)
} else {
// Bash updates $_ even when the command is not found, so mirror
// that here before reporting the error.
let last_arg = Self::take_last_arg(&self.args);
self.shell.update_last_arg_variable(last_arg);
if let Some(post_execute) = self.post_execute {
let _ = post_execute(&mut self.shell);
}
Err(ErrorKind::CommandNotFound(self.command_name).into())
}
} else {
let command_name = PathBuf::from(self.command_name.clone());
self.execute_via_external(command_name.as_path())
}
}
/// Extracts the owned string representation of the last argument of a
/// command, suitable for recording into `$_`.
fn take_last_arg(args: &[CommandArg]) -> Option<String> {
args.last().map(ToString::to_string)
}
async fn execute_via_builtin(
self,
builtin: builtins::Registration<SE>,
) -> Result<ExecutionSpawnResult, error::Error> {
match self.shell {
ShellForCommand::OwnedShell { target, .. } => {
Ok(Self::execute_via_builtin_in_owned_shell(
*target,
self.params,
builtin,
self.command_name,
self.args,
))
}
ShellForCommand::ParentShell(..) => {
self.execute_via_builtin_in_parent_shell(builtin).await
}
}
}
fn execute_via_builtin_in_owned_shell(
mut shell: Shell<SE>,
params: ExecutionParameters,
builtin: builtins::Registration<SE>,
command_name: String,
args: Vec<CommandArg>,
) -> ExecutionSpawnResult {
let last_arg = Self::take_last_arg(&args);
let join_handle = tokio::task::spawn_blocking(move || {
let cmd_context = ExecutionContext {
shell: &mut shell,
command_name,
params,
};
let rt = tokio::runtime::Handle::current();
let result = rt.block_on(execute_builtin_command(&builtin, cmd_context, args));
// Update $_ after command execution.
shell.update_last_arg_variable(last_arg);
result
});
ExecutionSpawnResult::StartedTask(join_handle)
}
async fn execute_via_builtin_in_parent_shell(
self,
builtin: builtins::Registration<SE>,
) -> Result<ExecutionSpawnResult, error::Error> {
let mut shell = self.shell;
let last_arg = Self::take_last_arg(&self.args);
let cmd_context = ExecutionContext {
shell: &mut shell,
command_name: self.command_name,
params: self.params,
};
let result = execute_builtin_command(&builtin, cmd_context, self.args).await;
// Update $_ after command execution.
shell.update_last_arg_variable(last_arg);
if let Some(post_execute) = self.post_execute {
let _ = post_execute(&mut shell);
}
let result = result?;
Ok(result.into())
}
async fn execute_via_function(
self,
func_registration: functions::Registration,
) -> Result<ExecutionSpawnResult, error::Error> {
let mut shell = self.shell;
let last_arg = Self::take_last_arg(&self.args);
let cmd_context = ExecutionContext {
shell: &mut shell,
command_name: self.command_name,
params: self.params,
};
// Strip the function name off args.
let result = invoke_shell_function(func_registration, cmd_context, &self.args[1..]).await;
// $_ is reset *after* the function body runs, to the last argument of
// the invocation (or the function name itself if zero args). Any
// mutations made inside the body are overwritten — this matches bash,
// where the caller observes only the invocation's last argument.
shell.update_last_arg_variable(last_arg);
if let Some(post_execute) = self.post_execute {
let _ = post_execute(&mut shell);
}
result
}
fn execute_via_external(self, path: &Path) -> Result<ExecutionSpawnResult, error::Error> {
let mut shell = self.shell;
let last_arg = Self::take_last_arg(&self.args);
let cmd_context = ExecutionContext {
shell: &mut shell,
command_name: self.command_name,
params: self.params,
};
let resolved_path = path.to_string_lossy();
let result = execute_external_command(
cmd_context,
resolved_path.as_ref(),
self.process_group_id,
self.argv0.as_deref(),
&self.args[1..],
);
// Update $_ after command execution.
shell.update_last_arg_variable(last_arg);
if let Some(post_execute) = self.post_execute {
let _ = post_execute(&mut shell);
}
result
}
}
pub(crate) fn execute_external_command(
context: ExecutionContext<'_, impl extensions::ShellExtensions>,
executable_path: &str,
process_group_id: Option<i32>,
argv0_override: Option<&str>,
args: &[CommandArg],
) -> Result<ExecutionSpawnResult, error::Error> {
// Filter out the args; we only want strings.
let cmd_args = args
.iter()
.filter_map(|e| {
if let CommandArg::String(s) = e {
Some(s)
} else {
None
}
})
.collect::<Vec<_>>();
// Before we lose ownership of the open files, figure out if stdin will be a terminal.
let child_stdin_is_terminal = context
.try_fd(openfiles::OpenFiles::STDIN_FD)
.is_some_and(|f| f.is_terminal());
// Figure out if we should be setting up a new process group.
let new_pg = matches!(
context.params.process_group_policy,
ProcessGroupPolicy::NewProcessGroup
);
// Compose the std::process::Command that encapsulates what we want to launch.
// argv[0] defaults to context.command_name (the user-facing name of the
// command) unless the caller specified an explicit override.
let argv0 = argv0_override.unwrap_or(context.command_name.as_str());
#[allow(unused_mut, reason = "only mutated on unix platforms")]
let mut cmd = compose_std_command(
&context,
executable_path,
argv0,
cmd_args.as_slice(),
false, /* empty environment? */
)?;
// Set up process group state.
if new_pg {
// Check if we'll be doing terminal control setup (which includes setsid)
if child_stdin_is_terminal && context.shell.options().external_cmd_leads_session {
// Don't set process_group(0) - setsid() in pre_exec will handle it
cmd.lead_session();
} else {
// Normal case: create new process group in current session
cmd.process_group(0);
if child_stdin_is_terminal {
cmd.take_foreground();
}
}
} else {
// We need to join an established process group.
if let Some(pgid) = process_group_id {
cmd.process_group(pgid);
}
}
// When tracing is enabled, report.
tracing::debug!(
target: trace_categories::COMMANDS,
"Spawning: cmd='{} {}'",
cmd.get_program().to_string_lossy().to_string(),
cmd.get_args()
.map(|a| a.to_string_lossy().to_string())
.join(" ")
);
match sys::process::spawn(cmd) {
Ok(child) => {
// Retrieve the pid.
#[expect(clippy::cast_possible_wrap)]
let pid = child.id().map(|id| id as i32);
let mut actual_pgid = process_group_id;
if let Some(pid) = &pid {
if new_pg {
actual_pgid = Some(*pid);
}
} else {
tracing::warn!("could not retrieve pid for child process");
}
Ok(ExecutionSpawnResult::StartedProcess(
processes::ChildProcess::new(child, pid, actual_pgid),
))
}
Err(spawn_err) => {
if context.shell.options().interactive {
sys::terminal::move_self_to_foreground()?;
}
if spawn_err.kind() == std::io::ErrorKind::NotFound {
if !context.shell.working_dir().exists() {
Err(
error::ErrorKind::WorkingDirMissing(context.shell.working_dir().to_owned())
.into(),
)
} else {
Err(error::ErrorKind::CommandNotFound(context.command_name).into())
}
} else {
Err(
error::ErrorKind::FailedToExecuteCommand(context.command_name, spawn_err)
.into(),
)
}
}
}
}
async fn execute_builtin_command<SE: extensions::ShellExtensions>(
builtin: &builtins::Registration<SE>,
context: ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> Result<ExecutionResult, error::Error> {
// In POSIX mode, special builtins that return errors are to be treated as fatal.
let mark_errors_fatal = builtin.special_builtin && context.shell.options().posix_mode;
match (builtin.execute_func)(context, args).await {
Ok(result) => Ok(result),
Err(e) => {
// Broken pipe errors should silently return the appropriate exit code
if let Some(io_err) = e.as_io_error() {
if io_err.kind() == std::io::ErrorKind::BrokenPipe {
return Ok(ExecutionExitCode::from(io_err).into());
}
}
Err(if mark_errors_fatal { e.into_fatal() } else { e })
}
}
}
pub(crate) async fn invoke_shell_function(
function: functions::Registration,
mut context: ExecutionContext<'_, impl extensions::ShellExtensions>,
args: &[CommandArg],
) -> Result<ExecutionSpawnResult, error::Error> {
let ast::FunctionBody(body, redirects) = &function.definition().body;
// Apply any redirects specified at function definition-time.
if let Some(redirects) = redirects {
for redirect in &redirects.0 {
interp::setup_redirect(context.shell, &mut context.params, redirect).await?;
}
}
let positional_args = args.iter().map(|a| a.to_string());
// Pass through open files.
let params = context.params.clone();
// Note that we're going deeper. Once we do this, we need to make sure we don't bail early
// before "exiting" the function.
context.shell.enter_function(
context.command_name.as_str(),
&function,
positional_args,
&context.params,
)?;
// Invoke the function.
let result = body.execute(context.shell, &params).await;
// Clean up parameters so any owned files are closed.
drop(params);
// We've come back out, reflect it.
context.shell.leave_function()?;
// Get the actual execution result from the body of the function.
let mut result = result?;
// Handle control-flow.
match result.next_control_flow {
ExecutionControlFlow::BreakLoop { .. } | ExecutionControlFlow::ContinueLoop { .. } => {
return error::unimp("break or continue returned from function invocation");
}
ExecutionControlFlow::ReturnFromFunctionOrScript => {
// It's now been handled.
result.next_control_flow = ExecutionControlFlow::Normal;
}
_ => {}
}
Ok(result.into())
}
pub(crate) async fn invoke_command_in_subshell_and_get_output(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
s: String,
) -> Result<String, error::Error> {
// Instantiate a subshell to run the command in.
let mut subshell = shell.clone();
// Command substitutions don't inherit errexit by default. Only inherit it when
// command_subst_inherits_errexit is enabled, otherwise disable errexit in the subshell.
if !shell.options().command_subst_inherits_errexit {
subshell.options_mut().exit_on_nonzero_command_exit = false;
}
// Get our own set of parameters we can customize and use.
let mut params = params.clone();
params.process_group_policy = ProcessGroupPolicy::SameProcessGroup;
// Set up pipe so we can read the output.
let (reader, writer) = std::io::pipe()?;
params.set_fd(OpenFiles::STDOUT_FD, writer.into());
let mut async_reader = sys::async_pipe::AsyncPipeReader::new(reader)?;
let cmd_join_handle = tokio::spawn(run_substitution_command(subshell, params, s));
let output_str = async_reader.read_to_string().await?;
// Now observe the command's completion.
let run_result = cmd_join_handle.await?;
let cmd_result = run_result?;
// Store the status.
shell.set_last_exit_status(cmd_result.exit_code.into());
// Note: $_ is naturally isolated from the parent because we cloned the
// shell to run the substitution.
Ok(output_str)
}
async fn run_substitution_command(
mut shell: Shell<impl extensions::ShellExtensions>,
mut params: ExecutionParameters,
command: String,
) -> Result<ExecutionResult, error::Error> {
// Parse the string into a whole shell program.
let parse_result = shell.parse_string(command);
// Check for a command that is only an input redirection ("< file").
// If detected, emulate `cat file` to stdout and return immediately.
// If we failed to parse, then we'll fall below and handle it there.
if let Ok(program) = &parse_result {
if let Some(redir) = try_unwrap_bare_input_redir_program(program) {
interp::setup_redirect(&mut shell, &mut params, redir).await?;
std::io::copy(&mut params.stdin(&shell), &mut params.stdout(&shell))?;
return Ok(ExecutionResult::new(0));
}
}
// TODO(source-info): review this
let source_info = crate::SourceInfo::from("main");
// Handle the parse result using default shell behavior.
shell
.run_parsed_result(parse_result, &source_info, &params)
.await
}
// Detects a subshell command that consists solely of a single input redirection
// (e.g., "< file"), returning the IoRedirect when present.
fn try_unwrap_bare_input_redir_program(program: &ast::Program) -> Option<&ast::IoRedirect> {
// We're looking for exactly one complete command...
let [complete] = program.complete_commands.as_slice() else {
return None;
};
// ...a single list item...
let ast::CompoundList(items) = complete;
let [item] = items.as_slice() else {
return None;
};
// ...with a single pipeline (no && or || chaining)...
let and_or = &item.0;
if !and_or.additional.is_empty() {
return None;
}
// ...not negated...
let pipeline = &and_or.first;
if pipeline.bang {
return None;
}
// ...with a single command in the pipeline...
let [ast::Command::Simple(simple_cmd)] = pipeline.seq.as_slice() else {
return None;
};
// ...with no program word/name and no suffix...
if simple_cmd.word_or_name.is_some() || simple_cmd.suffix.is_some() {
return None;
}
// ...and exactly one prefix containing an I/O redirect...
let prefix = simple_cmd.prefix.as_ref()?;
let [ast::CommandPrefixOrSuffixItem::IoRedirect(redir)] = prefix.0.as_slice() else {
return None;
};
// ...that is a file input redirection to a filename, targeting stdin.
match redir {
ast::IoRedirect::File(
fd,
ast::IoFileRedirectKind::Read,
ast::IoFileRedirectTarget::Filename(..),
) if fd.is_none_or(|fd| fd == openfiles::OpenFiles::STDIN_FD) => Some(redir),
_ => None,
}
}
File diff suppressed because it is too large Load Diff
+676
View File
@@ -0,0 +1,676 @@
//! Implements a shell variable environment.
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::hash_map;
use crate::Shell;
use crate::error;
use crate::extensions;
use crate::variables::{self, ShellValue, ShellValueUnsetType, ShellVariable};
/// Represents the policy for looking up variables in a shell environment.
#[derive(Clone, Copy)]
pub enum EnvironmentLookup {
/// Look anywhere.
Anywhere,
/// Look only in the global scope.
OnlyInGlobal,
/// Look only in the current local scope.
OnlyInCurrentLocal,
/// Look only in local scopes.
OnlyInLocal,
}
/// Represents a shell environment scope.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EnvironmentScope {
/// Scope local to a function instance
Local,
/// Globals
Global,
/// Transient overrides for a command invocation
Command,
}
impl std::fmt::Display for EnvironmentScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Local => write!(f, "local"),
Self::Global => write!(f, "global"),
Self::Command => write!(f, "command"),
}
}
}
/// A guard that pushes a scope onto a shell environment and pops it when dropped.
pub(crate) struct ScopeGuard<'a, SE: extensions::ShellExtensions> {
scope_type: EnvironmentScope,
shell: &'a mut crate::Shell<SE>,
detached: bool,
}
impl<'a, SE: extensions::ShellExtensions> ScopeGuard<'a, SE> {
/// Creates a new scope guard, pushing the given scope type onto the environment.
///
/// # Arguments
///
/// * `shell` - The shell whose environment to modify.
/// * `scope_type` - The type of scope to push.
pub fn new(shell: &'a mut crate::Shell<SE>, scope_type: EnvironmentScope) -> Self {
shell.env_mut().push_scope(scope_type);
Self {
scope_type,
shell,
detached: false,
}
}
/// Returns a mutable reference to the shell.
pub const fn shell(&mut self) -> &mut crate::Shell<SE> {
self.shell
}
/// Detaches the guard, preventing it from popping the scope on drop.
pub const fn detach(&mut self) {
self.detached = true;
}
}
impl<SE: extensions::ShellExtensions> Drop for ScopeGuard<'_, SE> {
fn drop(&mut self) {
if !self.detached {
let _ = self.shell.env_mut().pop_scope(self.scope_type);
}
}
}
/// Represents the shell variable environment, composed of a stack of scopes.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShellEnvironment {
/// Stack of scopes, with the top of the stack being the current scope.
scopes: Vec<(EnvironmentScope, ShellVariableMap)>,
/// Whether or not to auto-export variables on creation or modification.
export_variables_on_modification: bool,
/// Count of total entries (may include duplicates with shadowed variables).
entry_count: usize,
}
impl Default for ShellEnvironment {
fn default() -> Self {
Self::new()
}
}
impl ShellEnvironment {
/// Returns a new shell environment.
pub fn new() -> Self {
Self {
scopes: vec![(EnvironmentScope::Global, ShellVariableMap::default())],
export_variables_on_modification: false,
entry_count: 0,
}
}
/// Pushes a new scope of the given type onto the environment's scope stack.
///
/// # Arguments
///
/// * `scope_type` - The type of scope to push.
pub fn push_scope(&mut self, scope_type: EnvironmentScope) {
self.scopes.push((scope_type, ShellVariableMap::default()));
}
/// Pops the top-most scope off the environment's scope stack.
///
/// # Arguments
///
/// * `expected_scope_type` - The type of scope that is expected to be atop the stack.
pub fn pop_scope(&mut self, expected_scope_type: EnvironmentScope) -> Result<(), error::Error> {
// TODO(env): Should we panic instead on failure? It's effectively a broken invariant.
match self.scopes.pop() {
Some((actual_scope_type, _)) if actual_scope_type == expected_scope_type => Ok(()),
Some((actual_scope_type, _)) => Err(error::ErrorKind::UnexpectedScopeType {
expected: expected_scope_type,
actual: actual_scope_type,
}
.into()),
None => Err(error::ErrorKind::MissingScope.into()),
}
}
//
// Iterators/Getters
//
/// Returns an iterator over all exported variables defined in the variable.
pub fn iter_exported(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
// We won't actually need to store all entries, but we expect it should be
// within the same order.
let mut visible_vars: HashMap<&String, &ShellVariable> =
HashMap::with_capacity(self.entry_count);
for (_, var_map) in self.scopes.iter().rev() {
for (name, var) in var_map.iter().filter(|(_, v)| v.is_exported()) {
// Only insert the variable if it hasn't been seen yet.
if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
entry.insert(var);
}
}
}
visible_vars.into_iter()
}
/// Returns an iterator over all the variables defined in the environment.
pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
self.iter_using_policy(EnvironmentLookup::Anywhere)
}
/// Returns an iterator over all the variables defined in the environment,
/// using the given lookup policy.
///
/// # Arguments
///
/// * `lookup_policy` - The policy to use when looking up variables.
pub fn iter_using_policy(
&self,
lookup_policy: EnvironmentLookup,
) -> impl Iterator<Item = (&String, &ShellVariable)> {
// We won't actually need to store all entries, but we expect it should be
// within the same order.
let mut visible_vars: HashMap<&String, &ShellVariable> =
HashMap::with_capacity(self.entry_count);
let mut local_count = 0;
for (scope_type, var_map) in self.scopes.iter().rev() {
if matches!(scope_type, EnvironmentScope::Local) {
local_count += 1;
}
match lookup_policy {
EnvironmentLookup::Anywhere => (),
EnvironmentLookup::OnlyInGlobal => {
if !matches!(scope_type, EnvironmentScope::Global) {
continue;
}
}
EnvironmentLookup::OnlyInCurrentLocal => {
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
continue;
}
}
EnvironmentLookup::OnlyInLocal => {
if !matches!(scope_type, EnvironmentScope::Local) {
continue;
}
}
}
for (name, var) in var_map.iter() {
// Only insert the variable if it hasn't been seen yet.
if let hash_map::Entry::Vacant(entry) = visible_vars.entry(name) {
entry.insert(var);
}
}
if matches!(scope_type, EnvironmentScope::Local)
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
{
break;
}
}
visible_vars.into_iter()
}
/// Tries to retrieve an immutable reference to the variable with the given name
/// in the environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn get<S: AsRef<str>>(&self, name: S) -> Option<(EnvironmentScope, &ShellVariable)> {
// Look through scopes, from the top of the stack on down.
for (scope_type, map) in self.scopes.iter().rev() {
if let Some(var) = map.get(name.as_ref()) {
return Some((*scope_type, var));
}
}
None
}
/// Tries to retrieve a mutable reference to the variable with the given name
/// in the environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn get_mut<S: AsRef<str>>(
&mut self,
name: S,
) -> Option<(EnvironmentScope, &mut ShellVariable)> {
// Look through scopes, from the top of the stack on down.
for (scope_type, map) in self.scopes.iter_mut().rev() {
if let Some(var) = map.get_mut(name.as_ref()) {
return Some((*scope_type, var));
}
}
None
}
/// Tries to retrieve the string value of the variable with the given name in the
/// environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
/// * `shell` - The shell owning the environment.
pub fn get_str<S: AsRef<str>, SE: extensions::ShellExtensions>(
&self,
name: S,
shell: &Shell<SE>,
) -> Option<Cow<'_, str>> {
self.get(name.as_ref())
.map(|(_, v)| v.value().to_cow_str(shell))
}
/// Checks if a variable of the given name is set in the environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to check.
pub fn is_set<S: AsRef<str>>(&self, name: S) -> bool {
if let Some((_, var)) = self.get(name) {
!matches!(var.value(), ShellValue::Unset(_))
} else {
false
}
}
//
// Setters
//
/// Tries to unset the variable with the given name in the environment, returning
/// whether or not such a variable existed.
///
/// # Arguments
///
/// * `name` - The name of the variable to unset.
pub fn unset(&mut self, name: &str) -> Result<Option<ShellVariable>, error::Error> {
let mut local_count = 0;
for (scope_type, map) in self.scopes.iter_mut().rev() {
if matches!(scope_type, EnvironmentScope::Local) {
local_count += 1;
}
let unset_result = Self::try_unset_in_map(map, name)?;
if unset_result.is_some() {
// If we end up finding a local in the top-most local frame, then we replace
// it with a placeholder.
if matches!(scope_type, EnvironmentScope::Local) && local_count == 1 {
map.set(
name,
ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped)),
);
} else if self.entry_count > 0 {
// Entry count should never be 0 here, but we're being defensive.
self.entry_count -= 1;
}
return Ok(unset_result);
}
}
Ok(None)
}
/// Tries to unset an array element from the environment, using the given name and
/// element index for lookup. Returns whether or not an element was unset.
///
/// # Arguments
///
/// * `name` - The name of the array variable to unset an element from.
/// * `index` - The index of the element to unset.
pub fn unset_index(&mut self, name: &str, index: &str) -> Result<bool, error::Error> {
if let Some((_, var)) = self.get_mut(name) {
var.unset_index(index)
} else {
Ok(false)
}
}
fn try_unset_in_map(
map: &mut ShellVariableMap,
name: &str,
) -> Result<Option<ShellVariable>, error::Error> {
match map.get(name).map(|v| v.is_readonly()) {
Some(true) => Err(error::ErrorKind::ReadonlyVariable.into()),
Some(false) => Ok(map.unset(name)),
None => Ok(None),
}
}
/// Tries to retrieve an immutable reference to a variable from the environment,
/// using the given name and lookup policy.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
/// * `lookup_policy` - The policy to use when looking up the variable.
pub fn get_using_policy<N: AsRef<str>>(
&self,
name: N,
lookup_policy: EnvironmentLookup,
) -> Option<&ShellVariable> {
let mut local_count = 0;
for (scope_type, var_map) in self.scopes.iter().rev() {
if matches!(scope_type, EnvironmentScope::Local) {
local_count += 1;
}
match lookup_policy {
EnvironmentLookup::Anywhere => (),
EnvironmentLookup::OnlyInGlobal => {
if !matches!(scope_type, EnvironmentScope::Global) {
continue;
}
}
EnvironmentLookup::OnlyInCurrentLocal => {
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
continue;
}
}
EnvironmentLookup::OnlyInLocal => {
if !matches!(scope_type, EnvironmentScope::Local) {
continue;
}
}
}
if let Some(var) = var_map.get(name.as_ref()) {
return Some(var);
}
if matches!(scope_type, EnvironmentScope::Local)
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
{
break;
}
}
None
}
/// Tries to retrieve a mutable reference to a variable from the environment,
/// using the given name and lookup policy.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
/// * `lookup_policy` - The policy to use when looking up the variable.
pub fn get_mut_using_policy<N: AsRef<str>>(
&mut self,
name: N,
lookup_policy: EnvironmentLookup,
) -> Option<&mut ShellVariable> {
let mut local_count = 0;
for (scope_type, var_map) in self.scopes.iter_mut().rev() {
if matches!(scope_type, EnvironmentScope::Local) {
local_count += 1;
}
match lookup_policy {
EnvironmentLookup::Anywhere => (),
EnvironmentLookup::OnlyInGlobal => {
if !matches!(scope_type, EnvironmentScope::Global) {
continue;
}
}
EnvironmentLookup::OnlyInCurrentLocal => {
if !(matches!(scope_type, EnvironmentScope::Local) && local_count == 1) {
continue;
}
}
EnvironmentLookup::OnlyInLocal => {
if !matches!(scope_type, EnvironmentScope::Local) {
continue;
}
}
}
if let Some(var) = var_map.get_mut(name.as_ref()) {
return Some(var);
}
if matches!(scope_type, EnvironmentScope::Local)
&& matches!(lookup_policy, EnvironmentLookup::OnlyInCurrentLocal)
{
break;
}
}
None
}
/// Update a variable in the environment, or add it if it doesn't already exist.
///
/// # Arguments
///
/// * `name` - The name of the variable to update or add.
/// * `value` - The value to assign to the variable.
/// * `updater` - A function to call to update the variable after assigning the value.
/// * `lookup_policy` - The policy to use when looking up the variable.
/// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist.
pub fn update_or_add<N: Into<String>>(
&mut self,
name: N,
value: variables::ShellValueLiteral,
updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
lookup_policy: EnvironmentLookup,
scope_if_creating: EnvironmentScope,
) -> Result<(), error::Error> {
let name = name.into();
let auto_export = self.export_variables_on_modification;
if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
var.assign(value, false)?;
if auto_export {
var.export();
}
updater(var)
} else {
let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
var.assign(value, false)?;
if auto_export {
var.export();
}
updater(&mut var)?;
self.add(name, var, scope_if_creating)
}
}
/// Update an array element in the environment, or add it if it doesn't already exist.
///
/// # Arguments
///
/// * `name` - The name of the variable to update or add.
/// * `index` - The index of the element to update or add.
/// * `value` - The value to assign to the variable.
/// * `updater` - A function to call to update the variable after assigning the value.
/// * `lookup_policy` - The policy to use when looking up the variable.
/// * `scope_if_creating` - The scope to create the variable in if it doesn't already exist.
pub fn update_or_add_array_element<N: Into<String>>(
&mut self,
name: N,
index: String,
value: String,
updater: impl Fn(&mut ShellVariable) -> Result<(), error::Error>,
lookup_policy: EnvironmentLookup,
scope_if_creating: EnvironmentScope,
) -> Result<(), error::Error> {
let name = name.into();
if let Some(var) = self.get_mut_using_policy(&name, lookup_policy) {
var.assign_at_index(index, value, false)?;
updater(var)
} else {
let mut var = ShellVariable::new(ShellValue::Unset(ShellValueUnsetType::Untyped));
var.assign(
variables::ShellValueLiteral::Array(variables::ArrayLiteral(vec![(
Some(index),
value,
)])),
false,
)?;
updater(&mut var)?;
self.add(name, var, scope_if_creating)
}
}
/// Adds a variable to the environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to add.
/// * `var` - The variable to add.
/// * `target_scope` - The scope to add the variable to.
pub fn add<N: Into<String>>(
&mut self,
name: N,
mut var: ShellVariable,
target_scope: EnvironmentScope,
) -> Result<(), error::Error> {
if self.export_variables_on_modification {
var.export();
}
for (scope_type, map) in self.scopes.iter_mut().rev() {
if *scope_type == target_scope {
let prev_var = map.set(name, var);
if prev_var.is_none() {
self.entry_count += 1;
}
return Ok(());
}
}
Err(error::ErrorKind::MissingScopeForNewVariable.into())
}
/// Sets a global variable in the environment.
///
/// # Arguments
///
/// * `name` - The name of the variable to set.
/// * `var` - The variable to set.
pub fn set_global<N: Into<String>>(
&mut self,
name: N,
var: ShellVariable,
) -> Result<(), error::Error> {
self.add(name, var, EnvironmentScope::Global)
}
}
/// Represents a map from names to shell variables.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShellVariableMap {
variables: HashMap<String, ShellVariable>,
}
impl ShellVariableMap {
//
// Iterators/Getters
//
/// Returns an iterator over all the variables in the map.
pub fn iter(&self) -> impl Iterator<Item = (&String, &ShellVariable)> {
self.variables.iter()
}
/// Tries to retrieve an immutable reference to the variable with the given name.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn get(&self, name: &str) -> Option<&ShellVariable> {
self.variables.get(name)
}
/// Tries to retrieve a mutable reference to the variable with the given name.
///
/// # Arguments
///
/// * `name` - The name of the variable to retrieve.
pub fn get_mut(&mut self, name: &str) -> Option<&mut ShellVariable> {
self.variables.get_mut(name)
}
//
// Setters
//
/// Tries to unset the variable with the given name, returning the removed
/// variable or None if it was not already set.
///
/// # Arguments
///
/// * `name` - The name of the variable to unset.
pub fn unset(&mut self, name: &str) -> Option<ShellVariable> {
self.variables.remove(name)
}
/// Sets a variable in the map.
///
/// # Arguments
///
/// * `name` - The name of the variable to set.
/// * `var` - The variable to set.
pub fn set<N: Into<String>>(&mut self, name: N, var: ShellVariable) -> Option<ShellVariable> {
self.variables.insert(name.into(), var)
}
}
/// Checks if the given name is a valid variable name.
pub fn valid_variable_name(s: &str) -> bool {
let mut cs = s.chars();
match cs.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {
cs.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
Some(_) | None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_variable_name() {
assert!(!valid_variable_name(""));
assert!(!valid_variable_name("1"));
assert!(!valid_variable_name(" a"));
assert!(!valid_variable_name(" "));
assert!(valid_variable_name("_"));
assert!(valid_variable_name("_a"));
assert!(valid_variable_name("_1"));
assert!(valid_variable_name("_a1"));
assert!(valid_variable_name("a"));
assert!(valid_variable_name("A"));
assert!(valid_variable_name("a1"));
assert!(valid_variable_name("A1"));
}
}
+482
View File
@@ -0,0 +1,482 @@
//! Error facilities
use std::path::PathBuf;
use crate::{Shell, ShellFd, extensions, results, sys};
/// Unified error type for this crate. Contains just a kind for now,
/// but will be extended later with additional context.
#[derive(thiserror::Error, Debug)]
#[error("{kind}")]
pub struct Error {
/// The kind of error.
#[source]
kind: ErrorKind,
/// Whether or not the error should be considered a "fatal" error that would
/// result in abnormal exit of a non-interactive shell.
fatal: bool,
}
/// Monolithic error type for the shell
#[derive(thiserror::Error, Debug)]
pub enum ErrorKind {
/// A tilde expression was used without a valid HOME variable
#[error("cannot expand tilde expression with HOME not set")]
TildeWithoutValidHome,
/// An attempt was made to assign a list to an array member
#[error("cannot assign list to array member")]
AssigningListToArrayMember,
/// An attempt was made to convert an associative array to an indexed array.
#[error("cannot convert associative array to indexed array")]
ConvertingAssociativeArrayToIndexedArray,
/// An attempt was made to convert an indexed array to an associative array.
#[error("cannot convert indexed array to associative array")]
ConvertingIndexedArrayToAssociativeArray,
/// An error occurred while sourcing the indicated script file.
#[error("failed to source file: {0}")]
FailedSourcingFile(PathBuf, #[source] std::io::Error),
/// The shell failed to send a signal to a process.
#[error("failed to send signal to process")]
FailedToSendSignal,
/// An attempt was made to assign a value to a special parameter.
#[error("cannot assign in this way")]
CannotAssignToSpecialParameter,
/// Checked expansion error.
#[error("expansion error: {0}")]
CheckedExpansionError(String),
/// A reference was made to an unknown shell function.
#[error("function not found: {0}")]
FunctionNotFound(String),
/// Command was not found.
#[error("command not found: {0}")]
CommandNotFound(String),
/// Not a builtin.
#[error("not a shell builtin: {0}")]
BuiltinNotFound(String),
/// The working directory does not exist.
#[error("working directory does not exist: {0}")]
WorkingDirMissing(PathBuf),
/// Failed to execute command.
#[error("failed to execute command '{0}': {1}")]
FailedToExecuteCommand(String, #[source] std::io::Error),
/// History item was not found.
#[error("history item not found")]
HistoryItemNotFound,
/// The requested functionality has not yet been implemented in this shell.
#[error("not yet implemented: {0}")]
Unimplemented(&'static str),
/// The requested functionality has not yet been implemented in this shell; it is tracked in a
/// GitHub issue.
#[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
UnimplementedAndTracked(&'static str, u32),
/// An expected environment scope could not be found.
#[error("missing environment scope")]
MissingScope,
/// The environment scope required for a new variable is not available.
#[error("environment scope required for new variable is not available")]
MissingScopeForNewVariable,
/// An unexpected environment scope type was encountered.
#[error("unexpected environment scope type: expected '{expected}', found '{actual}'")]
UnexpectedScopeType {
/// The expected scope type.
expected: crate::env::EnvironmentScope,
/// The actual scope type.
actual: crate::env::EnvironmentScope,
},
/// The given path is not a directory.
#[error("not a directory: {0}")]
NotADirectory(PathBuf),
/// The given path is a directory.
#[error("path is a directory")]
IsADirectory,
/// The given variable is not an array.
#[error("variable is not an array")]
NotArray,
/// The current user could not be determined.
#[error("no current user")]
NoCurrentUser,
/// The requested input or output redirection is invalid.
#[error("invalid redirection target")]
InvalidRedirection,
/// An error occurred while redirecting input or output with the given file.
#[error("failed to redirect to {0}: {1}")]
RedirectionFailure(String, String),
/// An error occurred evaluating an arithmetic expression.
#[error("arithmetic evaluation error: {0}")]
EvalError(#[from] crate::arithmetic::EvalError),
/// The given string could not be parsed as an integer.
#[error("failed to parse '{s}' as a {int_type_name}, base-{radix} integer: {inner}")]
IntParseError {
/// The string that failed to parse.
s: String,
/// The integer type being parsed.
int_type_name: &'static str,
/// The radix (base) used for parsing.
radix: u32,
/// The underlying parse error.
inner: std::num::ParseIntError,
},
/// The given integer could not be converted to the target type.
#[error("integer conversion error")]
TryIntParseError(#[from] std::num::TryFromIntError),
/// A byte sequence could not be decoded as a valid UTF-8 string.
#[error("failed to decode utf-8")]
FromUtf8Error(#[from] std::string::FromUtf8Error),
/// A byte sequence could not be decoded as a valid UTF-8 string.
#[error("failed to decode utf-8")]
Utf8Error(#[from] std::str::Utf8Error),
/// An attempt was made to modify a readonly variable.
#[error("cannot mutate readonly variable")]
ReadonlyVariable,
/// The indicated pattern is invalid.
#[error("invalid pattern: '{0}'")]
InvalidPattern(String),
/// A regular expression error occurred
#[error("regex error: {0}")]
RegexError(#[from] fancy_regex::Error),
/// An invalid regular expression was provided.
#[error("invalid regex: {0}; expression: '{1}'")]
InvalidRegexError(fancy_regex::Error, String),
/// An I/O error occurred.
#[error("i/o error: {0}")]
IoError(#[from] std::io::Error),
/// Invalid substitution syntax.
#[error("bad substitution: {0}")]
BadSubstitution(String),
/// An error occurred while creating a child process.
#[error("failed to create child process")]
ChildCreationFailure,
/// An error occurred while formatting a string.
#[error(transparent)]
FormattingError(#[from] std::fmt::Error),
/// An error occurred while parsing.
#[error("{1}: {0}")]
ParseError(crate::parser::ParseError, crate::SourceInfo),
/// An error occurred while parsing a function body.
#[error("{0}: {1}")]
FunctionParseError(String, crate::parser::ParseError),
/// An error occurred while parsing a word.
#[error(transparent)]
WordParseError(#[from] crate::parser::WordParseError),
/// Unable to parse a test command.
#[error("invalid test command")]
TestCommandParseError(#[from] crate::parser::TestCommandParseError),
/// Unable to parse a key binding specification.
#[error(transparent)]
BindingParseError(#[from] crate::parser::BindingParseError),
/// A threading error occurred.
#[error("threading error")]
ThreadingError(#[from] tokio::task::JoinError),
/// An invalid signal was referenced.
#[error("{0}: invalid signal specification")]
InvalidSignal(String),
/// A platform error occurred.
#[error("platform error: {0}")]
PlatformError(#[from] sys::PlatformError),
/// An invalid umask was provided.
#[error("invalid umask value")]
InvalidUmask,
/// The given open file cannot be read from.
#[error("cannot read from {0}")]
OpenFileNotReadable(&'static str),
/// The given open file cannot be written to.
#[error("cannot write to {0}")]
OpenFileNotWritable(&'static str),
/// Bad file descriptor.
#[error("bad file descriptor: {0}")]
BadFileDescriptor(ShellFd),
/// Printf failure
#[error("printf failure: {0}")]
PrintfFailure(i32),
/// Printf invalid usage
#[error("printf: {0}")]
PrintfInvalidUsage(String),
/// Interrupted
#[error("interrupted")]
Interrupted,
/// Maximum function call depth was exceeded.
#[error("maximum function call depth exceeded")]
MaxFunctionCallDepthExceeded,
/// System time error.
#[error("system time error: {0}")]
TimeError(#[from] std::time::SystemTimeError),
/// Array index out of range.
#[error("array index out of range: {0}")]
ArrayIndexOutOfRange(String),
/// Unhandled key code.
#[error("unhandled key code: {0:?}")]
UnhandledKeyCode(Vec<u8>),
/// An error occurred in a built-in command.
#[error("{1}: {0}")]
BuiltinError(Box<dyn BuiltinError>, String),
/// Operation not supported on this platform.
#[error("operation not supported on this platform: {0}")]
NotSupportedOnThisPlatform(&'static str),
/// Command history is not enabled in this shell.
#[error("command history is not enabled in this shell")]
HistoryNotEnabled,
/// Expanding an unset variable.
#[error("expanding unset variable: {0}")]
ExpandingUnsetVariable(String),
/// An internal error occurred.
#[error("internal shell error: {0}")]
InternalError(String),
/// Attempted to perform an operation that requires an interactive session.
#[error("operation requires an interactive session")]
NotInInteractiveSession,
/// Attempted to perform an operation that requires command-string mode.
#[error("operation requires command-string mode")]
NotExecutingCommandString,
/// Too much data was provided to an operation.
#[error("too much data")]
TooMuchData,
/// Cannot convert open file to native file descriptor.
#[error("cannot convert open file to native file descriptor")]
CannotConvertToNativeFd,
/// History file is too large to import.
#[error("history file is too large to import")]
HistoryFileTooLargeToImport,
/// Too many open files.
#[error("too many open files")]
TooManyOpenFiles,
/// The function name shadows a special built-in command.
#[error("function name '{}' shadows a special built-in command", .name)]
FunctionNameShadowsSpecialBuiltin {
/// Name of the function.
name: String,
},
/// A glob pattern failed to match any files (failglob).
#[error("no match: {0}")]
NoMatch(String),
}
/// Trait implementable by built-in commands to represent errors.
pub trait BuiltinError: std::error::Error + ConvertibleToExitCode + Send + Sync {
/// Try to extract a reference to the underlying `std::io::Error`, if any.
/// Implementations should return `None` if there is no inner I/O error.
/// They should not attempt to *synthesize* an I/O error if one does not
/// naturally exist.
fn as_io_error(&self) -> Option<&std::io::Error> {
None
}
}
impl BuiltinError for Error {
fn as_io_error(&self) -> Option<&std::io::Error> {
self.as_io_error()
}
}
/// Helper trait for converting values to exit codes.
pub trait ConvertibleToExitCode {
/// Converts to an exit code.
fn as_exit_code(&self) -> results::ExecutionExitCode;
}
impl<T> ConvertibleToExitCode for T
where
results::ExecutionExitCode: for<'a> From<&'a T>,
{
fn as_exit_code(&self) -> results::ExecutionExitCode {
self.into()
}
}
impl From<&ErrorKind> for results::ExecutionExitCode {
fn from(value: &ErrorKind) -> Self {
match value {
ErrorKind::CommandNotFound(..) => Self::NotFound,
ErrorKind::Unimplemented(..) | ErrorKind::UnimplementedAndTracked(..) => {
Self::Unimplemented
}
ErrorKind::ParseError(..) => Self::InvalidUsage,
ErrorKind::FunctionParseError(..) => Self::InvalidUsage,
ErrorKind::TestCommandParseError(..) => Self::InvalidUsage,
ErrorKind::FailedToExecuteCommand(..) => Self::CannotExecute,
ErrorKind::FunctionNameShadowsSpecialBuiltin { .. } => Self::InvalidUsage,
ErrorKind::IoError(io_err) => io_err.into(),
ErrorKind::BuiltinError(inner, ..) => inner.as_exit_code(),
_ => Self::GeneralError,
}
}
}
impl From<&std::io::Error> for results::ExecutionExitCode {
fn from(io_err: &std::io::Error) -> Self {
if io_err.kind() == std::io::ErrorKind::BrokenPipe {
Self::BrokenPipe
} else {
Self::GeneralError
}
}
}
impl From<&Error> for results::ExecutionExitCode {
fn from(error: &Error) -> Self {
Self::from(&error.kind)
}
}
impl<T> From<T> for Error
where
ErrorKind: From<T>,
{
fn from(convertible_to_kind: T) -> Self {
Self {
kind: convertible_to_kind.into(),
fatal: false,
}
}
}
impl Error {
/// Marks this error as fatal.
#[must_use]
pub const fn into_fatal(mut self) -> Self {
self.fatal = true;
self
}
/// Returns whether or not this error is fatal.
pub const fn is_fatal(&self) -> bool {
self.fatal
}
/// Returns a reference to the error kind.
pub const fn kind(&self) -> &ErrorKind {
&self.kind
}
/// Try to extract a reference to the underlying `std::io::Error`, if any.
pub fn as_io_error(&self) -> Option<&std::io::Error> {
match &self.kind {
ErrorKind::IoError(io_err) => Some(io_err),
ErrorKind::BuiltinError(inner, _) => inner.as_io_error(),
_ => None,
}
}
/// Converts this error into the appropriate control flow based on the shell's current state.
/// This centralizes the logic for determining how fatal errors should affect execution flow.
///
/// # Arguments
///
/// * `shell` - The shell instance, used to check interactive mode and script call stack.
pub fn to_control_flow(
&self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> results::ExecutionControlFlow {
if self.is_fatal() && !shell.options().interactive {
results::ExecutionControlFlow::ExitShell
} else {
results::ExecutionControlFlow::Normal
}
}
/// Converts this error into an execution result for the shell.
///
/// # Arguments
///
/// * `shell` - The shell instance, used to determine control flow.
pub fn into_result(
self,
shell: &Shell<impl extensions::ShellExtensions>,
) -> results::ExecutionResult {
let next_control_flow = self.to_control_flow(shell);
let exit_code = results::ExecutionExitCode::from(&self);
results::ExecutionResult {
next_control_flow,
exit_code,
}
}
}
/// Convenience function for returning an error for unimplemented functionality.
///
/// # Arguments
///
/// * `msg` - The message to include in the error
pub fn unimp<T>(msg: &'static str) -> Result<T, Error> {
Err(ErrorKind::Unimplemented(msg).into())
}
/// Convenience function for returning an error for *tracked*, unimplemented functionality.
///
/// # Arguments
///
/// * `msg` - The message to include in the error
/// * `project_issue_id` - The GitHub issue ID where the implementation is tracked.
pub fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
Err(ErrorKind::UnimplementedAndTracked(msg, project_issue_id).into())
}
+536
View File
@@ -0,0 +1,536 @@
//! String escaping utilities
use std::borrow::Cow;
use itertools::Itertools;
use crate::{error, int_utils};
/// Escape expansion mode.
#[derive(Clone, Copy)]
pub enum EscapeExpansionMode {
/// echo builtin mode.
EchoBuiltin,
/// ANSI-C quotes.
AnsiCQuotes,
}
/// Expands backslash escapes in the provided string.
///
/// # Arguments
///
/// * `s` - The string to expand.
/// * `mode` - The mode to use for expansion.
#[expect(clippy::too_many_lines)]
pub fn expand_backslash_escapes(
s: &str,
mode: EscapeExpansionMode,
) -> Result<(Vec<u8>, bool), error::Error> {
let mut result: Vec<u8> = Vec::with_capacity(s.len());
let mut it = s.chars();
while let Some(c) = it.next() {
if c != '\\' {
// Not a backslash, add and move on.
result.append(c.to_string().into_bytes().as_mut());
continue;
}
let Some(escape_cmd) = it.next() else {
// Trailing backslash.
result.push(b'\\');
continue;
};
match escape_cmd {
'a' => result.push(b'\x07'),
'b' => result.push(b'\x08'),
'c' => {
match mode {
EscapeExpansionMode::EchoBuiltin => {
// Stop all additional output!
return Ok((result, false));
}
EscapeExpansionMode::AnsiCQuotes => {
if let Some(char_value) = it.next() {
// Special case backslash. If it's immediately followed by another
// backslash, then we consume both; if not, we still will use the
// backslash character as the one to apply the control transformation
// to.
if char_value == '\\' {
let orig_it = it.clone();
if !matches!(it.next(), Some('\\')) {
// Didn't find another backslash; restore iterator.
it = orig_it;
}
}
let mut bytes: Vec<u8> = if char_value.is_ascii_lowercase() {
char_value
.to_ascii_uppercase()
.to_string()
.bytes()
.collect()
} else {
char_value.to_string().bytes().collect()
};
if !bytes.is_empty() {
if bytes[0] == b'?' {
// We can't explain why this is the case, but it is.
bytes[0] = 0x7f;
} else {
bytes[0] &= 0x1f;
}
}
result.append(bytes.as_mut());
} else {
result.push(b'\\');
result.push(b'c');
}
}
}
}
'e' | 'E' => result.push(b'\x1b'),
'f' => result.push(b'\x0c'),
'n' => result.push(b'\n'),
'r' => result.push(b'\r'),
't' => result.push(b'\t'),
'v' => result.push(b'\x0b'),
'\\' => result.push(b'\\'),
'\'' if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'\''),
'\"' if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'\"'),
'?' if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => result.push(b'?'),
'0' => {
// Consume 0-3 valid octal chars
let mut taken_so_far = 0;
let mut octal_chars: String = it
.take_while_ref(|c| {
if taken_so_far < 3 && matches!(*c, '0'..='7') {
taken_so_far += 1;
true
} else {
false
}
})
.collect();
if octal_chars.is_empty() {
octal_chars.push('0');
}
let value = int_utils::parse::<u8>(octal_chars.as_str(), 8)?;
result.push(value);
}
'x' => {
// Consume 1-2 valid hex chars (or unlimited with braces in ANSI-C mode)
let mut hex_chars = String::new();
let mut invalid_prefix = false;
let mut hexits_consumed = 0;
let mut start_brace_consumed = false;
loop {
// Save the original in case we go too far and need to restore.
let orig_it = it.clone();
let Some(next_c) = it.next() else {
break;
};
if matches!(mode, EscapeExpansionMode::AnsiCQuotes)
&& !start_brace_consumed
&& next_c == '{'
{
start_brace_consumed = true;
} else if start_brace_consumed && next_c == '}' {
break;
} else if ((start_brace_consumed && !invalid_prefix)
|| (!start_brace_consumed && hexits_consumed < 2))
&& next_c.is_ascii_hexdigit()
{
hex_chars.push(next_c);
hexits_consumed += 1;
} else if start_brace_consumed && hexits_consumed == 0 {
invalid_prefix = true;
} else {
// Went too far; restore iterator and break.
it = orig_it;
break;
}
}
if hex_chars.is_empty() {
if start_brace_consumed {
result.push(0);
} else {
result.push(b'\\');
result.append(escape_cmd.to_string().into_bytes().as_mut());
}
} else {
let value32 = int_utils::parse::<u32>(hex_chars.as_str(), 16)?;
let value8: u8 = (value32 & 0xFF) as u8;
result.push(value8);
}
}
'u' => {
// Consume 1-4 hex digits
let mut taken_so_far = 0;
let hex_chars: String = it
.take_while_ref(|next_c| {
if taken_so_far < 4 && next_c.is_ascii_hexdigit() {
taken_so_far += 1;
true
} else {
false
}
})
.collect();
if hex_chars.is_empty() {
result.push(b'\\');
result.append(escape_cmd.to_string().into_bytes().as_mut());
} else {
let value = int_utils::parse::<u16>(hex_chars.as_str(), 16)?;
if let Some(decoded) = char::from_u32(u32::from(value)) {
result.append(decoded.to_string().into_bytes().as_mut());
} else {
result.push(b'\\');
result.append(escape_cmd.to_string().into_bytes().as_mut());
}
}
}
'U' => {
// Consume 1-8 hex digits
let mut taken_so_far = 0;
let hex_chars: String = it
.take_while_ref(|next_c| {
if taken_so_far < 8 && next_c.is_ascii_hexdigit() {
taken_so_far += 1;
true
} else {
false
}
})
.collect();
if hex_chars.is_empty() {
result.push(b'\\');
result.append(escape_cmd.to_string().into_bytes().as_mut());
} else {
let value = int_utils::parse::<u32>(hex_chars.as_str(), 16)?;
if let Some(decoded) = char::from_u32(value) {
result.append(decoded.to_string().into_bytes().as_mut());
} else {
result.push(b'\\');
result.append(escape_cmd.to_string().into_bytes().as_mut());
}
}
}
first_octal @ '1'..='7' if matches!(mode, EscapeExpansionMode::AnsiCQuotes) => {
// We've already consumed the first octal digit.
let mut octal_chars = String::new();
octal_chars.push(first_octal);
// Consume up to 2 more valid octal chars
let mut taken_so_far = 1;
for next_c in it.take_while_ref(|next_c| {
if taken_so_far < 3 && matches!(next_c, '0'..='7') {
taken_so_far += 1;
true
} else {
false
}
}) {
octal_chars.push(next_c);
}
let value = int_utils::parse::<u8>(octal_chars.as_str(), 8)?;
result.push(value);
}
unknown => {
// Not a valid escape sequence.
result.push(b'\\');
result.append(unknown.to_string().into_bytes().as_mut());
}
}
}
// In ANSI-C quotes, we crop the result at the first NUL.
if matches!(mode, EscapeExpansionMode::AnsiCQuotes) {
if let Some(nul_index) = result.iter().position(|&b| b == 0) {
result.truncate(nul_index);
}
}
Ok((result, true))
}
/// Quoting mode to use for escaping.
#[derive(Clone, Copy, Default)]
pub enum QuoteMode {
/// Single-quote.
#[default]
SingleQuote,
/// Double-quote.
DoubleQuote,
/// Backslash-escape.
BackslashEscape,
}
/// Options influencing how to escape/quote an input string.
#[derive(Default)]
pub(crate) struct QuoteOptions {
/// Whether or not to *always* escape or quote the input; if false, then escaping/quoting
/// will only be applied if the input contains characters that *require* it.
pub always_quote: bool,
/// Preferred mode for quoting/escaping. Quoting may be "upgraded" to a more expressive
/// format if the input is not expressible otherwise.
pub preferred_mode: QuoteMode,
/// Whether or not to *avoid* using ANSI C quoting just for the benefit of newline characters.
/// Default is for newline characters to require upgrading the string's quoting to
/// ANSI C quoting.
pub avoid_ansi_c_quoting_newline: bool,
}
pub(crate) fn quote<'a>(s: &'a str, options: &QuoteOptions) -> Cow<'a, str> {
let use_ansi_c_quotes = s.contains(|c| {
needs_ansi_c_quoting(c) && (!options.avoid_ansi_c_quoting_newline || c != '\n')
});
if use_ansi_c_quotes {
return ansi_c_quote(s).into();
}
let use_default_quotes =
!use_ansi_c_quotes && (options.always_quote || s.is_empty() || s.contains(needs_escaping));
if !use_default_quotes {
return s.into();
}
match options.preferred_mode {
QuoteMode::BackslashEscape => backslash_escape(s),
QuoteMode::SingleQuote => single_quote(s),
QuoteMode::DoubleQuote => double_quote(s).into(),
}
}
/// Escape the given string, forcing quoting.
///
/// # Arguments
///
/// * `s` - The string to escape.
/// * `mode` - The quoting mode to use.
pub fn force_quote(s: &str, mode: QuoteMode) -> String {
let options = QuoteOptions {
always_quote: true,
preferred_mode: mode,
..Default::default()
};
quote(s, &options).to_string()
}
/// Applies the given quoting mode to the provided string, only changing it if required.
///
/// # Arguments
///
/// * `s` - The string to escape.
/// * `mode` - The quoting mode to use.
pub fn quote_if_needed(s: &str, mode: QuoteMode) -> Cow<'_, str> {
let options = QuoteOptions {
always_quote: false,
preferred_mode: mode,
..Default::default()
};
quote(s, &options)
}
fn backslash_escape(s: &str) -> Cow<'_, str> {
if s.is_empty() {
// An empty string must be represented as '' to be a valid shell word.
Cow::Owned("''".to_string())
} else if !s.chars().any(needs_escaping) {
Cow::Borrowed(s)
} else {
let mut output = String::with_capacity(s.len());
for c in s.chars() {
if needs_escaping(c) {
output.push('\\');
}
output.push(c);
}
Cow::Owned(output)
}
}
fn single_quote(s: &str) -> Cow<'_, str> {
// Special-case the empty string.
if s.is_empty() {
return Cow::Borrowed("''");
}
let mut result = String::with_capacity(s.len());
// Go through the string; put everything in single quotes except for
// the single quote character itself. It will get escaped outside
// all quoting.
let mut first = true;
for part in s.split('\'') {
if !first {
result.push('\\');
result.push('\'');
} else {
first = false;
}
if !part.is_empty() {
result.push('\'');
result.push_str(part);
result.push('\'');
}
}
Cow::Owned(result)
}
fn double_quote(s: &str) -> String {
let mut result = String::with_capacity(s.len());
result.push('"');
for c in s.chars() {
if matches!(c, '$' | '`' | '"' | '\\') {
result.push('\\');
}
result.push(c);
}
result.push('"');
result
}
fn ansi_c_quote(s: &str) -> String {
let mut result = String::with_capacity(s.len());
result.push_str("$'");
for c in s.chars() {
match c {
'\x07' => result.push_str("\\a"),
'\x08' => result.push_str("\\b"),
'\x1b' => result.push_str("\\E"),
'\x0c' => result.push_str("\\f"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
'\x0b' => result.push_str("\\v"),
'\\' => result.push_str("\\\\"),
'\'' => result.push_str("\\'"),
c if needs_ansi_c_quoting(c) => {
result.push_str(std::format!("\\{:03o}", c as u8).as_str());
}
_ => result.push(c),
}
}
result.push('\'');
result
}
// Returns whether or not the given character needs to be escaped (or quoted) if outside
// quotes.
const fn needs_escaping(c: char) -> bool {
matches!(
c,
'(' | ')'
| '['
| ']'
| '{'
| '}'
| '$'
| '*'
| '?'
| '|'
| '&'
| ';'
| '<'
| '>'
| '`'
| '\\'
| '"'
| '!'
| '^'
| ','
| ' '
| '\''
)
}
const fn needs_ansi_c_quoting(c: char) -> bool {
c.is_ascii_control()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backslash_escape() {
assert_eq!(quote_if_needed("a", QuoteMode::BackslashEscape), "a");
assert_eq!(quote_if_needed("a b", QuoteMode::BackslashEscape), r"a\ b");
assert_eq!(quote_if_needed("", QuoteMode::BackslashEscape), "''");
}
#[test]
fn test_single_quote_escape() {
assert_eq!(quote_if_needed("a", QuoteMode::SingleQuote), "a");
assert_eq!(quote_if_needed("a b", QuoteMode::SingleQuote), "'a b'");
assert_eq!(quote_if_needed("", QuoteMode::SingleQuote), "''");
assert_eq!(quote_if_needed("'", QuoteMode::SingleQuote), "\\'");
}
fn assert_echo_expands_to(unexpanded: &str, expected: &str) {
assert_eq!(
String::from_utf8(
expand_backslash_escapes(unexpanded, EscapeExpansionMode::EchoBuiltin)
.unwrap()
.0
)
.unwrap(),
expected
);
}
#[test]
fn test_echo_expansion() {
assert_echo_expands_to("a", "a");
assert_echo_expands_to(r"\M", "\\M");
assert_echo_expands_to(r"a\nb", "a\nb");
assert_echo_expands_to(r"\a", "\x07");
assert_echo_expands_to(r"\b", "\x08");
assert_echo_expands_to(r"\e", "\x1b");
assert_echo_expands_to(r"\f", "\x0c");
assert_echo_expands_to(r"\n", "\n");
assert_echo_expands_to(r"\r", "\r");
assert_echo_expands_to(r"\t", "\t");
assert_echo_expands_to(r"\v", "\x0b");
assert_echo_expands_to(r"\\", "\\");
assert_echo_expands_to(r"\'", "\\'");
assert_echo_expands_to(r#"\""#, r#"\""#);
assert_echo_expands_to(r"\?", "\\?");
assert_echo_expands_to(r"\0", "\0");
assert_echo_expands_to(r"\00", "\0");
assert_echo_expands_to(r"\000", "\0");
assert_echo_expands_to(r"\081", "\081");
assert_echo_expands_to(r"\0101", "A");
assert_echo_expands_to(r"abc\", "abc\\");
assert_echo_expands_to(r"\x41", "A");
assert_echo_expands_to(r"\xf0\x9f\x90\x8d", "🐍");
assert_echo_expands_to(r"\u2620", "☠");
assert_echo_expands_to(r"\U0001f602", "😂");
}
}
File diff suppressed because it is too large Load Diff
+612
View File
@@ -0,0 +1,612 @@
use brush_parser::ast;
use std::path::Path;
use crate::{
ExecutionParameters, Shell, ShellFd, arithmetic, env, error, escape, expansion, extensions,
namedoptions, patterns,
sys::{
fs::{MetadataExt, PathExt},
users,
},
variables::{self, ArrayLiteral},
};
#[async_recursion::async_recursion]
pub(crate) async fn eval_extended_test_expr(
expr: &ast::ExtendedTestExpr,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
match expr {
ast::ExtendedTestExpr::UnaryTest(op, operand) => {
apply_unary_predicate(op, operand, shell, params).await
}
ast::ExtendedTestExpr::BinaryTest(op, left, right) => {
apply_binary_predicate(op, left, right, shell, params).await
}
ast::ExtendedTestExpr::And(left, right) => {
let result = eval_extended_test_expr(left, shell, params).await?
&& eval_extended_test_expr(right, shell, params).await?;
Ok(result)
}
ast::ExtendedTestExpr::Or(left, right) => {
let result = eval_extended_test_expr(left, shell, params).await?
|| eval_extended_test_expr(right, shell, params).await?;
Ok(result)
}
ast::ExtendedTestExpr::Not(expr) => {
let result = !eval_extended_test_expr(expr, shell, params).await?;
Ok(result)
}
ast::ExtendedTestExpr::Parenthesized(expr) => {
eval_extended_test_expr(expr, shell, params).await
}
}
}
async fn apply_unary_predicate(
op: &ast::UnaryPredicate,
operand: &ast::Word,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
let expanded_operand = expansion::basic_expand_word(shell, params, operand).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(
params,
std::format!(
"[[ {op} {} ]]",
escape::quote_if_needed(&expanded_operand, escape::QuoteMode::SingleQuote)
),
)
.await;
}
apply_unary_predicate_to_str(op, expanded_operand.as_str(), shell, params)
}
#[expect(clippy::too_many_lines)]
pub(crate) fn apply_unary_predicate_to_str(
op: &ast::UnaryPredicate,
operand: &str,
shell: &Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
match op {
ast::UnaryPredicate::StringHasNonZeroLength => Ok(!operand.is_empty()),
ast::UnaryPredicate::StringHasZeroLength => Ok(operand.is_empty()),
ast::UnaryPredicate::FileExists => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists())
}
ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_block_device())
}
ast::UnaryPredicate::FileExistsAndIsCharSpecialFile => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_char_device())
}
ast::UnaryPredicate::FileExistsAndIsDir => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.is_dir())
}
ast::UnaryPredicate::FileExistsAndIsRegularFile => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.is_file())
}
ast::UnaryPredicate::FileExistsAndIsSetgid => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_setgid())
}
ast::UnaryPredicate::FileExistsAndIsSymlink => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.is_symlink())
}
ast::UnaryPredicate::FileExistsAndHasStickyBit => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_sticky_bit())
}
ast::UnaryPredicate::FileExistsAndIsFifo => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_fifo())
}
ast::UnaryPredicate::FileExistsAndIsReadable => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.readable())
}
ast::UnaryPredicate::FileExistsAndIsNotZeroLength => {
let path = shell.absolute_path(Path::new(operand));
if let Ok(metadata) = path.metadata() {
Ok(metadata.len() > 0)
} else {
Ok(false)
}
}
ast::UnaryPredicate::FdIsOpenTerminal => {
// Trim whitespace before parsing, matching bash behavior.
if let Ok(fd) = operand.trim().parse::<ShellFd>() {
if let Some(open_file) = params.try_fd(shell, fd) {
Ok(open_file.is_terminal())
} else {
Ok(false)
}
} else {
Ok(false)
}
}
ast::UnaryPredicate::FileExistsAndIsSetuid => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_setuid())
}
ast::UnaryPredicate::FileExistsAndIsWritable => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.writable())
}
ast::UnaryPredicate::FileExistsAndIsExecutable => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.executable())
}
ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId => {
let path = shell.absolute_path(Path::new(operand));
if !path.exists() {
return Ok(false);
}
let md = path.metadata()?;
Ok(md.gid() == users::get_effective_gid()?)
}
ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead => {
error::unimp("unary extended test predicate: FileExistsAndModifiedSinceLastRead")
}
ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId => {
let path = shell.absolute_path(Path::new(operand));
if !path.exists() {
return Ok(false);
}
let md = path.metadata()?;
Ok(md.uid() == users::get_effective_uid()?)
}
ast::UnaryPredicate::FileExistsAndIsSocket => {
let path = shell.absolute_path(Path::new(operand));
Ok(path.exists_and_is_socket())
}
ast::UnaryPredicate::ShellOptionEnabled => {
let shopt_name = operand;
if let Some(option) =
namedoptions::options(namedoptions::ShellOptionKind::SetO).get(shopt_name)
{
Ok(option.get(shell.options()))
} else {
Ok(false)
}
}
ast::UnaryPredicate::ShellVariableIsSetAndAssigned => Ok(shell.env().is_set(operand)),
ast::UnaryPredicate::ShellVariableIsSetAndNameRef => match shell.env().get(operand) {
Some((_, reffed)) => Ok(reffed.value().is_set() && reffed.is_treated_as_nameref()),
None => Ok(false),
},
}
}
#[expect(clippy::too_many_lines)]
async fn apply_binary_predicate(
op: &ast::BinaryPredicate,
left: &ast::Word,
right: &ast::Word,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
match op {
ast::BinaryPredicate::StringMatchesRegex => {
let s = expansion::basic_expand_word(shell, params, left).await?;
let regex = expansion::basic_expand_regex(shell, params, right)
.await?
.set_multiline(true);
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {s} {op} {right} ]]"))
.await;
}
let (matches, captures) = match regex.matches(s.as_str()) {
Ok(Some(captures)) => (true, captures),
Ok(None) => (false, vec![]),
// If we can't compile the regex, don't abort the whole operation but make sure to
// report it.
// TODO(test): Docs indicate we should yield 2 on an invalid regex (not 1).
Err(e) => {
tracing::warn!("error using regex: {}", e);
(false, vec![])
}
};
let captures_value = variables::ShellValueLiteral::Array(ArrayLiteral(
captures
.into_iter()
.map(|c| (None, c.unwrap_or_default()))
.collect(),
));
shell.env_mut().update_or_add(
"BASH_REMATCH",
captures_value,
|_| Ok(()),
env::EnvironmentLookup::Anywhere,
env::EnvironmentScope::Global,
)?;
Ok(matches)
}
ast::BinaryPredicate::StringExactlyMatchesString => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left == right)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchString => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left != right)
}
ast::BinaryPredicate::StringContainsSubstring => {
let s = expansion::basic_expand_word(shell, params, left).await?;
let substring = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {s} {op} {substring} ]]"))
.await;
}
Ok(s.contains(substring.as_str()))
}
ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
files_refer_to_same_device_and_inode_numbers(shell, left, right)
}
ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
left_file_is_newer_or_exists_when_right_does_not(shell, left, right)
}
ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
left_file_is_older_or_does_not_exist_when_right_does(shell, left, right)
}
ast::BinaryPredicate::LeftSortsBeforeRight => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
// TODO(test): According to docs, should be lexicographical order of the current locale.
Ok(left < right)
}
ast::BinaryPredicate::LeftSortsAfterRight => {
let left = expansion::basic_expand_word(shell, params, left).await?;
let right = expansion::basic_expand_word(shell, params, right).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
// TODO(test): According to docs, should be lexicographical order of the current locale.
Ok(left > right)
}
ast::BinaryPredicate::ArithmeticEqualTo => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left == right)
}
ast::BinaryPredicate::ArithmeticNotEqualTo => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left != right)
}
ast::BinaryPredicate::ArithmeticLessThan => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left < right)
}
ast::BinaryPredicate::ArithmeticLessThanOrEqualTo => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left <= right)
}
ast::BinaryPredicate::ArithmeticGreaterThan => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left > right)
}
ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo => {
let left =
arithmetic::expand_and_eval(shell, params, left.value.as_str(), false).await?;
let right =
arithmetic::expand_and_eval(shell, params, right.value.as_str(), false).await?;
if shell.options().print_commands_and_arguments {
shell
.trace_command(params, std::format!("[[ {left} {op} {right} ]]"))
.await;
}
Ok(left >= right)
}
// N.B. The "=", "==", and "!=" operators don't compare 2 strings; they check
// for whether the lefthand operand (a string) is matched by the righthand
// operand (treated as a shell pattern).
// TODO(test): implement case-insensitive matching if relevant via shopt options
// (nocasematch).
ast::BinaryPredicate::StringExactlyMatchesPattern => {
let s = expansion::basic_expand_word(shell, params, left).await?;
let pattern = expansion::basic_expand_pattern(shell, params, right)
.await?
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_conditionals);
if shell.options().print_commands_and_arguments {
let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
let escaped_right = escape::quote_if_needed(
expanded_right.as_str(),
escape::QuoteMode::BackslashEscape,
);
shell
.trace_command(params, std::format!("[[ {s} {op} {escaped_right} ]]"))
.await;
}
pattern.exactly_matches(s.as_str())
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
let s = expansion::basic_expand_word(shell, params, left).await?;
let pattern = expansion::basic_expand_pattern(shell, params, right)
.await?
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_conditionals);
if shell.options().print_commands_and_arguments {
let expanded_right = expansion::basic_expand_word(shell, params, right).await?;
let escaped_right = escape::quote_if_needed(
expanded_right.as_str(),
escape::QuoteMode::BackslashEscape,
);
shell
.trace_command(params, std::format!("[[ {s} {op} {escaped_right} ]]"))
.await;
}
let eq = pattern.exactly_matches(s.as_str())?;
Ok(!eq)
}
}
}
pub(crate) fn apply_binary_predicate_to_strs(
op: &ast::BinaryPredicate,
left: &str,
right: &str,
shell: &Shell<impl extensions::ShellExtensions>,
) -> Result<bool, error::Error> {
match op {
ast::BinaryPredicate::FilesReferToSameDeviceAndInodeNumbers => {
files_refer_to_same_device_and_inode_numbers(shell, left, right)
}
ast::BinaryPredicate::LeftFileIsNewerOrExistsWhenRightDoesNot => {
left_file_is_newer_or_exists_when_right_does_not(shell, left, right)
}
ast::BinaryPredicate::LeftFileIsOlderOrDoesNotExistWhenRightDoes => {
left_file_is_older_or_does_not_exist_when_right_does(shell, left, right)
}
ast::BinaryPredicate::LeftSortsBeforeRight => {
// TODO(test): According to docs, should be lexicographical order of the current locale.
Ok(left < right)
}
ast::BinaryPredicate::LeftSortsAfterRight => {
// TODO(test): According to docs, should be lexicographical order of the current locale.
Ok(left > right)
}
ast::BinaryPredicate::ArithmeticEqualTo => Ok(apply_test_binary_arithmetic_predicate(
left,
right,
|left, right| left == right,
)),
ast::BinaryPredicate::ArithmeticNotEqualTo => Ok(apply_test_binary_arithmetic_predicate(
left,
right,
|left, right| left != right,
)),
ast::BinaryPredicate::ArithmeticLessThan => Ok(apply_test_binary_arithmetic_predicate(
left,
right,
|left, right| left < right,
)),
ast::BinaryPredicate::ArithmeticLessThanOrEqualTo => Ok(
apply_test_binary_arithmetic_predicate(left, right, |left, right| left <= right),
),
ast::BinaryPredicate::ArithmeticGreaterThan => Ok(apply_test_binary_arithmetic_predicate(
left,
right,
|left, right| left > right,
)),
ast::BinaryPredicate::ArithmeticGreaterThanOrEqualTo => Ok(
apply_test_binary_arithmetic_predicate(left, right, |left, right| left >= right),
),
ast::BinaryPredicate::StringExactlyMatchesPattern => {
let pattern = patterns::Pattern::from(right)
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_conditionals);
pattern.exactly_matches(left)
}
ast::BinaryPredicate::StringDoesNotExactlyMatchPattern => {
let pattern = patterns::Pattern::from(right)
.set_extended_globbing(shell.options().extended_globbing)
.set_case_insensitive(shell.options().case_insensitive_conditionals);
let eq = pattern.exactly_matches(left)?;
Ok(!eq)
}
ast::BinaryPredicate::StringExactlyMatchesString => Ok(left == right),
ast::BinaryPredicate::StringDoesNotExactlyMatchString => Ok(left != right),
_ => error::unimp("unsupported test binary predicate"),
}
}
fn apply_test_binary_arithmetic_predicate(
left: &str,
right: &str,
op: fn(i64, i64) -> bool,
) -> bool {
// We trim leading/trailing whitespace (including newlines) before parsing integers.
let left: Result<i64, _> = left.trim().parse();
let right: Result<i64, _> = right.trim().parse();
if let (Ok(left), Ok(right)) = (left, right) {
op(left, right)
} else {
false
}
}
fn left_file_is_older_or_does_not_exist_when_right_does(
shell: &Shell<impl extensions::ShellExtensions>,
left: impl AsRef<str>,
right: impl AsRef<str>,
) -> Result<bool, error::Error> {
let (l_path, r_path) = (
shell.absolute_path(Path::new(left.as_ref())),
shell.absolute_path(Path::new(right.as_ref())),
);
match (l_path.metadata(), r_path.metadata()) {
(Ok(m1), Ok(m2)) => Ok(m1.modified()? < m2.modified()?),
(Err(_), Ok(_)) => Ok(true),
_ => Ok(false),
}
}
fn left_file_is_newer_or_exists_when_right_does_not(
shell: &Shell<impl extensions::ShellExtensions>,
left: impl AsRef<str>,
right: impl AsRef<str>,
) -> Result<bool, error::Error> {
let (l_path, r_path) = (
shell.absolute_path(Path::new(left.as_ref())),
shell.absolute_path(Path::new(right.as_ref())),
);
match (l_path.metadata(), r_path.metadata()) {
(Ok(m1), Ok(m2)) => Ok(m1.modified()? > m2.modified()?),
(Ok(_), Err(_)) => Ok(true),
_ => Ok(false),
}
}
fn files_refer_to_same_device_and_inode_numbers(
shell: &Shell<impl extensions::ShellExtensions>,
left: impl AsRef<str>,
right: impl AsRef<str>,
) -> Result<bool, error::Error> {
let (l_path, r_path) = (
shell.absolute_path(Path::new(left.as_ref())),
shell.absolute_path(Path::new(right.as_ref())),
);
if !l_path.readable() || !r_path.readable() {
return Ok(false);
}
Ok(l_path.get_device_and_inode()? == r_path.get_device_and_inode()?)
}
+57
View File
@@ -0,0 +1,57 @@
//! Definition of shell behavior traits and defaults.
use crate::{Shell, error, extensions};
/// Trait for static shell extensions. Collects all associated types needed to
/// instantiate a shell into a single containing struct.
pub trait ShellExtensions: Clone + Default + Send + Sync + 'static {
/// Type of the error behavior implementation.
type ErrorFormatter: ErrorFormatter;
}
/// Shell extensions implementation constructed from component types.
#[derive(Clone, Default)]
pub struct ShellExtensionsImpl<EF: ErrorFormatter = DefaultErrorFormatter> {
_marker: std::marker::PhantomData<EF>,
}
impl<EF: ErrorFormatter> ShellExtensions for ShellExtensionsImpl<EF> {
type ErrorFormatter = EF;
}
/// Default shell extensions implementation.
/// This is a type alias for the most common shell configuration.
pub type DefaultShellExtensions = ShellExtensionsImpl<DefaultErrorFormatter>;
/// Trait for defining shell error behaviors.
pub trait ErrorFormatter: Clone + Default + Send + Sync + 'static {
/// Format the given error for display within the context of the provided shell.
///
/// # Arguments
///
/// * `error` - The error to format
/// * `shell` - The shell context in which the error occurred.
fn format_error(
&self,
error: &error::Error,
shell: &Shell<impl extensions::ShellExtensions>,
) -> String {
let _ = shell;
std::format!("error: {error:#}\n")
}
}
/// Default shell error behavior implementation.
#[derive(Clone, Default)]
pub struct DefaultErrorFormatter;
impl ErrorFormatter for DefaultErrorFormatter {}
/// Trait for placeholder behavior (stub for future extension).
pub trait PlaceholderBehavior: Clone + Default + Send + Sync + 'static {}
/// Default placeholder implementation.
#[derive(Clone, Default)]
pub struct DefaultPlaceholder;
impl PlaceholderBehavior for DefaultPlaceholder {}
+126
View File
@@ -0,0 +1,126 @@
//! Structures for managing function registrations and calls.
use std::{collections::HashMap, sync::Arc};
/// An environment for defined, named functions.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FunctionEnv {
functions: HashMap<String, Registration>,
}
impl FunctionEnv {
/// Tries to retrieve the registration for a function by name.
///
/// # Arguments
///
/// * `name` - The name of the function to retrieve.
pub fn get(&self, name: &str) -> Option<&Registration> {
self.functions.get(name)
}
/// Tries to retrieve a mutable reference to the registration for a
/// function by name.
///
/// # Arguments
///
/// * `name` - The name of the function to retrieve.
pub fn get_mut(&mut self, name: &str) -> Option<&mut Registration> {
self.functions.get_mut(name)
}
/// Unregisters a function from the environment.
///
/// # Arguments
///
/// * `name` - The name of the function to remove.
pub fn remove(&mut self, name: &str) -> Option<Registration> {
self.functions.remove(name)
}
/// Updates a function registration in this environment.
///
/// # Arguments
///
/// * `name` - The name of the function to update.
/// * `registration` - The new registration for the function.
pub fn update(&mut self, name: String, registration: Registration) {
self.functions.insert(name, registration);
}
/// Clear all functions in this environment.
pub fn clear(&mut self) {
self.functions.clear();
}
/// Returns an iterator over the functions registered in this environment.
pub fn iter(&self) -> impl Iterator<Item = (&String, &Registration)> {
self.functions.iter()
}
}
/// Encapsulates a registration for a defined function.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Registration {
/// The parsed definition of the function.
definition: Arc<brush_parser::ast::FunctionDefinition>,
/// The source info for the function definition.
source_info: crate::SourceInfo,
/// Whether or not this function definition should be exported to children.
exported: bool,
}
impl From<brush_parser::ast::FunctionDefinition> for Registration {
fn from(definition: brush_parser::ast::FunctionDefinition) -> Self {
Self {
definition: Arc::new(definition),
source_info: crate::SourceInfo::default(),
exported: false,
}
}
}
impl Registration {
/// Creates a new function registration.
///
/// # Arguments
///
/// * `definition` - The function definition.
/// * `source_info` - Source information for the function definition.
pub fn new(
definition: brush_parser::ast::FunctionDefinition,
source_info: &crate::SourceInfo,
) -> Self {
Self {
definition: Arc::new(definition),
source_info: source_info.clone(),
exported: false,
}
}
/// Returns a reference to the function definition.
pub fn definition(&self) -> &brush_parser::ast::FunctionDefinition {
&self.definition
}
/// Returns a reference to the source info for the function definition.
pub const fn source(&self) -> &crate::SourceInfo {
&self.source_info
}
/// Marks the function for export.
pub const fn export(&mut self) {
self.exported = true;
}
/// Unmarks the function for export.
pub const fn unexport(&mut self) {
self.exported = false;
}
/// Returns whether this function is exported.
pub const fn is_exported(&self) -> bool {
self.exported
}
}
+499
View File
@@ -0,0 +1,499 @@
//! Facilities for tracking and persisting the shell's command history.
use chrono::Utc;
use std::{
io::{BufRead, Read, Write},
path::Path,
};
use crate::error;
/// Represents a unique identifier for a history item.
type ItemId = i64;
/// Interface for querying and manipulating the shell's recorded history of commands.
// TODO(history): support maximum item count
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct History {
items: rpds::VectorSync<ItemId>,
id_map: rpds::HashTrieMapSync<ItemId, Item>,
next_id: ItemId,
}
impl History {
/// Constructs a new `History` instance, with its contents initialized from the given readable
/// stream. If errors are encountered reading lines from the stream, unreadable lines will
/// be skipped but the call will still return successfully, with a warning logged. An error
/// result will be returned only if an internal error occurs updating the history.
///
/// # Arguments
///
/// * `reader` - The readable stream to import history from.
pub fn import(reader: impl Read) -> Result<Self, error::Error> {
let mut history = Self::default();
let buf_reader = std::io::BufReader::new(reader);
let mut next_timestamp = None;
for line_result in buf_reader.lines() {
let line = match line_result {
Ok(line) => line,
// If we couldn't decode the line due to invalid data (perhaps it wasn't
// valid UTF8?), skip it and make a best-effort attempt to proceed on.
// We'll later warn the user.
Err(err) if err.kind() == std::io::ErrorKind::InvalidData => {
tracing::warn!("unreadable history line; {err}");
continue;
}
// In the event of other kinds of errors, return an error result. We don't
// want to get stuck in a failing I/O loop.
Err(err) => {
return Err(err.into());
}
};
// Look for timestamp comments; ignore other comment lines.
if let Some(comment) = line.strip_prefix("#") {
if let Ok(seconds_since_epoch) = comment.trim().parse() {
next_timestamp = ItemTimestamp::from_timestamp(seconds_since_epoch, 0);
} else {
next_timestamp = None;
}
continue;
}
let item = Item {
id: history.next_id,
command_line: line,
timestamp: next_timestamp.take(),
dirty: false,
};
history.add(item)?;
}
Ok(history)
}
/// Tries to retrieve a history item by its unique identifier. Returns `None` if no item is
/// found.
///
/// # Arguments
///
/// * `id` - The unique identifier of the history item to retrieve.
pub fn get_by_id(&self, id: ItemId) -> Result<Option<&Item>, error::Error> {
Ok(self.id_map.get(&id))
}
/// Replaces the history item with the given ID with a new item. Returns an error if the item
/// cannot be updated.
///
/// # Arguments
///
/// * `id` - The unique identifier of the history item to update.
/// * `item` - The new history item to replace the old one.
pub fn update_by_id(&mut self, id: ItemId, item: Item) -> Result<(), error::Error> {
let existing_item = self
.id_map
.get_mut(&id)
.ok_or(error::ErrorKind::HistoryItemNotFound)?;
*existing_item = item;
Ok(())
}
/// Removes the nth item from the history. Returns the removed item, or `None` if no such item
/// exists (i.e., because it was out of range).
pub fn remove_nth_item(&mut self, n: usize) -> bool {
if let Some(id) = self.items.get(n).copied() {
self.items = self
.items
.into_iter()
.enumerate()
.filter_map(|(i, id)| if i != n { Some(id) } else { None })
.copied()
.collect();
self.id_map.remove_mut(&id);
true
} else {
false
}
}
/// Adds a new history item. Returns the unique identifier of the newly added item.
///
/// # Arguments
///
/// * `item` - The history item to add.
pub fn add(&mut self, mut item: Item) -> Result<ItemId, error::Error> {
let id = self.next_id;
item.id = id;
self.next_id += 1;
self.items.push_back_mut(item.id);
self.id_map.insert_mut(item.id, item);
Ok(id)
}
/// Deletes a history item by its unique identifier. Returns an error if the item cannot be
/// deleted.
///
/// # Arguments
///
/// * `id` - The unique identifier of the history item to delete.
pub fn delete_item_by_id(&mut self, id: ItemId) -> Result<(), error::Error> {
self.id_map.remove_mut(&id);
self.items = self
.items
.into_iter()
.filter(|&item_id| *item_id != id)
.copied()
.collect();
Ok(())
}
/// Clears all history items.
pub fn clear(&mut self) -> Result<(), error::Error> {
self.id_map = rpds::HashTrieMapSync::new_sync();
self.items = rpds::VectorSync::new_sync();
Ok(())
}
/// Flushes the history to backing storage (if relevant).
///
/// # Arguments
///
/// * `history_file_path` - The path to the history file.
/// * `append` - Whether to append to the file or overwrite it.
/// * `unsaved_items_only` - Whether to only write unsaved items; if true, any items will be
/// marked as "saved" once saved.
/// * `write_timestamps` - Whether to write timestamps for each command line.
pub fn flush(
&mut self,
history_file_path: impl AsRef<Path>,
append: bool,
unsaved_items_only: bool,
write_timestamps: bool,
) -> Result<(), error::Error> {
// Open the file
let mut file_options = std::fs::File::options();
if append {
file_options.append(true);
} else {
file_options.write(true).truncate(true);
}
let mut file = file_options.create(true).open(history_file_path.as_ref())?;
for item_id in &self.items {
if let Some(item) = self.id_map.get_mut(item_id) {
if unsaved_items_only && !item.dirty {
continue;
}
if write_timestamps && let Some(timestamp) = item.timestamp {
writeln!(file, "#{}", timestamp.timestamp())?;
}
writeln!(file, "{}", item.command_line)?;
if unsaved_items_only {
item.dirty = false;
}
}
}
file.flush()?;
Ok(())
}
/// Searches through history using the given query.
///
/// # Arguments
///
/// * `query` - The query to use.
pub fn search(&self, query: Query) -> Result<impl Iterator<Item = &self::Item>, error::Error> {
Ok(Search::new(self, query))
}
/// Returns an iterator over the history items.
pub fn iter(&self) -> impl Iterator<Item = &self::Item> {
Search::all(self)
}
/// Retrieves the nth history item, if it exists. Returns `None` if no such item exists.
/// Indexing is zero-based, with an index of 0 referencing the oldest item in the history.
///
/// # Arguments
///
/// * `index` - The index of the history item to retrieve.
pub fn get(&self, index: usize) -> Option<&Item> {
if let Some(id) = self.items.get(index) {
self.id_map.get(id)
} else {
None
}
}
/// Returns the number of items in the history.
pub fn count(&self) -> usize {
self.items.len()
}
}
/// Represents a timestamp for a history item.
pub type ItemTimestamp = chrono::DateTime<Utc>;
/// Represents an item in the history.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Item {
/// The unique identifier of the history item.
pub id: ItemId,
/// The actual command line.
pub command_line: String,
/// The timestamp when the command was started.
pub timestamp: Option<ItemTimestamp>,
/// Whether or not the item is dirty, i.e., has not yet been written to backing storage.
pub dirty: bool,
}
impl Item {
/// Constructs a new `Item` with the given command line.
///
/// # Arguments
///
/// * `command_line` - The command line of the item.
pub fn new(command_line: impl Into<String>) -> Self {
Self {
id: 0, // NOTE: ID will be assigned when added to the history.
command_line: command_line.into(),
timestamp: Some(chrono::Utc::now()),
dirty: true,
}
}
}
/// Encapsulates query parameters for searching through history.
#[derive(Default)]
pub struct Query {
/// Whether to search forward or backward
pub direction: Direction,
/// Optionally, clamp results to items with a timestamp strictly after this.
pub not_at_or_before_time: Option<ItemTimestamp>,
/// Optionally, clamp results to items with a timestamp strictly before this.
pub not_at_or_after_time: Option<ItemTimestamp>,
/// Optionally, clamp results to items with an ID equal strictly after this.
pub not_at_or_before_id: Option<ItemId>,
/// Optionally, clamp results to items with an ID equal strictly before this.
pub not_at_or_after_id: Option<ItemId>,
/// Optionally, maximum number of items to retrieve
pub max_items: Option<i64>,
/// Optionally, a string-based filter on command line.
pub command_line_filter: Option<CommandLineFilter>,
}
impl Query {
/// Checks if the query includes the given item.
///
/// # Arguments
///
/// * `item` - The item to check.
pub fn includes(&self, item: &Item) -> bool {
// Filter based on not_at_or_before_time.
if let Some(not_at_or_before_time) = &self.not_at_or_before_time {
if item
.timestamp
.is_some_and(|ts| ts <= *not_at_or_before_time)
{
return false;
}
}
// Filter based on not_at_or_after_time
if let Some(not_at_or_after_time) = &self.not_at_or_after_time {
if item.timestamp.is_some_and(|ts| ts >= *not_at_or_after_time) {
return false;
}
}
// Filter based on not_at_or_before_id
if self
.not_at_or_before_id
.is_some_and(|query_id| item.id <= query_id)
{
return false;
}
// Filter based on not_at_or_after_id
if self
.not_at_or_after_id
.is_some_and(|query_id| item.id >= query_id)
{
return false;
}
// Filter based on command_line_filter
if let Some(command_line_filter) = &self.command_line_filter {
match command_line_filter {
CommandLineFilter::Prefix(prefix) => {
if !item.command_line.starts_with(prefix) {
return false;
}
}
CommandLineFilter::Suffix(suffix) => {
if !item.command_line.ends_with(suffix) {
return false;
}
}
CommandLineFilter::Contains(contains) => {
if !item.command_line.contains(contains) {
return false;
}
}
CommandLineFilter::Exact(exact) => {
if item.command_line != *exact {
return false;
}
}
}
}
true
}
}
/// Represents the direction of a search operation.
#[derive(Default)]
pub enum Direction {
/// Search forward from the oldest part of history.
#[default]
Forward,
/// Search backward from the youngest part of history.
Backward,
}
/// Filter criteria for command lines.
pub enum CommandLineFilter {
/// The command line must start with this string.
Prefix(String),
/// The command line must end with this string.
Suffix(String),
/// The command line must contain this string.
Contains(String),
/// The command line must match this string exactly.
Exact(String),
}
/// Represents a search operation.
pub struct Search<'a> {
/// The history to search through.
history: &'a History,
/// The query to apply.
query: Query,
/// The next index in `items`.
next_index: Option<usize>,
/// Count of items returned so far.
count: usize,
}
impl<'a> Search<'a> {
/// Constructs a new search against the provided history, querying *all* items.
///
/// # Arguments
///
/// * `history` - The history to search through.
pub fn all(history: &'a History) -> Self {
Self::new(history, Query::default())
}
/// Constructs a new search against the provided history, using the given query.
///
/// # Arguments
///
/// * `history` - The history to search through.
/// * `query` - The query to use.
pub fn new(history: &'a History, query: Query) -> Self {
let next_index = match query.direction {
Direction::Forward => Some(0),
Direction::Backward => {
if history.items.is_empty() {
None
} else {
Some(history.items.len() - 1)
}
}
};
Self {
history,
query,
next_index,
count: 0,
}
}
const fn increment_next_index(&mut self) {
if let Some(index) = self.next_index {
self.next_index = match self.query.direction {
Direction::Forward => Some(index + 1),
Direction::Backward => {
if index == 0 {
None
} else {
Some(index - 1)
}
}
}
}
}
}
impl<'a> Iterator for Search<'a> {
type Item = &'a Item;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(index) = self.next_index {
// Make sure we haven't hit the end of the history.
if index >= self.history.items.len() {
return None;
}
let id = self.history.items[index];
self.increment_next_index();
if let Some(item) = self.history.id_map.get(&id) {
// Filter based on max_items. Once we hit the limit,
// we stop searching.
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_sign_loss)]
if self
.query
.max_items
.is_some_and(|max_items| self.count >= max_items as usize)
{
return None;
}
// Check other filters. If they don't match, then we
// skip but keep searching.
if self.query.includes(item) {
self.count += 1;
return Some(item);
}
}
} else {
return None;
}
}
}
}
+67
View File
@@ -0,0 +1,67 @@
//! Generic utilities.
use crate::error;
/// Trait for integer types that support parsing from strings with a radix.
pub trait ParseIntRadix: Sized {
/// Parse a string as this integer type using the specified radix.
fn from_str_radix(s: &str, radix: u32) -> Result<Self, std::num::ParseIntError>;
/// Returns the name of the integer type as a static string.
fn type_name() -> &'static str;
}
macro_rules! impl_parse_int_radix {
($t:ty) => {
impl ParseIntRadix for $t {
fn from_str_radix(s: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
Self::from_str_radix(s, radix)
}
fn type_name() -> &'static str {
stringify!($t)
}
}
};
}
impl_parse_int_radix!(u8);
impl_parse_int_radix!(u16);
impl_parse_int_radix!(i32);
impl_parse_int_radix!(u32);
impl_parse_int_radix!(usize);
/// Parse the given string as an integer in the specified radix.
///
/// # Arguments
///
/// * `s` - The string to parse.
/// * `radix` - The base to use for parsing.
///
/// # Type Parameters
///
/// * `T` - The integer type to parse. Must implement `ParseIntRadix`.
///
/// # Examples
///
/// ```
/// use brush_core::int_utils::parse;
///
/// let result: u32 = parse("42", 10)?;
/// assert_eq!(result, 42);
///
/// let result: u8 = parse("FF", 16)?;
/// assert_eq!(result, 255);
/// # Ok::<(), brush_core::error::Error>(())
/// ```
pub fn parse<T: ParseIntRadix>(s: &str, radix: u32) -> Result<T, error::Error> {
T::from_str_radix(s, radix).map_err(|inner| {
error::ErrorKind::IntParseError {
s: s.to_owned(),
int_type_name: T::type_name(),
radix,
inner,
}
.into()
})
}
+5
View File
@@ -0,0 +1,5 @@
//! Exports traits for shell interfaces implemented by callers.
mod keybindings;
pub use keybindings::{InputFunction, Key, KeyAction, KeyBindings, KeySequence, KeyStroke};
+420
View File
@@ -0,0 +1,420 @@
use std::{
collections::HashMap,
fmt::{self, Display, Formatter},
};
/// Represents an action that can be taken in response to a key sequence.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum KeyAction {
/// Execute a shell command.
ShellCommand(String),
/// Execute an input "function".
DoInputFunction(InputFunction),
/// Execute a sequence of actions (in order).
Sequence(Vec<Self>),
}
impl Display for KeyAction {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::ShellCommand(command) => write!(f, "shell command: {command}"),
Self::DoInputFunction(function) => function.fmt(f),
Self::Sequence(actions) => {
write!(f, "sequence[")?;
for (i, action) in actions.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
action.fmt(f)?;
}
write!(f, "]")
}
}
}
}
/// Defines all input functions. Based on standard `readline` functions,
/// augmented with some `brush`-specific extensions.
#[derive(
Clone,
Debug,
Eq,
Hash,
PartialEq,
strum_macros::EnumString,
strum_macros::Display,
strum_macros::EnumIter,
strum_macros::IntoStaticStr,
)]
#[strum(serialize_all = "kebab-case")]
#[expect(missing_docs)]
pub enum InputFunction {
Abort,
AcceptLine,
AliasExpandLine,
ArrowKeyPrefix,
BackwardByte,
BackwardChar,
BackwardDeleteChar,
BackwardKillLine,
BackwardKillWord,
BackwardWord,
BashViComplete,
BeginningOfHistory,
BeginningOfLine,
BracketedPasteBegin,
BrushAcceptHint,
BrushAcceptHintWord,
CallLastKbdMacro,
CapitalizeWord,
CharacterSearch,
CharacterSearchBackward,
ClearDisplay,
ClearScreen,
Complete,
CompleteCommand,
CompleteFilename,
CompleteHostname,
CompleteIntoBraces,
CompleteUsername,
CompleteVariable,
CopyBackwardWord,
CopyForwardWord,
CopyRegionAsKill,
DabbrevExpand,
DeleteChar,
DeleteCharOrList,
DeleteHorizontalSpace,
DigitArgument,
DisplayShellVersion,
DoLowercaseVersion,
DowncaseWord,
DumpFunctions,
DumpMacros,
DumpVariables,
DynamicCompleteHistory,
EditAndExecuteCommand,
EmacsEditingMode,
EndKbdMacro,
EndOfHistory,
EndOfLine,
ExchangePointAndMark,
ExecuteNamedCommand,
ExportCompletions,
FetchHistory,
ForwardBackwardDeleteChar,
ForwardByte,
ForwardChar,
ForwardSearchHistory,
ForwardWord,
GlobCompleteWord,
GlobExpandWord,
GlobListExpansions,
HistoryAndAliasExpandLine,
HistoryExpandLine,
HistorySearchBackward,
HistorySearchForward,
HistorySubstringSearchBackward,
HistorySubstringSearchForward,
InsertComment,
InsertCompletions,
InsertLastArgument,
KillLine,
KillRegion,
KillWholeLine,
KillWord,
MagicSpace,
MenuComplete,
MenuCompleteBackward,
NextHistory,
NextScreenLine,
NonIncrementalForwardSearchHistory,
NonIncrementalForwardSearchHistoryAgain,
NonIncrementalReverseSearchHistory,
NonIncrementalReverseSearchHistoryAgain,
OldMenuComplete,
OperateAndGetNext,
OverwriteMode,
PossibleCommandCompletions,
PossibleCompletions,
PossibleFilenameCompletions,
PossibleHostnameCompletions,
PossibleUsernameCompletions,
PossibleVariableCompletions,
PreviousHistory,
PreviousScreenLine,
PrintLastKbdMacro,
QuotedInsert,
ReReadInitFile,
RedrawCurrentLine,
ReverseSearchHistory,
RevertLine,
SelfInsert,
SetMark,
ShellBackwardKillWord,
ShellBackwardWord,
ShellExpandLine,
ShellForwardWord,
ShellKillWord,
ShellTransposeWords,
SkipCsiSequence,
SpellCorrectWord,
StartKbdMacro,
TabInsert,
TildeExpand,
TransposeChars,
TransposeWords,
TtyStatus,
Undo,
UniversalArgument,
UnixFilenameRubout,
UnixLineDiscard,
UnixWordRubout,
UpcaseWord,
ViAppendEol,
ViAppendMode,
ViArgDigit,
#[strum(serialize = "vi-bWord")]
ViBWord,
ViBackToIndent,
ViBackwardBigword,
ViBackwardWord,
ViBword,
ViChangeCase,
ViChangeChar,
ViChangeTo,
ViCharSearch,
ViColumn,
ViComplete,
ViDelete,
ViDeleteTo,
#[strum(serialize = "vi-eWord")]
ViEWord,
ViEditAndExecuteCommand,
ViEditingMode,
ViEndBigword,
ViEndWord,
ViEofMaybe,
ViEword,
#[strum(serialize = "vi-fWord")]
ViFWord,
ViFetchHistory,
ViFirstPrint,
ViForwardBigword,
ViForwardWord,
ViFword,
ViGotoMark,
ViInsertBeg,
ViInsertionMode,
ViMatch,
ViMovementMode,
ViNextWord,
ViOverstrike,
ViOverstrikeDelete,
ViPrevWord,
ViPut,
ViRedo,
ViReplace,
ViRubout,
ViSearch,
ViSearchAgain,
ViSetMark,
ViSubst,
ViTildeExpand,
ViUndo,
ViUnixWordRubout,
ViYankArg,
ViYankPop,
ViYankTo,
Yank,
YankLastArg,
YankNthArg,
YankPop,
}
/// Represents a sequence of keys.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum KeySequence {
/// Strokes that make up the sequence.
Strokes(Vec<KeyStroke>),
/// Raw bytes that were used to generate this sequence.
Bytes(Vec<Vec<u8>>),
}
impl Display for KeySequence {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Strokes(strokes) => {
for stroke in strokes {
stroke.fmt(f)?;
}
}
Self::Bytes(bytes) => {
for byte in bytes.iter().flatten() {
if !byte.is_ascii_control() {
write!(f, "{}", *byte as char)?;
} else if *byte == b'\x1b' {
write!(f, r"\e")?;
} else if *byte >= 0x01 && *byte <= 0x1A {
// Control characters: display as \C-<letter>
let letter = (b'a' + (*byte - 1)) as char;
write!(f, r"\C-{letter}")?;
} else {
write!(f, r"\x{byte:02x}")?;
}
}
}
}
Ok(())
}
}
impl From<KeyStroke> for KeySequence {
/// Creates a new key sequence with a single stroke.
fn from(value: KeyStroke) -> Self {
Self::Strokes(vec![value])
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
/// Represents a single key press.
pub struct KeyStroke {
/// Alt key was pressed.
pub alt: bool,
/// Control key was pressed.
pub control: bool,
/// Shift key was pressed.
pub shift: bool,
/// Primary key pressed.
pub key: Key,
}
impl Display for KeyStroke {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.alt {
write!(f, "\\e")?;
}
if self.control {
write!(f, "\\C-")?;
}
if self.shift {
// TODO(input): Figure out what to do here or if the key encodes the shift in it.
}
self.key.fmt(f)
}
}
impl From<Key> for KeyStroke {
/// Creates a new key stroke with a single key.
fn from(value: Key) -> Self {
Self {
alt: false,
control: false,
shift: false,
key: value,
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
/// Represents a single key.
pub enum Key {
/// A simple character key.
Character(char),
/// Backspace key.
Backspace,
/// Enter key.
Enter,
/// Left arrow key.
Left,
/// Right arrow key.
Right,
/// Up arrow key.
Up,
/// Down arrow key.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page up key.
PageUp,
/// Page down key.
PageDown,
/// Tab key.
Tab,
/// Shift + Tab key.
BackTab,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// F key.
F(u8),
/// Escape key.
Escape,
}
impl Display for Key {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Character(c @ ('\\' | '\"' | '\'')) => write!(f, "\\{c}")?,
Self::Character(c) => write!(f, "{c}")?,
Self::Backspace => write!(f, "Backspace")?,
Self::Enter => write!(f, "Enter")?,
Self::Left => write!(f, "Left")?,
Self::Right => write!(f, "Right")?,
Self::Up => write!(f, "Up")?,
Self::Down => write!(f, "Down")?,
Self::Home => write!(f, "Home")?,
Self::End => write!(f, "End")?,
Self::PageUp => write!(f, "PageUp")?,
Self::PageDown => write!(f, "PageDown")?,
Self::Tab => write!(f, "Tab")?,
Self::BackTab => write!(f, "BackTab")?,
Self::Delete => write!(f, "Delete")?,
Self::Insert => write!(f, "Insert")?,
Self::F(n) => write!(f, "F{n}")?,
Self::Escape => write!(f, "Esc")?,
}
Ok(())
}
}
/// Encapsulates the shell's interaction with key bindings for input.
pub trait KeyBindings: Send {
/// Retrieves current bindings.
fn get_current(&self) -> HashMap<KeySequence, KeyAction>;
/// Tries to find a binding for an untranslated byte sequence.
fn get_untranslated(&self, bytes: &[u8]) -> Option<&KeyAction>;
/// Sets or updates a binding.
///
/// # Arguments
///
/// * `seq` - The key sequence to bind.
/// * `action` - The action to bind to the sequence.
fn bind(&mut self, seq: KeySequence, action: KeyAction) -> Result<(), std::io::Error>;
/// Unbinds a key sequence. Returns true if a binding was removed.
///
/// # Arguments
///
/// * `seq` - The key sequence to unbind.
fn try_unbind(&mut self, seq: KeySequence) -> bool;
/// Defines a macro that remaps a key sequence to another key sequence.
///
/// # Arguments
///
/// * `seq` - The key sequence to bind the macro to.
/// * `target` - The sequence that makes up the macro.
fn define_macro(&mut self, seq: KeySequence, target: KeySequence)
-> Result<(), std::io::Error>;
/// Retrieves all defined macros.
fn get_macros(&self) -> HashMap<KeySequence, KeySequence>;
}
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
//! Internal I/O utilities.
use crate::openfiles;
/// Error type for `FailingReaderWriter`.
#[derive(Clone, Debug, thiserror::Error)]
enum ReaderWriterError {
/// I/O read error.
#[error("I/O read error: {0}")]
Read(&'static str),
/// I/O write error.
#[error("I/O write error: {0}")]
Write(&'static str),
/// I/O flush error.
#[error("I/O flush error: {0}")]
Flush(&'static str),
}
/// An implementation of `std::io::Read` and `std::io::Write` that always fails.
///
/// Also implements `openfiles::Stream` for use with `openfiles::OpenFile`. Useful
/// for providing a valid stream that gracefully fails all operations in cases where
/// no real I/O is possible.
#[derive(Clone)]
pub(crate) struct FailingReaderWriter {
message: &'static str,
}
impl FailingReaderWriter {
/// Creates a new `FailingReaderWriter` with the given error message.
///
/// # Arguments
///
/// * `message` - The error message to use for all operations.
pub const fn new(message: &'static str) -> Self {
Self { message }
}
}
impl openfiles::Stream for FailingReaderWriter {
fn clone_box(&self) -> Box<dyn openfiles::Stream> {
Box::new(self.clone())
}
#[cfg(unix)]
fn try_clone_to_owned(&self) -> Result<std::os::fd::OwnedFd, crate::error::Error> {
Err(crate::error::ErrorKind::CannotConvertToNativeFd.into())
}
#[cfg(unix)]
fn try_borrow_as_fd(&self) -> Result<std::os::fd::BorrowedFd<'_>, crate::error::Error> {
Err(crate::error::ErrorKind::CannotConvertToNativeFd.into())
}
}
impl std::io::Read for FailingReaderWriter {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other(ReaderWriterError::Read(self.message)))
}
}
impl std::io::Write for FailingReaderWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other(ReaderWriterError::Write(
self.message,
)))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other(ReaderWriterError::Flush(
self.message,
)))
}
}
impl From<FailingReaderWriter> for openfiles::OpenFile {
fn from(frw: FailingReaderWriter) -> Self {
Self::Stream(Box::new(frw))
}
}
+458
View File
@@ -0,0 +1,458 @@
//! Job management
use std::collections::VecDeque;
use std::fmt::Display;
use futures::FutureExt;
use crate::ExecutionResult;
use crate::error;
use crate::processes;
use crate::sys;
use crate::trace_categories;
use crate::traps;
pub(crate) type JobJoinHandle = tokio::task::JoinHandle<Result<ExecutionResult, error::Error>>;
pub(crate) type JobResult = (Job, Result<ExecutionResult, error::Error>);
/// Manages the jobs that are currently managed by the shell.
#[derive(Default)]
pub struct JobManager {
/// The jobs that are currently managed by the shell.
pub jobs: Vec<Job>,
}
/// Represents a task that is part of a job.
pub enum JobTask {
/// An external process.
External(processes::ChildProcess),
/// An internal asynchronous task.
Internal(JobJoinHandle),
}
/// Represents the result of waiting on a job task.
pub enum JobTaskWaitResult {
/// The task has completed.
Completed(ExecutionResult),
/// The task was stopped.
Stopped,
}
impl JobTask {
/// Returns whether the task is an external process.
pub const fn is_external(&self) -> bool {
matches!(self, Self::External(_))
}
/// Waits for the task to complete. Returns the result of the wait.
pub async fn wait(&mut self) -> Result<JobTaskWaitResult, error::Error> {
match self {
Self::External(process) => {
let wait_result = process.wait().await?;
match wait_result {
processes::ProcessWaitResult::Completed(output) => {
Ok(JobTaskWaitResult::Completed(output.into()))
}
processes::ProcessWaitResult::Stopped => Ok(JobTaskWaitResult::Stopped),
}
}
Self::Internal(handle) => Ok(JobTaskWaitResult::Completed(handle.await??)),
}
}
/// Polls the task for completion. Returns `Some(result)` if the task has completed,
/// or `None` if it is still running. The result is the execution result of the task.
/// Behaves in a best-effort manner; if an internal error occurs during polling,
/// it will return `None`.
fn poll(&mut self) -> Option<Result<ExecutionResult, error::Error>> {
match self {
Self::External(process) => {
let check_result = process.poll();
check_result.map(|polled_result| polled_result.map(|output| output.into()))
}
Self::Internal(handle) => {
let checkable_handle = handle;
checkable_handle.now_or_never().and_then(|r| r.ok())
}
}
}
}
impl JobManager {
/// Returns a new job manager.
pub fn new() -> Self {
Self::default()
}
/// Adds a job to the job manager and marks it as the current job;
/// returns an immutable reference to the job.
///
/// # Arguments
///
/// * `job` - The job to add.
#[allow(
clippy::missing_panics_doc,
reason = "push() guarantees the vector length is >= 1"
)]
pub fn add_as_current(&mut self, mut job: Job) -> &Job {
for j in &mut self.jobs {
if matches!(j.annotation, JobAnnotation::Current) {
j.annotation = JobAnnotation::Previous;
break;
}
}
let id = self.jobs.len() + 1;
job.id = id;
job.annotation = JobAnnotation::Current;
self.jobs.push(job);
#[allow(clippy::unwrap_used, reason = "we just pushed an element")]
self.jobs.last().unwrap()
}
/// Returns the current job, if there is one.
pub fn current_job(&self) -> Option<&Job> {
self.jobs
.iter()
.find(|j| matches!(j.annotation, JobAnnotation::Current))
}
/// Returns a mutable reference to the current job, if there is one.
pub fn current_job_mut(&mut self) -> Option<&mut Job> {
self.jobs
.iter_mut()
.find(|j| matches!(j.annotation, JobAnnotation::Current))
}
/// Returns the previous job, if there is one.
pub fn prev_job(&self) -> Option<&Job> {
self.jobs
.iter()
.find(|j| matches!(j.annotation, JobAnnotation::Previous))
}
/// Returns a mutable reference to the previous job, if there is one.
pub fn prev_job_mut(&mut self) -> Option<&mut Job> {
self.jobs
.iter_mut()
.find(|j| matches!(j.annotation, JobAnnotation::Previous))
}
/// Tries to resolve the given job specification to a job.
///
/// # Arguments
///
/// * `job_spec` - The job specification to resolve.
pub fn resolve_job_spec(&mut self, job_spec: &str) -> Option<&mut Job> {
let remainder = job_spec.strip_prefix('%')?;
match remainder {
"%" | "+" => self.current_job_mut(),
"-" => self.prev_job_mut(),
s if s.chars().all(char::is_numeric) => {
let id = s.parse::<usize>().ok()?;
self.jobs.iter_mut().find(|j| j.id == id)
}
_ => {
tracing::warn!(target: trace_categories::UNIMPLEMENTED, "unimplemented: job spec naming command: '{job_spec}'");
None
}
}
}
/// Waits for all managed jobs to complete.
pub async fn wait_all(&mut self) -> Result<Vec<Job>, error::Error> {
for job in &mut self.jobs {
job.wait().await?;
}
Ok(self.sweep_completed_jobs())
}
/// Polls all managed jobs for completion.
pub fn poll(&mut self) -> Result<Vec<JobResult>, error::Error> {
let mut results = vec![];
let mut i = 0;
while i != self.jobs.len() {
if let Some(result) = self.jobs[i].poll_done()? {
let job = self.jobs.remove(i);
results.push((job, result));
} else if matches!(self.jobs[i].state, JobState::Done) {
// TODO(jobs): This is a workaround to remove jobs that are done but for which we
// don't know what happened.
results.push((self.jobs.remove(i), Ok(ExecutionResult::success())));
} else {
i += 1;
}
}
Ok(results)
}
fn sweep_completed_jobs(&mut self) -> Vec<Job> {
let mut completed_jobs = vec![];
let mut i = 0;
while i != self.jobs.len() {
if self.jobs[i].tasks.is_empty() {
completed_jobs.push(self.jobs.remove(i));
} else {
i += 1;
}
}
completed_jobs
}
}
/// Represents the current execution state of a job.
#[derive(Clone)]
pub enum JobState {
/// Unknown state.
Unknown,
/// The job is running.
Running,
/// The job is stopped.
Stopped,
/// The job has completed.
Done,
}
impl Display for JobState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unknown => write!(f, "Unknown"),
Self::Running => write!(f, "Running"),
Self::Stopped => write!(f, "Stopped"),
Self::Done => write!(f, "Done"),
}
}
}
/// Represents an annotation for a job.
#[derive(Clone)]
pub enum JobAnnotation {
/// No annotation.
None,
/// The job is the current job.
Current,
/// The job is the previous job.
Previous,
}
impl Display for JobAnnotation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, ""),
Self::Current => write!(f, "+"),
Self::Previous => write!(f, "-"),
}
}
}
/// Encapsulates a set of processes managed by the shell as a single unit.
pub struct Job {
/// The tasks that make up the job.
tasks: VecDeque<JobTask>,
/// If available, the process group ID of the job's processes.
pgid: Option<sys::process::ProcessId>,
/// The annotation of the job (e.g., current, previous).
annotation: JobAnnotation,
/// The shell-internal ID of the job.
pub id: usize,
/// The command line of the job.
pub command_line: String,
/// The current operational state of the job.
pub state: JobState,
}
impl Display for Job {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{}]{:3}{}\t{}",
self.id,
self.annotation.to_string(),
self.state,
self.command_line
)
}
}
impl Job {
/// Returns a new job object.
///
/// # Arguments
///
/// * `children` - The job's known child processes.
/// * `command_line` - The command line of the job.
/// * `state` - The current operational state of the job.
pub(crate) fn new<I>(tasks: I, command_line: String, state: JobState) -> Self
where
I: IntoIterator<Item = JobTask>,
{
Self {
id: 0,
tasks: tasks.into_iter().collect(),
pgid: None,
annotation: JobAnnotation::None,
command_line,
state,
}
}
/// Returns a pid-style string for the job.
pub fn to_pid_style_string(&self) -> String {
let display_pid = self
.representative_pid()
.map_or_else(|| String::from("<pid unknown>"), |pid| pid.to_string());
std::format!("[{}]{}\t{}", self.id, self.annotation, display_pid)
}
/// Returns the annotation of the job.
pub fn annotation(&self) -> JobAnnotation {
self.annotation.clone()
}
/// Returns the command name of the job.
pub fn command_name(&self) -> &str {
self.command_line
.split_ascii_whitespace()
.next()
.unwrap_or_default()
}
/// Returns whether the job is the current job.
pub const fn is_current(&self) -> bool {
matches!(self.annotation, JobAnnotation::Current)
}
/// Returns whether the job is the previous job.
pub const fn is_prev(&self) -> bool {
matches!(self.annotation, JobAnnotation::Previous)
}
/// Polls whether the job has completed.
pub fn poll_done(
&mut self,
) -> Result<Option<Result<ExecutionResult, error::Error>>, error::Error> {
let mut result: Option<Result<ExecutionResult, error::Error>> = None;
tracing::debug!(target: trace_categories::JOBS, "Polling job {} for completion...", self.id);
while !self.tasks.is_empty() {
let task = &mut self.tasks[0];
match task.poll() {
Some(r) => {
self.tasks.remove(0);
result = Some(r);
}
None => {
return Ok(None);
}
}
}
tracing::debug!(target: trace_categories::JOBS, "Job {} has completed.", self.id);
self.state = JobState::Done;
Ok(result)
}
/// Waits for the job to complete.
pub async fn wait(&mut self) -> Result<ExecutionResult, error::Error> {
let mut result = ExecutionResult::success();
while let Some(task) = self.tasks.back_mut() {
match task.wait().await? {
JobTaskWaitResult::Completed(execution_result) => {
result = execution_result;
self.tasks.pop_back();
}
JobTaskWaitResult::Stopped => {
self.state = JobState::Stopped;
return Ok(ExecutionResult::stopped());
}
}
}
self.state = JobState::Done;
Ok(result)
}
/// Moves the job to execute in the background.
pub fn move_to_background(&mut self) -> Result<(), error::Error> {
if matches!(self.state, JobState::Stopped) {
if let Some(pgid) = self.process_group_id() {
sys::signal::continue_process(pgid)?;
self.state = JobState::Running;
Ok(())
} else {
Err(error::ErrorKind::FailedToSendSignal.into())
}
} else {
error::unimp("move job to background")
}
}
/// Moves the job to execute in the foreground.
pub fn move_to_foreground(&mut self) -> Result<(), error::Error> {
if matches!(self.state, JobState::Stopped) {
if let Some(pgid) = self.process_group_id() {
sys::signal::continue_process(pgid)?;
self.state = JobState::Running;
} else {
return Err(error::ErrorKind::FailedToSendSignal.into());
}
}
if let Some(pgid) = self.process_group_id() {
sys::terminal::move_to_foreground(pgid)?;
}
Ok(())
}
/// Kills the job.
///
/// # Arguments
///
/// * `signal` - The signal to send to the job.
pub fn kill(&self, signal: traps::TrapSignal) -> Result<(), error::Error> {
if let Some(pid) = self.process_group_id() {
sys::signal::kill_process(pid, signal)
} else {
Err(error::ErrorKind::FailedToSendSignal.into())
}
}
/// Tries to retrieve a "representative" pid for the job.
pub fn representative_pid(&self) -> Option<sys::process::ProcessId> {
for task in &self.tasks {
match task {
JobTask::External(p) => {
if let Some(pid) = p.pid() {
return Some(pid);
}
}
JobTask::Internal(_) => (),
}
}
None
}
/// Tries to retrieve the process group ID (PGID) of the job.
pub fn process_group_id(&self) -> Option<sys::process::ProcessId> {
// TODO(jobs): Don't assume that the first PID is the PGID.
self.pgid.or_else(|| self.representative_pid())
}
}
+37
View File
@@ -0,0 +1,37 @@
use std::collections::HashSet;
use std::sync::LazyLock;
fn get_keywords(sh_mode_only: bool) -> HashSet<&'static str> {
let mut keywords = HashSet::new();
keywords.insert("!");
keywords.insert("{");
keywords.insert("}");
keywords.insert("case");
keywords.insert("do");
keywords.insert("done");
keywords.insert("elif");
keywords.insert("else");
keywords.insert("esac");
keywords.insert("fi");
keywords.insert("for");
keywords.insert("if");
keywords.insert("in");
keywords.insert("then");
keywords.insert("until");
keywords.insert("while");
if !sh_mode_only {
keywords.insert("[[");
keywords.insert("]]");
keywords.insert("coproc");
keywords.insert("function");
keywords.insert("select");
keywords.insert("time");
}
keywords
}
pub(crate) static SH_MODE_KEYWORDS: LazyLock<HashSet<&'static str>> =
LazyLock::new(|| get_keywords(true));
pub(crate) static KEYWORDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| get_keywords(false));
+64
View File
@@ -0,0 +1,64 @@
//! Core implementation of the brush shell. Implements the shell's abstraction, its interpreter, and
//! various facilities used internally by the shell.
pub mod arithmetic;
mod braceexpansion;
pub mod builtins;
pub mod callstack;
pub mod commands;
pub mod completion;
pub mod env;
pub mod error;
pub mod escape;
pub mod expansion;
mod extendedtests;
pub mod extensions;
pub mod functions;
pub mod history;
pub mod int_utils;
pub mod interfaces;
mod interp;
mod ioutils;
pub mod jobs;
mod keywords;
pub mod namedoptions;
pub mod openfiles;
pub mod options;
pub mod pathcache;
pub mod pathsearch;
pub mod patterns;
pub mod processes;
mod prompt;
mod regex;
pub mod results;
mod shell;
pub mod sourceinfo;
pub mod sys;
pub mod terminal;
pub mod tests;
pub mod timing;
pub mod trace_categories;
pub mod traps;
pub mod variables;
mod wellknownvars;
/// Re-export parser types used in core definitions.
pub mod parser {
pub use brush_parser::{
BindingParseError, ParseError, ParserImpl, SourcePosition, SourcePositionOffset,
SourceSpan, TestCommandParseError, WordParseError, ast,
};
}
pub use commands::{CommandArg, ExecutionContext};
pub use error::{BuiltinError, Error, ErrorKind};
pub use extensions::ShellExtensions;
pub use interp::{ExecutionParameters, ProcessGroupPolicy};
pub use parser::{SourcePosition, SourcePositionOffset, SourceSpan};
pub use results::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, ExecutionSpawnResult};
pub use shell::{
CreateOptions, ProfileLoadBehavior, RcLoadBehavior, Shell, ShellBuilder, ShellBuilderState,
ShellFd, ShellState,
};
pub use sourceinfo::SourceInfo;
pub use variables::{ShellValue, ShellVariable};
+878
View File
@@ -0,0 +1,878 @@
//! Defines shell options.
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::options::RuntimeOptions;
type OptionGetter = fn(shell: &RuntimeOptions) -> bool;
type OptionSetter = fn(shell: &mut RuntimeOptions, value: bool) -> ();
/// Defines an option.
pub struct ShellOptionDef {
/// Getter function that retrieves the current value of the option.
getter: OptionGetter,
/// Setter function that may be used to set the current value of the option.
setter: OptionSetter,
}
impl ShellOptionDef {
/// Constructs a new option definition.
///
/// # Arguments
///
/// * `getter` - A function that retrieves the current value of the option.
/// * `setter` - A function that sets the current value of the option.
fn new(getter: OptionGetter, setter: OptionSetter) -> Self {
Self { getter, setter }
}
/// Retrieves the current value of this option from the given runtime options.
///
/// # Arguments
///
/// * `options` - The runtime options to retrieve the value from.
pub fn get(&self, options: &RuntimeOptions) -> bool {
(self.getter)(options)
}
/// Sets the value of this option in the given runtime options.
///
/// # Arguments
///
/// * `options` - The runtime options to modify.
/// * `value` - The new value to set for the option.
pub fn set(&self, options: &mut RuntimeOptions, value: bool) {
(self.setter)(options, value);
}
}
/// Describes a shell option.
pub struct ShellOption {
/// The name of the option.
pub name: &'static str,
/// The definition of the option.
pub definition: &'static ShellOptionDef,
}
/// Describes a set of shell options.
pub struct ShellOptionSet {
inner: &'static HashMap<&'static str, ShellOptionDef>,
}
/// Kind of shell option.
#[derive(Clone, Copy)]
pub enum ShellOptionKind {
/// `set` option.
Set,
/// `set -o` option.
SetO,
/// `shopt` option.
Shopt,
}
/// Returns the options for the given shell option kind.
///
/// # Arguments
///
/// * `kind` - The kind of shell options to retrieve.
pub fn options(kind: ShellOptionKind) -> ShellOptionSet {
match kind {
ShellOptionKind::Set => ShellOptionSet {
inner: &SET_OPTIONS,
},
ShellOptionKind::SetO => ShellOptionSet {
inner: &SET_O_OPTIONS,
},
ShellOptionKind::Shopt => ShellOptionSet {
inner: &SHOPT_OPTIONS,
},
}
}
impl ShellOptionSet {
/// Returns an iterator over the options defined in this set.
pub fn iter(&self) -> impl Iterator<Item = ShellOption> {
self.inner
.iter()
.map(|(&name, definition)| ShellOption { name, definition })
}
/// Returns the option with the given name, if it exists.
///
/// # Arguments
///
/// * `name` - The name of the option to retrieve.
pub fn get(&self, name: &str) -> Option<&'static ShellOptionDef> {
self.inner.get(name)
}
}
static SET_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
HashMap::from([
(
"a",
ShellOptionDef::new(
|options| options.export_variables_on_modification,
|options, value| options.export_variables_on_modification = value,
),
),
(
"b",
ShellOptionDef::new(
|options| options.notify_job_termination_immediately,
|options, value| options.notify_job_termination_immediately = value,
),
),
(
"c",
ShellOptionDef::new(
|options| options.command_string_mode,
|options, value| options.command_string_mode = value,
),
),
(
"e",
ShellOptionDef::new(
|options| options.exit_on_nonzero_command_exit,
|options, value| options.exit_on_nonzero_command_exit = value,
),
),
(
"f",
ShellOptionDef::new(
|options| options.disable_filename_globbing,
|options, value| options.disable_filename_globbing = value,
),
),
(
"h",
ShellOptionDef::new(
|options| options.remember_command_locations,
|options, value| options.remember_command_locations = value,
),
),
(
"i",
ShellOptionDef::new(
|options| options.interactive,
|options, value| options.interactive = value,
),
),
(
"k",
ShellOptionDef::new(
|options| options.place_all_assignment_args_in_command_env,
|options, value| options.place_all_assignment_args_in_command_env = value,
),
),
(
"m",
ShellOptionDef::new(
|options| options.enable_job_control,
|options, value| options.enable_job_control = value,
),
),
(
"n",
ShellOptionDef::new(
|options| options.do_not_execute_commands,
|options, value| options.do_not_execute_commands = value,
),
),
(
"p",
ShellOptionDef::new(
|options| options.real_effective_uid_mismatch,
|options, value| options.real_effective_uid_mismatch = value,
),
),
(
"t",
ShellOptionDef::new(
|options| options.exit_after_one_command,
|options, value| options.exit_after_one_command = value,
),
),
(
"u",
ShellOptionDef::new(
|options| options.treat_unset_variables_as_error,
|options, value| options.treat_unset_variables_as_error = value,
),
),
(
"v",
ShellOptionDef::new(
|options| options.print_shell_input_lines,
|options, value| options.print_shell_input_lines = value,
),
),
(
"x",
ShellOptionDef::new(
|options| options.print_commands_and_arguments,
|options, value| options.print_commands_and_arguments = value,
),
),
(
"B",
ShellOptionDef::new(
|options| options.perform_brace_expansion,
|options, value| options.perform_brace_expansion = value,
),
),
(
"C",
ShellOptionDef::new(
|options| options.disallow_overwriting_regular_files_via_output_redirection,
|options, value| {
options.disallow_overwriting_regular_files_via_output_redirection = value;
},
),
),
(
"E",
ShellOptionDef::new(
|options| options.shell_functions_inherit_err_trap,
|options, value| options.shell_functions_inherit_err_trap = value,
),
),
(
"H",
ShellOptionDef::new(
|options| options.enable_bang_style_history_substitution,
|options, value| options.enable_bang_style_history_substitution = value,
),
),
(
"P",
ShellOptionDef::new(
|options| options.do_not_resolve_symlinks_when_changing_dir,
|options, value| options.do_not_resolve_symlinks_when_changing_dir = value,
),
),
(
"T",
ShellOptionDef::new(
|options| options.shell_functions_inherit_debug_and_return_traps,
|options, value| options.shell_functions_inherit_debug_and_return_traps = value,
),
),
(
"s",
ShellOptionDef::new(
|options| options.read_commands_from_stdin,
|options, value| options.read_commands_from_stdin = value,
),
),
])
});
static SET_O_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
HashMap::from([
(
"allexport",
ShellOptionDef::new(
|options| options.export_variables_on_modification,
|options, value| options.export_variables_on_modification = value,
),
),
(
"braceexpand",
ShellOptionDef::new(
|options| options.perform_brace_expansion,
|options, value| options.perform_brace_expansion = value,
),
),
(
"emacs",
ShellOptionDef::new(
|options| options.emacs_mode,
|options, value| options.emacs_mode = value,
),
),
(
"errexit",
ShellOptionDef::new(
|options| options.exit_on_nonzero_command_exit,
|options, value| options.exit_on_nonzero_command_exit = value,
),
),
(
"errtrace",
ShellOptionDef::new(
|options| options.shell_functions_inherit_err_trap,
|options, value| options.shell_functions_inherit_err_trap = value,
),
),
(
"functrace",
ShellOptionDef::new(
|options| options.shell_functions_inherit_debug_and_return_traps,
|options, value| options.shell_functions_inherit_debug_and_return_traps = value,
),
),
(
"hashall",
ShellOptionDef::new(
|options| options.remember_command_locations,
|options, value| options.remember_command_locations = value,
),
),
(
"histexpand",
ShellOptionDef::new(
|options| options.enable_bang_style_history_substitution,
|options, value| options.enable_bang_style_history_substitution = value,
),
),
(
"history",
ShellOptionDef::new(
|options| options.enable_command_history,
|options, value| options.enable_command_history = value,
),
),
(
"ignoreeof",
ShellOptionDef::new(
|options| options.ignore_eof,
|options, value| options.ignore_eof = value,
),
),
(
"interactive-comments",
ShellOptionDef::new(
|options| options.interactive_comments,
|options, value| options.interactive_comments = value,
),
),
(
"keyword",
ShellOptionDef::new(
|options| options.place_all_assignment_args_in_command_env,
|options, value| options.place_all_assignment_args_in_command_env = value,
),
),
(
"monitor",
ShellOptionDef::new(
|options| options.enable_job_control,
|options, value| options.enable_job_control = value,
),
),
(
"noclobber",
ShellOptionDef::new(
|options| options.disallow_overwriting_regular_files_via_output_redirection,
|options, value| {
options.disallow_overwriting_regular_files_via_output_redirection = value;
},
),
),
(
"noexec",
ShellOptionDef::new(
|options| options.do_not_execute_commands,
|options, value| options.do_not_execute_commands = value,
),
),
(
"noglob",
ShellOptionDef::new(
|options| options.disable_filename_globbing,
|options, value| options.disable_filename_globbing = value,
),
),
("nolog", ShellOptionDef::new(|_| false, |_, _| ())),
(
"notify",
ShellOptionDef::new(
|options| options.notify_job_termination_immediately,
|options, value| options.notify_job_termination_immediately = value,
),
),
(
"nounset",
ShellOptionDef::new(
|options| options.treat_unset_variables_as_error,
|options, value| options.treat_unset_variables_as_error = value,
),
),
(
"onecmd",
ShellOptionDef::new(
|options| options.exit_after_one_command,
|options, value| options.exit_after_one_command = value,
),
),
(
"physical",
ShellOptionDef::new(
|options| options.do_not_resolve_symlinks_when_changing_dir,
|options, value| options.do_not_resolve_symlinks_when_changing_dir = value,
),
),
(
"pipefail",
ShellOptionDef::new(
|options| options.return_last_failure_from_pipeline,
|options, value| options.return_last_failure_from_pipeline = value,
),
),
(
"posix",
ShellOptionDef::new(
|options| options.posix_mode,
|options, value| options.posix_mode = value,
),
),
(
"privileged",
ShellOptionDef::new(
|options| options.real_effective_uid_mismatch,
|options, value| options.real_effective_uid_mismatch = value,
),
),
(
"verbose",
ShellOptionDef::new(
|options| options.print_shell_input_lines,
|options, value| options.print_shell_input_lines = value,
),
),
(
"vi",
ShellOptionDef::new(
|options| options.vi_mode,
|options, value| options.vi_mode = value,
),
),
(
"xtrace",
ShellOptionDef::new(
|options| options.print_commands_and_arguments,
|options, value| options.print_commands_and_arguments = value,
),
),
])
});
static SHOPT_OPTIONS: LazyLock<HashMap<&'static str, ShellOptionDef>> = LazyLock::new(|| {
HashMap::from([
(
"autocd",
ShellOptionDef::new(
|options| options.auto_cd,
|options, value| options.auto_cd = value,
),
),
(
"array_expand_once",
ShellOptionDef::new(
|options| options.array_expand_once,
|options, value| options.array_expand_once = value,
),
),
(
"assoc_expand_once",
ShellOptionDef::new(
|options| options.assoc_expand_once,
|options, value| options.assoc_expand_once = value,
),
),
(
"bash_source_fullpath",
ShellOptionDef::new(
|options| options.bash_source_full_path,
|options, value| options.bash_source_full_path = value,
),
),
(
"cdable_vars",
ShellOptionDef::new(
|options| options.cdable_vars,
|options, value| options.cdable_vars = value,
),
),
(
"cdspell",
ShellOptionDef::new(
|options| options.cd_autocorrect_spelling,
|options, value| options.cd_autocorrect_spelling = value,
),
),
(
"checkhash",
ShellOptionDef::new(
|options| options.check_hashtable_before_command_exec,
|options, value| options.check_hashtable_before_command_exec = value,
),
),
(
"checkjobs",
ShellOptionDef::new(
|options| options.check_jobs_before_exit,
|options, value| options.check_jobs_before_exit = value,
),
),
(
"checkwinsize",
ShellOptionDef::new(
|options| options.check_window_size_after_external_commands,
|options, value| options.check_window_size_after_external_commands = value,
),
),
(
"cmdhist",
ShellOptionDef::new(
|options| options.save_multiline_cmds_in_history,
|options, value| options.save_multiline_cmds_in_history = value,
),
),
(
"compat31",
ShellOptionDef::new(
|options| options.compat31,
|options, value| options.compat31 = value,
),
),
(
"compat32",
ShellOptionDef::new(
|options| options.compat32,
|options, value| options.compat32 = value,
),
),
(
"compat40",
ShellOptionDef::new(
|options| options.compat40,
|options, value| options.compat40 = value,
),
),
(
"compat41",
ShellOptionDef::new(
|options| options.compat41,
|options, value| options.compat41 = value,
),
),
(
"compat42",
ShellOptionDef::new(
|options| options.compat42,
|options, value| options.compat42 = value,
),
),
(
"compat43",
ShellOptionDef::new(
|options| options.compat43,
|options, value| options.compat43 = value,
),
),
(
"compat44",
ShellOptionDef::new(
|options| options.compat44,
|options, value| options.compat44 = value,
),
),
(
"complete_fullquote",
ShellOptionDef::new(
|options| options.quote_all_metachars_in_completion,
|options, value| options.quote_all_metachars_in_completion = value,
),
),
(
"direxpand",
ShellOptionDef::new(
|options| options.expand_dir_names_on_completion,
|options, value| options.expand_dir_names_on_completion = value,
),
),
(
"dirspell",
ShellOptionDef::new(
|options| options.autocorrect_dir_spelling_on_completion,
|options, value| options.autocorrect_dir_spelling_on_completion = value,
),
),
(
"dotglob",
ShellOptionDef::new(
|options| options.glob_matches_dotfiles,
|options, value| options.glob_matches_dotfiles = value,
),
),
(
"execfail",
ShellOptionDef::new(
|options| options.exit_on_exec_fail,
|options, value| options.exit_on_exec_fail = value,
),
),
(
"expand_aliases",
ShellOptionDef::new(
|options| options.expand_aliases,
|options, value| options.expand_aliases = value,
),
),
(
"extdebug",
ShellOptionDef::new(
|options| options.enable_debugger,
|options, value| options.enable_debugger = value,
),
),
(
"extglob",
ShellOptionDef::new(
|options| options.extended_globbing,
|options, value| options.extended_globbing = value,
),
),
(
"extquote",
ShellOptionDef::new(
|options| options.extquote,
|options, value| options.extquote = value,
),
),
(
"failglob",
ShellOptionDef::new(
|options| options.fail_expansion_on_globs_without_match,
|options, value| options.fail_expansion_on_globs_without_match = value,
),
),
(
"force_fignore",
ShellOptionDef::new(
|options| options.force_fignore,
|options, value| options.force_fignore = value,
),
),
(
"globasciiranges",
ShellOptionDef::new(
|options| options.glob_ranges_use_c_locale,
|options, value| options.glob_ranges_use_c_locale = value,
),
),
(
"globskipdots",
ShellOptionDef::new(
|options| options.glob_skip_dots,
|options, value| options.glob_skip_dots = value,
),
),
(
"globstar",
ShellOptionDef::new(
|options| options.enable_star_star_glob,
|options, value| options.enable_star_star_glob = value,
),
),
(
"gnu_errfmt",
ShellOptionDef::new(
|options| options.errors_in_gnu_format,
|options, value| options.errors_in_gnu_format = value,
),
),
(
"histappend",
ShellOptionDef::new(
|options| options.append_to_history_file,
|options, value| options.append_to_history_file = value,
),
),
(
"histreedit",
ShellOptionDef::new(
|options| options.allow_reedit_failed_history_subst,
|options, value| options.allow_reedit_failed_history_subst = value,
),
),
(
"histverify",
ShellOptionDef::new(
|options| options.allow_modifying_history_substitution,
|options, value| options.allow_modifying_history_substitution = value,
),
),
(
"hostcomplete",
ShellOptionDef::new(
|options| options.enable_hostname_completion,
|options, value| options.enable_hostname_completion = value,
),
),
(
"huponexit",
ShellOptionDef::new(
|options| options.send_sighup_to_all_jobs_on_exit,
|options, value| options.send_sighup_to_all_jobs_on_exit = value,
),
),
(
"inherit_errexit",
ShellOptionDef::new(
|options| options.command_subst_inherits_errexit,
|options, value| options.command_subst_inherits_errexit = value,
),
),
(
"interactive_comments",
ShellOptionDef::new(
|options| options.interactive_comments,
|options, value| options.interactive_comments = value,
),
),
(
"lastpipe",
ShellOptionDef::new(
|options| options.run_last_pipeline_cmd_in_current_shell,
|options, value| options.run_last_pipeline_cmd_in_current_shell = value,
),
),
(
"lithist",
ShellOptionDef::new(
|options| options.embed_newlines_in_multiline_cmds_in_history,
|options, value| options.embed_newlines_in_multiline_cmds_in_history = value,
),
),
(
"localvar_inherit",
ShellOptionDef::new(
|options| options.local_vars_inherit_value_and_attrs,
|options, value| options.local_vars_inherit_value_and_attrs = value,
),
),
(
"localvar_unset",
ShellOptionDef::new(
|options| options.localvar_unset,
|options, value| options.localvar_unset = value,
),
),
(
"login_shell",
ShellOptionDef::new(
|options| options.login_shell,
|options, value| options.login_shell = value,
),
),
(
"mailwarn",
ShellOptionDef::new(
|options| options.mail_warn,
|options, value| options.mail_warn = value,
),
),
(
"no_empty_cmd_completion",
ShellOptionDef::new(
|options| options.no_empty_cmd_completion,
|options, value| options.no_empty_cmd_completion = value,
),
),
(
"nocaseglob",
ShellOptionDef::new(
|options| options.case_insensitive_pathname_expansion,
|options, value| options.case_insensitive_pathname_expansion = value,
),
),
(
"nocasematch",
ShellOptionDef::new(
|options| options.case_insensitive_conditionals,
|options, value| options.case_insensitive_conditionals = value,
),
),
(
"noexpand_translation",
ShellOptionDef::new(
|options| options.no_expand_translation,
|options, value| options.no_expand_translation = value,
),
),
(
"nullglob",
ShellOptionDef::new(
|options| options.expand_non_matching_patterns_to_null,
|options, value| options.expand_non_matching_patterns_to_null = value,
),
),
(
"patsub_replacement",
ShellOptionDef::new(
|options| options.patsub_replacement,
|options, value| options.patsub_replacement = value,
),
),
(
"progcomp",
ShellOptionDef::new(
|options| options.programmable_completion,
|options, value| options.programmable_completion = value,
),
),
(
"progcomp_alias",
ShellOptionDef::new(
|options| options.programmable_completion_alias,
|options, value| options.programmable_completion_alias = value,
),
),
(
"promptvars",
ShellOptionDef::new(
|options| options.expand_prompt_strings,
|options, value| options.expand_prompt_strings = value,
),
),
(
"restricted_shell",
ShellOptionDef::new(
|options| options.restricted_shell,
|options, value| options.restricted_shell = value,
),
),
(
"shift_verbose",
ShellOptionDef::new(
|options| options.shift_verbose,
|options, value| options.shift_verbose = value,
),
),
(
"sourcepath",
ShellOptionDef::new(
|options| options.source_builtin_searches_path,
|options, value| options.source_builtin_searches_path = value,
),
),
(
"varredir_close",
ShellOptionDef::new(
|options| options.var_redir_close,
|options, value| options.var_redir_close = value,
),
),
(
"xpg_echo",
ShellOptionDef::new(
|options| options.echo_builtin_expands_escape_sequences,
|options, value| options.echo_builtin_expands_escape_sequences = value,
),
),
])
});
+457
View File
@@ -0,0 +1,457 @@
//! Managing files open within a shell instance.
use std::collections::HashMap;
use std::io::IsTerminal;
use std::process::Stdio;
use crate::ShellFd;
use crate::error;
use crate::ioutils;
use crate::sys;
/// A trait representing a stream that can be read from and written to.
/// This is used for custom stream implementations in `OpenFile`.
///
/// Types that implement this trait are expected to be cloneable via the
/// `clone_box` function.
pub trait Stream: std::io::Read + std::io::Write + Send + Sync {
/// Clones the stream into a boxed trait object.
fn clone_box(&self) -> Box<dyn Stream>;
/// Converts the stream into an `OwnedFd`. Returns an error if the operation
/// is not supported or if it fails.
#[cfg(unix)]
fn try_clone_to_owned(&self) -> Result<std::os::fd::OwnedFd, error::Error>;
/// Borrows the stream as a `BorrowedFd`. Returns an error if the operation
/// is not supported or if it fails.
#[cfg(unix)]
fn try_borrow_as_fd(&self) -> Result<std::os::fd::BorrowedFd<'_>, error::Error>;
}
/// Represents a file open in a shell context.
pub enum OpenFile {
/// The original standard input this process was started with.
Stdin(std::io::Stdin),
/// The original standard output this process was started with.
Stdout(std::io::Stdout),
/// The original standard error this process was started with.
Stderr(std::io::Stderr),
/// A file open for reading or writing.
File(std::fs::File),
/// A read end of a pipe.
PipeReader(std::io::PipeReader),
/// A write end of a pipe.
PipeWriter(std::io::PipeWriter),
/// A custom stream.
Stream(Box<dyn Stream>),
}
#[cfg(feature = "serde")]
impl serde::Serialize for OpenFile {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Stdin(_) => serializer.serialize_str("stdin"),
Self::Stdout(_) => serializer.serialize_str("stdout"),
Self::Stderr(_) => serializer.serialize_str("stderr"),
Self::File(_) => serializer.serialize_str("file"),
Self::PipeReader(_) => serializer.serialize_str("pipe_reader"),
Self::PipeWriter(_) => serializer.serialize_str("pipe_writer"),
Self::Stream(_) => serializer.serialize_str("stream"),
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for OpenFile {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
match String::deserialize(deserializer)?.as_str() {
"stdin" => return Ok(std::io::stdin().into()),
"stdout" => return Ok(std::io::stdout().into()),
"stderr" => return Ok(std::io::stderr().into()),
"file" => (),
"pipe_reader" => (),
"pipe_writer" => (),
"stream" => (),
_ => return Err(serde::de::Error::custom("invalid open file")),
}
// TODO(serde): Figure out something better to do with open pipes and files.
null().map_err(serde::de::Error::custom)
}
}
/// Returns an open file that will discard all I/O.
pub fn null() -> Result<OpenFile, error::Error> {
let file = sys::fs::open_null_file()?;
Ok(OpenFile::File(file))
}
impl Clone for OpenFile {
fn clone(&self) -> Self {
// If we fail to clone the open file for any reason, we return a special file
// that discards all I/O. This allows us to avoid fatally erroring out.
self.try_clone().unwrap_or_else(|_err| {
ioutils::FailingReaderWriter::new("failed to duplicate open file").into()
})
}
}
impl std::fmt::Display for OpenFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Stdin(_) => write!(f, "stdin"),
Self::Stdout(_) => write!(f, "stdout"),
Self::Stderr(_) => write!(f, "stderr"),
Self::File(_) => write!(f, "file"),
Self::PipeReader(_) => write!(f, "pipe reader"),
Self::PipeWriter(_) => write!(f, "pipe writer"),
Self::Stream(_) => write!(f, "stream"),
}
}
}
impl OpenFile {
/// Tries to duplicate the open file.
pub fn try_clone(&self) -> Result<Self, std::io::Error> {
let result = match self {
Self::Stdin(_) => std::io::stdin().into(),
Self::Stdout(_) => std::io::stdout().into(),
Self::Stderr(_) => std::io::stderr().into(),
Self::File(f) => f.try_clone()?.into(),
Self::PipeReader(f) => f.try_clone()?.into(),
Self::PipeWriter(f) => f.try_clone()?.into(),
Self::Stream(s) => Self::Stream(s.clone_box()),
};
Ok(result)
}
/// Converts the open file into an `OwnedFd`.
#[cfg(unix)]
pub(crate) fn try_clone_to_owned(self) -> Result<std::os::fd::OwnedFd, error::Error> {
use std::os::fd::AsFd as _;
match self {
Self::Stdin(f) => Ok(f.as_fd().try_clone_to_owned()?),
Self::Stdout(f) => Ok(f.as_fd().try_clone_to_owned()?),
Self::Stderr(f) => Ok(f.as_fd().try_clone_to_owned()?),
Self::File(f) => Ok(f.into()),
Self::PipeReader(r) => Ok(std::os::fd::OwnedFd::from(r)),
Self::PipeWriter(w) => Ok(std::os::fd::OwnedFd::from(w)),
Self::Stream(s) => s.try_clone_to_owned(),
}
}
/// Borrows the open file as a `BorrowedFd`.
///
/// # Errors
///
/// Returns an error if the operation is not supported for the underlying file type.
#[cfg(unix)]
pub fn try_borrow_as_fd(&self) -> Result<std::os::fd::BorrowedFd<'_>, error::Error> {
use std::os::fd::AsFd as _;
match self {
Self::Stdin(f) => Ok(f.as_fd()),
Self::Stdout(f) => Ok(f.as_fd()),
Self::Stderr(f) => Ok(f.as_fd()),
Self::File(f) => Ok(f.as_fd()),
Self::PipeReader(r) => Ok(r.as_fd()),
Self::PipeWriter(w) => Ok(w.as_fd()),
Self::Stream(s) => s.try_borrow_as_fd(),
}
}
pub(crate) fn is_dir(&self) -> bool {
match self {
Self::Stdin(_) | Self::Stdout(_) | Self::Stderr(_) => false,
Self::File(file) => file.metadata().is_ok_and(|m| m.is_dir()),
Self::PipeReader(_) | Self::PipeWriter(_) | Self::Stream(_) => false,
}
}
/// Checks if the open file is associated with a terminal.
pub fn is_terminal(&self) -> bool {
match self {
Self::Stdin(f) => f.is_terminal(),
Self::Stdout(f) => f.is_terminal(),
Self::Stderr(f) => f.is_terminal(),
Self::File(f) => f.is_terminal(),
Self::PipeReader(_) | Self::PipeWriter(_) | Self::Stream(_) => false,
}
}
}
impl From<std::io::Stdin> for OpenFile {
/// Creates an `OpenFile` from standard input.
fn from(stdin: std::io::Stdin) -> Self {
Self::Stdin(stdin)
}
}
impl From<std::io::Stdout> for OpenFile {
/// Creates an `OpenFile` from standard output.
fn from(stdout: std::io::Stdout) -> Self {
Self::Stdout(stdout)
}
}
impl From<std::io::Stderr> for OpenFile {
/// Creates an `OpenFile` from standard error.
fn from(stderr: std::io::Stderr) -> Self {
Self::Stderr(stderr)
}
}
impl From<std::fs::File> for OpenFile {
fn from(file: std::fs::File) -> Self {
Self::File(file)
}
}
impl From<std::io::PipeReader> for OpenFile {
fn from(reader: std::io::PipeReader) -> Self {
Self::PipeReader(reader)
}
}
impl From<std::io::PipeWriter> for OpenFile {
fn from(writer: std::io::PipeWriter) -> Self {
Self::PipeWriter(writer)
}
}
impl From<OpenFile> for Stdio {
fn from(open_file: OpenFile) -> Self {
match open_file {
OpenFile::Stdin(_) => Self::inherit(),
OpenFile::Stdout(_) => Self::inherit(),
OpenFile::Stderr(_) => Self::inherit(),
OpenFile::File(f) => f.into(),
OpenFile::PipeReader(f) => f.into(),
OpenFile::PipeWriter(f) => f.into(),
// NOTE: Custom streams cannot be converted to `Stdio`; we do our best here
// and return a null device instead.
OpenFile::Stream(_) => Self::null(),
}
}
}
impl std::io::Read for OpenFile {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
Self::Stdin(f) => f.read(buf),
Self::Stdout(_) => Err(std::io::Error::other(
error::ErrorKind::OpenFileNotReadable("stdout"),
)),
Self::Stderr(_) => Err(std::io::Error::other(
error::ErrorKind::OpenFileNotReadable("stderr"),
)),
Self::File(f) => f.read(buf),
Self::PipeReader(reader) => reader.read(buf),
Self::PipeWriter(_) => Err(std::io::Error::other(
error::ErrorKind::OpenFileNotReadable("pipe writer"),
)),
Self::Stream(s) => s.read(buf),
}
}
}
impl std::io::Write for OpenFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
Self::Stdin(_) => Err(std::io::Error::other(
error::ErrorKind::OpenFileNotWritable("stdin"),
)),
Self::Stdout(f) => f.write(buf),
Self::Stderr(f) => f.write(buf),
Self::File(f) => f.write(buf),
Self::PipeReader(_) => Err(std::io::Error::other(
error::ErrorKind::OpenFileNotWritable("pipe reader"),
)),
Self::PipeWriter(writer) => writer.write(buf),
Self::Stream(s) => s.write(buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Self::Stdin(_) => Ok(()),
Self::Stdout(f) => f.flush(),
Self::Stderr(f) => f.flush(),
Self::File(f) => f.flush(),
Self::PipeReader(_) => Ok(()),
Self::PipeWriter(writer) => writer.flush(),
Self::Stream(s) => s.flush(),
}
}
}
/// Tristate representing the an `OpenFile` entry in an `OpenFiles` structure.
pub enum OpenFileEntry<'a> {
/// File descriptor is present and has a valid associated `OpenFile`.
Open(&'a OpenFile),
/// File descriptor is explicitly marked as not being mapped to any `OpenFile`.
NotPresent,
/// File descriptor is not specified in any way; it may be provided by a
/// parent context of some kind.
NotSpecified,
}
/// Represents the open files in a shell context.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OpenFiles {
/// Maps shell file descriptors to open files.
files: HashMap<ShellFd, Option<OpenFile>>,
}
impl OpenFiles {
/// File descriptor used for standard input.
pub const STDIN_FD: ShellFd = 0;
/// File descriptor used for standard output.
pub const STDOUT_FD: ShellFd = 1;
/// File descriptor used for standard error.
pub const STDERR_FD: ShellFd = 2;
/// First file descriptor available for non-stdio files.
const FIRST_NON_STDIO_FD: ShellFd = 3;
/// Maximum file descriptor number allowed.
const MAX_FD: ShellFd = 1024;
/// Creates a new `OpenFiles` instance populated with stdin, stdout, and stderr
/// from the host environment.
pub(crate) fn new() -> Self {
Self {
files: HashMap::from([
(Self::STDIN_FD, Some(std::io::stdin().into())),
(Self::STDOUT_FD, Some(std::io::stdout().into())),
(Self::STDERR_FD, Some(std::io::stderr().into())),
]),
}
}
/// Updates the open files from the provided iterator of (fd number, `OpenFile`) pairs.
/// Any existing entries for the provided file descriptors will be overwritten.
///
/// # Arguments
///
/// * `files`: An iterator of (fd number, `OpenFile`) pairs to update the open files with.
pub fn update_from(&mut self, files: impl Iterator<Item = (ShellFd, OpenFile)>) {
for (fd, file) in files {
let _ = self.files.insert(fd, Some(file));
}
}
/// Retrieves the file backing standard input in this context.
pub fn try_stdin(&self) -> Option<&OpenFile> {
self.files.get(&Self::STDIN_FD).and_then(|f| f.as_ref())
}
/// Retrieves the file backing standard output in this context.
pub fn try_stdout(&self) -> Option<&OpenFile> {
self.files.get(&Self::STDOUT_FD).and_then(|f| f.as_ref())
}
/// Retrieves the file backing standard error in this context.
pub fn try_stderr(&self) -> Option<&OpenFile> {
self.files.get(&Self::STDERR_FD).and_then(|f| f.as_ref())
}
/// Tries to remove an open file by its file descriptor. If the file descriptor
/// is not used, `None` will be returned; otherwise, the removed file will
/// be returned.
///
/// Arguments:
///
/// * `fd`: The file descriptor to remove.
pub fn remove_fd(&mut self, fd: ShellFd) -> Option<OpenFile> {
self.files.insert(fd, None).and_then(|f| f)
}
/// Tries to lookup the `OpenFile` associated with a file descriptor.
/// Returns `None` if the file descriptor is not present.
///
/// Arguments:
///
/// * `fd`: The file descriptor to lookup.
pub fn try_fd(&self, fd: ShellFd) -> Option<&OpenFile> {
self.files.get(&fd).and_then(|f| f.as_ref())
}
/// Tries to lookup the `OpenFile` associated with a file descriptor. Returns
/// an `OpenFileEntry` representing the state of the file descriptor.
///
/// Arguments:
///
/// * `fd`: The file descriptor to lookup.
pub fn fd_entry(&self, fd: ShellFd) -> OpenFileEntry<'_> {
self.files
.get(&fd)
.map_or(OpenFileEntry::NotSpecified, |opt_file| match opt_file {
Some(f) => OpenFileEntry::Open(f),
None => OpenFileEntry::NotPresent,
})
}
/// Checks if the given file descriptor is in use.
pub fn contains_fd(&self, fd: ShellFd) -> bool {
self.files.contains_key(&fd)
}
/// Associates the given file descriptor with the provided file. If the file descriptor
/// is already in use, the previous file will be returned; otherwise, `None`
/// will be returned.
///
/// Arguments:
///
/// * `fd`: The file descriptor to associate with the file.
/// * `file`: The file to associate with the file descriptor.
pub fn set_fd(&mut self, fd: ShellFd, file: OpenFile) -> Option<OpenFile> {
self.files.insert(fd, Some(file)).and_then(|f| f)
}
/// Iterates over all file descriptors.
pub fn iter_fds(&self) -> impl Iterator<Item = (ShellFd, &OpenFile)> {
self.files
.iter()
.filter_map(|(fd, file)| file.as_ref().map(|f| (*fd, f)))
}
/// Adds a new open file, returning the assigned file descriptor.
///
/// # Arguments
///
/// * `file`: The open file to add.
pub fn add(&mut self, file: OpenFile) -> Result<ShellFd, error::Error> {
// Start searching for free file descriptors after the standard ones.
let mut fd = Self::FIRST_NON_STDIO_FD;
while self.files.contains_key(&fd) {
if fd >= Self::MAX_FD {
return Err(error::ErrorKind::TooManyOpenFiles.into());
}
fd += 1;
}
self.files.insert(fd, Some(file));
Ok(fd)
}
}
impl<I> From<I> for OpenFiles
where
I: Iterator<Item = (ShellFd, OpenFile)>,
{
fn from(iter: I) -> Self {
let files = iter.map(|(fd, file)| (fd, Some(file))).collect();
Self { files }
}
}
+392
View File
@@ -0,0 +1,392 @@
//! Defines runtime options for the shell.
use itertools::Itertools;
use crate::{CreateOptions, extensions, namedoptions};
/// Runtime changeable options for a shell instance.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[expect(clippy::module_name_repetitions)]
pub struct RuntimeOptions {
//
// Single-character options.
/// -a
pub export_variables_on_modification: bool,
/// -b
pub notify_job_termination_immediately: bool,
/// -e
pub exit_on_nonzero_command_exit: bool,
/// -f
pub disable_filename_globbing: bool,
/// -h
pub remember_command_locations: bool,
/// -k
pub place_all_assignment_args_in_command_env: bool,
/// -m
pub enable_job_control: bool,
/// -n
pub do_not_execute_commands: bool,
/// -p
pub real_effective_uid_mismatch: bool,
/// -t
pub exit_after_one_command: bool,
/// -u
pub treat_unset_variables_as_error: bool,
/// -v
pub print_shell_input_lines: bool,
/// -x
pub print_commands_and_arguments: bool,
/// -B
pub perform_brace_expansion: bool,
/// -C
pub disallow_overwriting_regular_files_via_output_redirection: bool,
/// -E
pub shell_functions_inherit_err_trap: bool,
/// -H
pub enable_bang_style_history_substitution: bool,
/// -P
pub do_not_resolve_symlinks_when_changing_dir: bool,
/// -T
pub shell_functions_inherit_debug_and_return_traps: bool,
//
// Options set through -o.
/// 'emacs'
pub emacs_mode: bool,
/// 'history'
pub enable_command_history: bool,
/// 'ignoreeof'
pub ignore_eof: bool,
/// 'pipefail'
pub return_last_failure_from_pipeline: bool,
/// 'posix'
pub posix_mode: bool,
/// 'vi'
pub vi_mode: bool,
//
// Options set through shopt.
/// `array_expand_once`
pub array_expand_once: bool,
/// `assoc_expand_once`
pub assoc_expand_once: bool,
/// 'autocd'
pub auto_cd: bool,
/// `bash_source_full_path`
pub bash_source_full_path: bool,
/// `cdable_vars`
pub cdable_vars: bool,
/// 'cdspell'
pub cd_autocorrect_spelling: bool,
/// 'checkhash'
pub check_hashtable_before_command_exec: bool,
/// 'checkjobs'
pub check_jobs_before_exit: bool,
/// 'checkwinsize'
pub check_window_size_after_external_commands: bool,
/// 'cmdhist'
pub save_multiline_cmds_in_history: bool,
/// 'compat31'
pub compat31: bool,
/// 'compat32'
pub compat32: bool,
/// 'compat40'
pub compat40: bool,
/// 'compat41'
pub compat41: bool,
/// 'compat42'
pub compat42: bool,
/// 'compat43'
pub compat43: bool,
/// 'compat44'
pub compat44: bool,
/// `complete_fullquote`
pub quote_all_metachars_in_completion: bool,
/// 'direxpand'
pub expand_dir_names_on_completion: bool,
/// 'dirspell'
pub autocorrect_dir_spelling_on_completion: bool,
/// 'dotglob'
pub glob_matches_dotfiles: bool,
/// 'execfail'
pub exit_on_exec_fail: bool,
/// `expand_aliases`
pub expand_aliases: bool,
/// 'extdebug'
pub enable_debugger: bool,
/// 'extglob'
pub extended_globbing: bool,
/// 'extquote'
pub extquote: bool,
/// 'failglob'
pub fail_expansion_on_globs_without_match: bool,
/// `force_fignore`
pub force_fignore: bool,
/// 'globasciiranges'
pub glob_ranges_use_c_locale: bool,
/// 'globskipdots'
pub glob_skip_dots: bool,
/// 'globstar'
pub enable_star_star_glob: bool,
/// `gnu_errfmt`
pub errors_in_gnu_format: bool,
/// 'histappend'
pub append_to_history_file: bool,
/// 'histreedit'
pub allow_reedit_failed_history_subst: bool,
/// 'histverify'
pub allow_modifying_history_substitution: bool,
/// 'hostcomplete'
pub enable_hostname_completion: bool,
/// 'huponexit'
pub send_sighup_to_all_jobs_on_exit: bool,
/// `inherit_errexit`
pub command_subst_inherits_errexit: bool,
/// `interactive_comments`
pub interactive_comments: bool,
/// 'lastpipe'
pub run_last_pipeline_cmd_in_current_shell: bool,
/// 'lithist'
pub embed_newlines_in_multiline_cmds_in_history: bool,
/// `localvar_inherit`
pub local_vars_inherit_value_and_attrs: bool,
/// `localvar_unset`
pub localvar_unset: bool,
/// `login_shell`
pub login_shell: bool,
/// 'mailwarn'
pub mail_warn: bool,
/// `no_empty_cmd_completion`
pub no_empty_cmd_completion: bool,
/// 'nocaseglob'
pub case_insensitive_pathname_expansion: bool,
/// 'nocasematch'
pub case_insensitive_conditionals: bool,
/// `noexpand_translation`
pub no_expand_translation: bool,
/// 'nullglob'
pub expand_non_matching_patterns_to_null: bool,
/// `patsub_replacement`
pub patsub_replacement: bool,
/// 'progcomp'
pub programmable_completion: bool,
/// `progcomp_alias`
pub programmable_completion_alias: bool,
/// 'promptvars'
pub expand_prompt_strings: bool,
/// `restricted_shell`
pub restricted_shell: bool,
/// `shift_verbose`
pub shift_verbose: bool,
/// `sourcepath`
pub source_builtin_searches_path: bool,
/// `varredir_close`
pub var_redir_close: bool,
/// `xpg_echo`
pub echo_builtin_expands_escape_sequences: bool,
//
// Options set by the shell.
/// Whether or not the shell is interactive.
pub interactive: bool,
/// Whether commands are being read from stdin.
pub read_commands_from_stdin: bool,
/// Whether the shell is in command string mode (-c).
pub command_string_mode: bool,
/// Whether or not the shell is in maximal `sh` compatibility mode.
pub sh_mode: bool,
/// Whether to treat external commands as session leaders.
pub external_cmd_leads_session: bool,
/// Maximum function call depth.
pub max_function_call_depth: Option<usize>,
}
impl RuntimeOptions {
/// Creates a default set of runtime options based on the given creation options.
///
/// # Arguments
///
/// * `create_options` - The options used to create the shell.
pub fn defaults_from<SE: extensions::ShellExtensions>(
create_options: &CreateOptions<SE>,
) -> Self {
// There's a set of options enabled by default for all shells.
let mut options = Self {
interactive: create_options.interactive,
disallow_overwriting_regular_files_via_output_redirection: create_options
.disallow_overwriting_regular_files_via_output_redirection,
do_not_execute_commands: create_options.do_not_execute_commands,
enable_command_history: create_options.interactive,
enable_job_control: create_options.interactive,
exit_after_one_command: create_options.exit_after_one_command,
read_commands_from_stdin: create_options.read_commands_from_stdin,
command_string_mode: create_options.command_string_mode,
sh_mode: create_options.sh_mode,
posix_mode: create_options.posix,
print_commands_and_arguments: create_options.print_commands_and_arguments,
print_shell_input_lines: create_options.verbose,
treat_unset_variables_as_error: create_options.treat_unset_variables_as_error,
exit_on_nonzero_command_exit: create_options.exit_on_nonzero_command_exit,
external_cmd_leads_session: create_options.external_cmd_leads_session,
login_shell: create_options.login,
disable_filename_globbing: create_options.disable_pathname_expansion,
remember_command_locations: true,
check_window_size_after_external_commands: true,
save_multiline_cmds_in_history: true,
extquote: true,
force_fignore: true,
case_insensitive_pathname_expansion:
crate::sys::fs::default_case_insensitive_path_expansion(),
enable_hostname_completion: true,
interactive_comments: true,
expand_prompt_strings: true,
source_builtin_searches_path: true,
perform_brace_expansion: true,
quote_all_metachars_in_completion: true,
programmable_completion: true,
glob_ranges_use_c_locale: true,
glob_skip_dots: true,
patsub_replacement: true,
max_function_call_depth: create_options.max_function_call_depth,
..Self::default()
};
// Additional options are enabled by default for interactive shells.
if create_options.interactive {
options.enable_bang_style_history_substitution = true;
options.emacs_mode = !create_options.no_editing;
options.expand_aliases = true;
}
// Update any options.
for enabled_option in &create_options.enabled_options {
if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
.get(enabled_option.as_str())
{
option.set(&mut options, true);
}
}
for disabled_option in &create_options.disabled_options {
if let Some(option) = namedoptions::options(namedoptions::ShellOptionKind::SetO)
.get(disabled_option.as_str())
{
option.set(&mut options, false);
}
}
// Update any shopt options.
for enabled_option in &create_options.enabled_shopt_options {
if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
.get(enabled_option.as_str())
{
shopt_option.set(&mut options, true);
}
}
for disabled_option in &create_options.disabled_shopt_options {
if let Some(shopt_option) = namedoptions::options(namedoptions::ShellOptionKind::Shopt)
.get(disabled_option.as_str())
{
shopt_option.set(&mut options, false);
}
}
options
}
/// Returns a string representing the current `set`-style option flags set in the shell.
pub fn option_flags(&self) -> String {
let mut cs = vec![];
for o in namedoptions::options(namedoptions::ShellOptionKind::Set).iter() {
if o.definition.get(self)
&& let Some(c) = o.name.chars().next()
{
cs.push(c);
}
}
// Sort the flags in a way that matches what bash does.
cs.sort_by_key(|flag| option_flag_sort_key(*flag));
cs.into_iter().collect()
}
/// Returns a colon-separated list of sorted 'set -o' options enabled.
pub fn seto_optstr(&self) -> String {
let mut cs = vec![];
for option in namedoptions::options(namedoptions::ShellOptionKind::SetO).iter() {
if option.definition.get(self) {
cs.push(option.name);
}
}
cs.sort_unstable();
cs.into_iter().join(":")
}
/// Returns a colon-separated list of sorted 'shopt' options enabled.
pub fn shopt_optstr(&self) -> String {
let mut cs = vec![];
for option in namedoptions::options(namedoptions::ShellOptionKind::Shopt).iter() {
if option.definition.get(self) {
cs.push(option.name);
}
}
cs.sort_unstable();
cs.into_iter().join(":")
}
}
/// Sort option flag character in a way that mirrors bash behavior.
///
/// # Arguments
///
/// * `ch` - The option flag character.
const fn option_flag_sort_key(ch: char) -> (u8, char) {
// NOTE: bash appears to sort in 3 groups. We mimic them:
// 1) Lowercase letters excluding 'c' and 's' (sorted)
// 2) Uppercase letters (sorted)
// 3) All other characters (sorted)
let group = if ch.is_ascii_lowercase() && !matches!(ch, 'c' | 's') {
0
} else if ch.is_ascii_uppercase() {
1
} else {
2
};
(group, ch)
}
#[cfg(test)]
mod tests {
use super::option_flag_sort_key;
#[test]
fn lowercase_excluding_c_and_s_sort_first() {
let mut flags = vec!['b', 'A', 'Z', 's', 'c', 'a'];
flags.sort_by_key(|flag| option_flag_sort_key(*flag));
assert_eq!(flags, vec!['a', 'b', 'A', 'Z', 'c', 's']);
}
#[test]
fn uppercase_sorted_before_miscellaneous() {
let mut flags = vec!['P', 'B', '1', 'T'];
flags.sort_by_key(|flag| option_flag_sort_key(*flag));
assert_eq!(flags, vec!['B', 'P', 'T', '1']);
}
#[test]
fn miscellaneous_characters_respect_ascii_order() {
let mut flags = vec!['s', 'c', '%', ':'];
flags.sort_by_key(|flag| option_flag_sort_key(*flag));
assert_eq!(flags, vec!['%', ':', 'c', 's']);
}
}
+59
View File
@@ -0,0 +1,59 @@
//! Path cache
use crate::{error, variables};
use std::path::PathBuf;
/// A cache of paths associated with names.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PathCache {
/// The cache itself.
cache: std::collections::HashMap<String, PathBuf>,
}
impl PathCache {
/// Clears all elements from the cache.
pub fn reset(&mut self) {
self.cache.clear();
}
/// Returns the path associated with the given name.
///
/// # Arguments
///
/// * `name` - The name to lookup.
pub fn get<S: AsRef<str>>(&self, name: S) -> Option<PathBuf> {
self.cache.get(name.as_ref()).cloned()
}
/// Sets the path associated with the given name.
///
/// # Arguments
///
/// * `name` - The name to set.
/// * `path` - The path to associate with the name.
pub fn set<T: Into<String>>(&mut self, name: T, path: PathBuf) {
self.cache.insert(name.into(), path);
}
/// Projects the cache into a shell value.
pub fn to_value(&self) -> Result<variables::ShellValue, error::Error> {
let pairs = self
.cache
.iter()
.map(|(k, v)| (Some(k.to_owned()), v.to_string_lossy().to_string()))
.collect::<Vec<_>>();
variables::ShellValue::associative_array_from_literals(variables::ArrayLiteral(pairs))
}
/// Removes the path associated with the given name, if there is one.
/// Returns whether or not an entry was removed.
///
/// # Arguments
///
/// * `name` - The name to remove.
pub fn unset<S: AsRef<str>>(&mut self, name: S) -> bool {
self.cache.remove(name.as_ref()).is_some()
}
}
+137
View File
@@ -0,0 +1,137 @@
//! Path searching utilities.
use std::{
collections::VecDeque,
path::{Path, PathBuf},
};
use crate::sys;
use crate::sys::fs::PathExt;
/// Encapsulates the result of a path search.
pub struct ExecutablePathSearch<PI, N> {
paths: VecDeque<PI>,
filename: N,
}
impl<PI, N> Iterator for ExecutablePathSearch<PI, N>
where
PI: AsRef<Path>,
N: AsRef<Path>,
{
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
while let Some(path) = self.paths.pop_front() {
let path = PathBuf::from(path.as_ref()).join(self.filename.as_ref());
// Skip directories outright, then ask the platform to resolve
// the path to an actual executable file (which, on Windows, may
// involve appending a PATHEXT extension). The helper takes
// ownership so Unix — where no resolution is needed — can return
// the path unchanged without allocating.
if path.is_dir() {
continue;
}
if let Some(resolved) = sys::fs::resolve_executable(path) {
return Some(resolved);
}
}
None
}
}
pub(crate) struct ExecutablePathPrefixSearch<PI> {
paths: VecDeque<PI>,
queued_items: VecDeque<PathBuf>,
filename_prefix: String,
case_insensitive: bool,
}
impl<PI> Iterator for ExecutablePathPrefixSearch<PI>
where
PI: AsRef<Path>,
{
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
// If we already found some items and queued them, then yield one now.
if let Some(item) = self.queued_items.pop_front() {
return Some(item);
}
while let Some(path) = self.paths.pop_front() {
let path = PathBuf::from(path.as_ref());
if let Ok(readdir) = path.read_dir() {
for entry in readdir.flatten() {
if let Ok(mut filename) = entry.file_name().into_string() {
if self.case_insensitive {
filename = filename.to_ascii_lowercase();
}
if !filename.starts_with(&self.filename_prefix) {
continue;
}
}
let entry_path = entry.path();
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() && entry_path.executable() {
self.queued_items.push_back(entry_path);
continue;
}
if file_type.is_symlink() && entry_path.executable() {
self.queued_items.push_back(entry_path);
}
}
}
}
if let Some(item) = self.queued_items.pop_front() {
return Some(item);
}
}
None
}
}
/// Search for the given executable name in the provided paths.
///
/// # Arguments
///
/// * `paths` - An iterator over the paths to search.
/// * `filename` - The name of the executable file to search for.
pub fn search_for_executable<P, PI, N>(paths: P, filename: N) -> ExecutablePathSearch<PI, N>
where
P: Iterator<Item = PI>,
PI: AsRef<Path>,
N: AsRef<Path>,
{
ExecutablePathSearch {
paths: paths.collect(),
filename,
}
}
pub(crate) fn search_for_executable_with_prefix<P, PI>(
paths: P,
filename_prefix: &str,
case_insensitive: bool,
) -> ExecutablePathPrefixSearch<PI>
where
P: Iterator<Item = PI>,
PI: AsRef<Path>,
{
let stored_prefix = if case_insensitive {
filename_prefix.to_ascii_lowercase()
} else {
filename_prefix.into()
};
ExecutablePathPrefixSearch {
paths: paths.collect(),
queued_items: VecDeque::new(),
filename_prefix: stored_prefix,
case_insensitive,
}
}
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
//! Process management
use futures::FutureExt;
use crate::{error, sys};
/// A waitable future that will yield the results of a child process's execution.
pub(crate) type WaitableChildProcess = std::pin::Pin<
Box<dyn futures::Future<Output = Result<std::process::Output, std::io::Error>> + Send + Sync>,
>;
/// Tracks a child process being awaited.
pub struct ChildProcess {
/// A waitable future that will yield the results of a child process's execution.
exec_future: WaitableChildProcess,
/// If available, the process ID of the child.
pid: Option<sys::process::ProcessId>,
/// If available, the process group ID of the child.
pgid: Option<sys::process::ProcessId>,
}
impl ChildProcess {
/// Wraps a child process and its future.
pub fn new(
child: sys::process::Child,
pid: Option<sys::process::ProcessId>,
pgid: Option<sys::process::ProcessId>,
) -> Self {
Self {
exec_future: Box::pin(child.wait_with_output()),
pid,
pgid,
}
}
/// Returns the process's ID.
pub const fn pid(&self) -> Option<sys::process::ProcessId> {
self.pid
}
/// Returns the process's group ID.
pub const fn pgid(&self) -> Option<sys::process::ProcessId> {
self.pgid
}
/// Waits for the process to exit.
pub async fn wait(&mut self) -> Result<ProcessWaitResult, error::Error> {
#[allow(unused_mut, reason = "only mutated on some platforms")]
let mut sigtstp = sys::signal::tstp_signal_listener()?;
#[allow(unused_mut, reason = "only mutated on some platforms")]
let mut sigchld = sys::signal::chld_signal_listener()?;
#[allow(clippy::ignored_unit_patterns)]
loop {
tokio::select! {
output = &mut self.exec_future => {
break Ok(ProcessWaitResult::Completed(output?))
},
_ = sigtstp.recv() => {
break Ok(ProcessWaitResult::Stopped)
},
_ = sigchld.recv() => {
if sys::signal::poll_for_stopped_children()? {
break Ok(ProcessWaitResult::Stopped);
}
},
_ = sys::signal::await_ctrl_c() => {
// SIGINT got thrown. Handle it and continue looping. The child should
// have received it as well, and either handled it or ended up getting
// terminated (in which case we'll see the child exit).
},
}
}
}
pub(crate) fn poll(&mut self) -> Option<Result<std::process::Output, error::Error>> {
let checkable_future = &mut self.exec_future;
checkable_future
.now_or_never()
.map(|result| result.map_err(Into::into))
}
}
/// Represents the result of waiting for an executing process.
pub enum ProcessWaitResult {
/// The process completed.
Completed(std::process::Output),
/// The process stopped and has not yet completed.
Stopped,
}
+278
View File
@@ -0,0 +1,278 @@
use crate::{
ExecutionParameters, error, expansion, extensions,
shell::Shell,
sys::{self, users},
};
use std::path::Path;
const VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
const VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
pub(crate) async fn expand_prompt(
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
spec: String,
) -> Result<String, error::Error> {
// Parse the prompt spec into its pieces.
let prompt_pieces = parse_prompt(spec)?;
// Now, render each piece.
let mut formatted_prompt = String::new();
for piece in prompt_pieces {
let needs_escaping = matches!(
piece,
brush_parser::prompt::PromptPiece::EscapedSequence(_)
| brush_parser::prompt::PromptPiece::DollarOrPound
);
let formatted_piece = format_prompt_piece(shell, piece)?;
if shell.options().expand_prompt_strings && needs_escaping {
formatted_prompt.push('\\');
}
formatted_prompt.push_str(&formatted_piece);
}
if shell.options().expand_prompt_strings {
// Now expand any remaining escape sequences, but without tilde-expansion.
let options = expansion::ExpanderOptions {
tilde_expand: false,
..Default::default()
};
formatted_prompt =
expansion::basic_expand_word_with_options(shell, params, &formatted_prompt, &options)
.await?;
}
Ok(formatted_prompt)
}
#[cached::proc_macro::cached(size = 64, result = true)]
fn parse_prompt(
spec: String,
) -> Result<Vec<brush_parser::prompt::PromptPiece>, brush_parser::WordParseError> {
brush_parser::prompt::parse(spec.as_str())
}
fn format_prompt_piece(
shell: &Shell<impl extensions::ShellExtensions>,
piece: brush_parser::prompt::PromptPiece,
) -> Result<String, error::Error> {
let formatted = match piece {
brush_parser::prompt::PromptPiece::EscapedSequence(s) => s,
brush_parser::prompt::PromptPiece::Literal(l) => l,
brush_parser::prompt::PromptPiece::AsciiCharacter(c) => {
char::from_u32(c).map_or_else(String::new, |c| c.to_string())
}
brush_parser::prompt::PromptPiece::Backslash => "\\".to_owned(),
brush_parser::prompt::PromptPiece::BellCharacter => "\x07".to_owned(),
brush_parser::prompt::PromptPiece::CarriageReturn => "\r".to_owned(),
brush_parser::prompt::PromptPiece::CurrentCommandNumber => {
return error::unimp("prompt: current command number");
}
brush_parser::prompt::PromptPiece::CurrentHistoryNumber => {
return error::unimp("prompt: current history number");
}
brush_parser::prompt::PromptPiece::CurrentUser => users::get_current_username()?,
brush_parser::prompt::PromptPiece::CurrentWorkingDirectory {
tilde_replaced,
basename,
} => format_current_working_directory(shell, tilde_replaced, basename),
brush_parser::prompt::PromptPiece::Date(format) => {
format_date(&chrono::Local::now(), &format)
}
brush_parser::prompt::PromptPiece::DollarOrPound => {
if users::is_root() {
"#".to_owned()
} else {
"$".to_owned()
}
}
// NOTE: We mimic bash and convert \[ into \001, a.k.a. RL_PROMPT_START_IGNORE.
// It will need to get removed before it's actually displayed. While present it
// also has the important (compatible) side effect of ensuring the text on either
// side of it is not concatenated together, potentially resulting in incompatible
// variable expansions. Also, we *only* do this if the shell is interactive.
brush_parser::prompt::PromptPiece::EndNonPrintingSequence => {
if shell.options().interactive {
"\x02".to_owned()
} else {
String::new()
}
}
brush_parser::prompt::PromptPiece::EscapeCharacter => "\x1b".to_owned(),
brush_parser::prompt::PromptPiece::Hostname {
only_up_to_first_dot,
} => {
let hn = sys::network::get_hostname()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if only_up_to_first_dot && let Some((first, _)) = hn.split_once('.') {
return Ok(first.to_owned());
}
hn
}
brush_parser::prompt::PromptPiece::Newline => "\n".to_owned(),
brush_parser::prompt::PromptPiece::NumberOfManagedJobs => {
shell.jobs().jobs.len().to_string()
}
brush_parser::prompt::PromptPiece::ShellBaseName => {
if let Some(shell_name) = shell.current_shell_name() {
Path::new(shell_name.as_ref())
.file_name()
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_default()
} else {
String::new()
}
}
brush_parser::prompt::PromptPiece::ShellRelease => {
std::format!("{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_PATCH}")
}
brush_parser::prompt::PromptPiece::ShellVersion => {
std::format!("{VERSION_MAJOR}.{VERSION_MINOR}")
}
// NOTE: See above note for EndNonPrintingSequence
brush_parser::prompt::PromptPiece::StartNonPrintingSequence => {
if shell.options().interactive {
"\x01".to_owned()
} else {
String::new()
}
}
brush_parser::prompt::PromptPiece::TerminalDeviceBaseName => {
sys::terminal::try_get_terminal_device_path()
.and_then(|p| p.file_name().map(|s| s.to_string_lossy().to_string()))
.unwrap_or_default()
}
brush_parser::prompt::PromptPiece::Time(time_fmt) => {
format_time(&chrono::Local::now(), &time_fmt)
}
};
Ok(formatted)
}
fn format_current_working_directory(
shell: &Shell<impl extensions::ShellExtensions>,
tilde_replaced: bool,
basename: bool,
) -> String {
let mut working_dir_str = shell.working_dir().to_string_lossy().to_string();
if tilde_replaced {
working_dir_str = shell.tilde_shorten(working_dir_str);
}
if basename && let Some(filename) = Path::new(&working_dir_str).file_name() {
working_dir_str = filename.to_string_lossy().to_string();
}
if cfg!(windows) {
working_dir_str = working_dir_str.replace('\\', "/");
}
working_dir_str
}
fn format_time<Tz: chrono::TimeZone>(
datetime: &chrono::DateTime<Tz>,
format: &brush_parser::prompt::PromptTimeFormat,
) -> String
where
Tz::Offset: std::fmt::Display,
{
let formatted = match format {
brush_parser::prompt::PromptTimeFormat::TwelveHourAM => datetime.format("%I:%M %p"),
brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS => datetime.format("%I:%M:%S"),
brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMM => datetime.format("%H:%M"),
brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS => datetime.format("%H:%M:%S"),
};
formatted.to_string()
}
fn format_date<Tz: chrono::TimeZone>(
datetime: &chrono::DateTime<Tz>,
format: &brush_parser::prompt::PromptDateFormat,
) -> String
where
Tz::Offset: std::fmt::Display,
{
match format {
brush_parser::prompt::PromptDateFormat::WeekdayMonthDate => {
datetime.format("%a %b %d").to_string()
}
brush_parser::prompt::PromptDateFormat::Custom(fmt) => {
let fmt_items = chrono::format::StrftimeItems::new(fmt);
datetime.format_with_items(fmt_items).to_string()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_time() {
// Create a well-known test date/time.
let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T13:34:56.789Z").unwrap();
assert_eq!(
format_time(&dt, &brush_parser::prompt::PromptTimeFormat::TwelveHourAM),
"01:34 PM"
);
assert_eq!(
format_time(
&dt,
&brush_parser::prompt::PromptTimeFormat::TwentyFourHourHHMMSS
),
"13:34:56"
);
assert_eq!(
format_time(
&dt,
&brush_parser::prompt::PromptTimeFormat::TwelveHourHHMMSS
),
"01:34:56"
);
}
#[test]
fn test_format_date() {
// Create a well-known test date/time.
let dt = chrono::DateTime::parse_from_rfc3339("2024-12-25T12:34:56.789Z").unwrap();
assert_eq!(
format_date(
&dt,
&brush_parser::prompt::PromptDateFormat::WeekdayMonthDate
),
"Wed Dec 25"
);
assert_eq!(
format_date(
&dt,
&brush_parser::prompt::PromptDateFormat::Custom(String::from("%Y-%m-%d"))
),
"2024-12-25"
);
assert_eq!(
format_date(
&dt,
&brush_parser::prompt::PromptDateFormat::Custom(String::from(
"%Y-%m-%d %H:%M:%S.%f"
))
),
"2024-12-25 12:34:56.789000000"
);
}
}
+220
View File
@@ -0,0 +1,220 @@
#![allow(clippy::needless_pass_by_value)]
use std::borrow::Cow;
use std::cell::RefCell;
use crate::error;
use cached::Cached;
thread_local! {
static REGEX_CACHE: RefCell<cached::SizedCache<(String, bool, bool), fancy_regex::Regex>> =
RefCell::new(cached::SizedCache::with_size(64));
}
/// Represents a piece of a regular expression.
#[derive(Clone, Debug)]
pub(crate) enum RegexPiece {
/// A pattern that should be interpreted as a regular expression.
Pattern(String),
/// A literal string that should be matched exactly.
Literal(String),
}
impl RegexPiece {
fn to_regex_str(&self) -> Cow<'_, str> {
match self {
Self::Pattern(s) => Cow::Borrowed(s.as_str()),
Self::Literal(s) => escape_literal_regex_piece(s.as_str()),
}
}
}
type RegexWord = Vec<RegexPiece>;
/// Encapsulates a regular expression usable in the shell.
#[derive(Clone, Debug)]
pub struct Regex {
pieces: RegexWord,
case_insensitive: bool,
multiline: bool,
}
impl From<RegexWord> for Regex {
fn from(pieces: RegexWord) -> Self {
Self {
pieces,
case_insensitive: false,
multiline: false,
}
}
}
impl Regex {
/// Sets the regular expression's case sensitivity.
///
/// # Arguments
///
/// * `value` - The new case sensitivity value.
pub const fn set_case_insensitive(mut self, value: bool) -> Self {
self.case_insensitive = value;
self
}
/// Enables (or disables) multiline support for this pattern.
/// This enables matching across lines as well as enables `.`
/// to match newline characters.
///
/// # Arguments
///
/// * `value` - The new multiline value.
pub const fn set_multiline(mut self, value: bool) -> Self {
self.multiline = value;
self
}
/// Computes if the regular expression matches the given string.
///
/// # Arguments
///
/// * `value` - The string to check for a match.
pub fn matches(&self, value: &str) -> Result<Option<Vec<Option<String>>>, error::Error> {
let regex_pattern: String = self
.pieces
.iter()
.map(|piece| piece.to_regex_str())
.collect();
let re = compile_regex(regex_pattern, self.case_insensitive, self.multiline)?;
Ok(re.captures(value)?.map(|captures| {
captures
.iter()
.map(|c| c.map(|m| m.as_str().to_owned()))
.collect()
}))
}
}
pub(crate) fn compile_regex(
regex_str: String,
case_insensitive: bool,
multiline: bool,
) -> Result<fancy_regex::Regex, error::Error> {
// Move regex_str into the key to avoid cloning on cache-hit path.
let key = (regex_str, case_insensitive, multiline);
let cached_regex = REGEX_CACHE.with(|cache| cache.borrow_mut().cache_get(&key).cloned());
if let Some(re) = cached_regex {
return Ok(re);
}
// Handle identified cases where a shell-supported regex isn't supported directly by
// `fancy_regex` -- specifically, adding missing escape characters.
let mut regex_str = add_missing_escape_chars_to_regex(key.0.as_str());
// Handle multiline enablement.
if multiline {
// The fancy_regex crate internally seems to have flags that can be used
// to enable multiline support, but they're not exposed via its
// RegexBuilder. We instead just prefix with the right flags.
let updated_str = std::format!("(?ms){regex_str}");
regex_str = updated_str.into();
}
let mut builder = fancy_regex::RegexBuilder::new(regex_str.as_ref());
builder.case_insensitive(case_insensitive);
let re = match builder.build() {
Ok(re) => re,
Err(e) => return Err(error::ErrorKind::InvalidRegexError(e, regex_str.to_string()).into()),
};
// Release borrow on key.0 before moving key into cache_set.
drop(regex_str);
REGEX_CACHE.with(|cache| {
cache.borrow_mut().cache_set(key, re.clone());
});
Ok(re)
}
fn add_missing_escape_chars_to_regex(s: &str) -> Cow<'_, str> {
// We may see a character class with an unescaped '[' (open bracket) character. We need
// to escape that character.
let mut in_escape = false;
let mut in_brackets = false;
let mut insertion_positions = vec![];
let mut peekable = s.char_indices().peekable();
while let Some((byte_offset, c)) = peekable.next() {
let next_is_colon = peekable.peek().is_some_and(|(_, c)| *c == ':');
match c {
'[' if !in_escape && !in_brackets => {
in_brackets = true;
}
'[' if !in_escape && in_brackets && !next_is_colon => {
// Need to escape.
insertion_positions.push(byte_offset);
}
']' if !in_escape && in_brackets => {
in_brackets = false;
}
_ => (),
}
in_escape = !in_escape && c == '\\';
}
if insertion_positions.is_empty() {
return s.into();
}
let mut updated = s.to_owned();
for pos in insertion_positions.iter().rev() {
updated.insert(*pos, '\\');
}
updated.into()
}
fn escape_literal_regex_piece(s: &str) -> Cow<'_, str> {
let mut result = String::new();
for c in s.chars() {
match c {
c if regex_char_is_special(c) => {
result.push('\\');
result.push(c);
}
c => result.push(c),
}
}
result.into()
}
pub(crate) const fn regex_char_is_special(c: char) -> bool {
matches!(
c,
'\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}'
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_missing_escape_chars_to_regex() {
// Negative cases -- where we don't need to escape.
assert_eq!(add_missing_escape_chars_to_regex("a[b]"), "a[b]");
assert_eq!(add_missing_escape_chars_to_regex(r"a\[b\]"), r"a\[b\]");
assert_eq!(add_missing_escape_chars_to_regex(r"a[b\[]"), r"a[b\[]");
// Positive case -- where we need to escape.
assert_eq!(add_missing_escape_chars_to_regex(r"a[b[]"), r"a[b\[]");
assert_eq!(add_missing_escape_chars_to_regex(r"a[[]"), r"a[\[]");
}
}
+305
View File
@@ -0,0 +1,305 @@
//! Encapsulation of execution results.
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use crate::{error, processes};
/// Represents the result of executing a command or similar item.
#[derive(Default)]
pub struct ExecutionResult {
/// The control flow transition to apply after execution.
pub next_control_flow: ExecutionControlFlow,
/// The exit code resulting from execution.
pub exit_code: ExecutionExitCode,
}
impl ExecutionResult {
/// Returns a new `ExecutionResult` with the given exit code.
///
/// # Arguments
///
/// * `exit_code` - The exit code of the command.
pub fn new(exit_code: u8) -> Self {
Self {
exit_code: exit_code.into(),
..Self::default()
}
}
/// Returns a new `ExecutionResult` reflecting a process that was stopped.
pub fn stopped() -> Self {
// TODO(jobs): Decide how to sort this out in a platform-independent way.
const SIGTSTP: std::os::raw::c_int = 20;
#[expect(clippy::cast_possible_truncation)]
Self::new(128 + SIGTSTP as u8)
}
/// Returns a new `ExecutionResult` with an exit code of 0.
pub const fn success() -> Self {
Self {
next_control_flow: ExecutionControlFlow::Normal,
exit_code: ExecutionExitCode::Success,
}
}
/// Returns a new `ExecutionResult` with a general error exit code.
pub const fn general_error() -> Self {
Self {
next_control_flow: ExecutionControlFlow::Normal,
exit_code: ExecutionExitCode::GeneralError,
}
}
/// Returns whether the command was successful.
pub const fn is_success(&self) -> bool {
self.exit_code.is_success()
}
/// Returns whether the execution result indicates normal control flow.
/// Returns `false` if there is any control flow transition requested.
pub const fn is_normal_flow(&self) -> bool {
matches!(self.next_control_flow, ExecutionControlFlow::Normal)
}
/// Returns whether the execution result indicates a loop break.
pub const fn is_break(&self) -> bool {
matches!(
self.next_control_flow,
ExecutionControlFlow::BreakLoop { .. }
)
}
/// Returns whether the execution result indicates a loop continue.
pub const fn is_continue(&self) -> bool {
matches!(
self.next_control_flow,
ExecutionControlFlow::ContinueLoop { .. }
)
}
/// Returns whether the execution result indicates an early return
/// from a function or script, or an exit from the shell. Returns `false`
/// otherwise, including loop breaks or continues.
pub const fn is_return_or_exit(&self) -> bool {
matches!(
self.next_control_flow,
ExecutionControlFlow::ReturnFromFunctionOrScript | ExecutionControlFlow::ExitShell
)
}
}
impl From<ExecutionExitCode> for ExecutionResult {
fn from(exit_code: ExecutionExitCode) -> Self {
Self {
next_control_flow: ExecutionControlFlow::Normal,
exit_code,
}
}
}
impl From<ExecutionWaitResult> for ExecutionResult {
fn from(wait_result: ExecutionWaitResult) -> Self {
match wait_result {
ExecutionWaitResult::Completed(result) => result,
// TODO(jobs): We need to job-manage the stopped process.
ExecutionWaitResult::Stopped(..) => Self::stopped(),
}
}
}
impl From<std::process::Output> for ExecutionResult {
fn from(output: std::process::Output) -> Self {
if let Some(code) = output.status.code() {
#[expect(clippy::cast_sign_loss)]
return Self::new((code & 0xFF) as u8);
}
#[cfg(unix)]
if let Some(signal) = output.status.signal() {
#[expect(clippy::cast_sign_loss)]
return Self::new((signal & 0xFF) as u8 + 128);
}
tracing::error!("unhandled process exit");
Self::new(127)
}
}
/// Represents an exit code from execution.
#[derive(Clone, Copy, Default)]
pub enum ExecutionExitCode {
/// Indicates successful execution.
#[default]
Success,
/// Indicates a general error.
GeneralError,
/// Indicates invalid usage.
InvalidUsage,
/// Cannot execute the command.
CannotExecute,
/// Indicates a command or similar item was not found.
NotFound,
/// Indicates execution was interrupted.
Interrupted,
/// Indicates a broken pipe (SIGPIPE) was encountered.
BrokenPipe,
/// Indicates unimplemented functionality was encountered.
Unimplemented,
/// A custom exit code.
Custom(u8),
}
impl ExecutionExitCode {
/// Returns whether the exit code indicates success.
pub const fn is_success(&self) -> bool {
matches!(self, Self::Success)
}
}
impl From<u8> for ExecutionExitCode {
fn from(code: u8) -> Self {
match code {
0 => Self::Success,
1 => Self::GeneralError,
2 => Self::InvalidUsage,
99 => Self::Unimplemented,
126 => Self::CannotExecute,
127 => Self::NotFound,
130 => Self::Interrupted,
141 => Self::BrokenPipe,
code => Self::Custom(code),
}
}
}
impl From<ExecutionExitCode> for u8 {
fn from(code: ExecutionExitCode) -> Self {
Self::from(&code)
}
}
impl From<&ExecutionExitCode> for u8 {
fn from(code: &ExecutionExitCode) -> Self {
match code {
ExecutionExitCode::Success => 0,
ExecutionExitCode::GeneralError => 1,
ExecutionExitCode::InvalidUsage => 2,
ExecutionExitCode::Unimplemented => 99,
ExecutionExitCode::CannotExecute => 126,
ExecutionExitCode::NotFound => 127,
ExecutionExitCode::Interrupted => 130,
ExecutionExitCode::BrokenPipe => 141,
ExecutionExitCode::Custom(code) => *code,
}
}
}
/// Represents a control flow transition to apply.
#[derive(Clone, Copy, Default)]
pub enum ExecutionControlFlow {
/// Continue normal execution.
#[default]
Normal,
/// Break out of an enclosing loop.
BreakLoop {
/// Identifies which level of nested loops to break out of. 0 indicates the innermost loop,
/// 1 indicates the next outer loop, and so on.
levels: usize,
},
/// Continue to the next iteration of an enclosing loop.
ContinueLoop {
/// Identifies which level of nested loops to continue. 0 indicates the innermost loop,
/// 1 indicates the next outer loop, and so on.
levels: usize,
},
/// Return from the current function or script.
ReturnFromFunctionOrScript,
/// Exit the shell.
ExitShell,
}
impl ExecutionControlFlow {
/// Attempts to decrement the loop levels for `BreakLoop` or `ContinueLoop`.
/// If the levels reach zero, transitions to `Normal`. If the control flow is not
/// a loop break or continue, no changes are made.
#[must_use]
pub const fn try_decrement_loop_levels(&self) -> Self {
match self {
Self::BreakLoop { levels: 0 } | Self::ContinueLoop { levels: 0 } => Self::Normal,
Self::BreakLoop { levels } => Self::BreakLoop {
levels: *levels - 1,
},
Self::ContinueLoop { levels } => Self::ContinueLoop {
levels: *levels - 1,
},
control_flow => *control_flow,
}
}
}
/// Represents the result of spawning an execution; captures both execution
/// that immediately returns as well as execution that starts a process
/// asynchronously.
pub enum ExecutionSpawnResult {
/// Indicates that the execution completed.
Completed(ExecutionResult),
/// Indicates that a process was started and had not yet completed.
StartedProcess(processes::ChildProcess),
/// Indicates that a task was started to handle the execution asynchronously.
StartedTask(tokio::task::JoinHandle<Result<ExecutionResult, error::Error>>),
}
impl From<ExecutionResult> for ExecutionSpawnResult {
fn from(result: ExecutionResult) -> Self {
Self::Completed(result)
}
}
impl ExecutionSpawnResult {
/// Waits for the command to complete.
pub async fn wait(self) -> Result<ExecutionWaitResult, error::Error> {
let result = match self {
Self::StartedProcess(mut child) => {
// Wait for the process to exit or for a relevant signal, whichever happens
// first.
match child.wait().await? {
processes::ProcessWaitResult::Completed(output) => {
ExecutionWaitResult::Completed(ExecutionResult::from(output))
}
processes::ProcessWaitResult::Stopped => ExecutionWaitResult::Stopped(child),
}
}
Self::Completed(result) => ExecutionWaitResult::Completed(result),
Self::StartedTask(join_handle) => {
let result = join_handle.await?;
ExecutionWaitResult::Completed(result?)
}
};
Ok(result)
}
pub(crate) async fn poll(self) -> Result<ExecutionWaitResult, error::Error> {
let result = match self {
Self::StartedProcess(child) => ExecutionWaitResult::Stopped(child),
Self::Completed(result) => ExecutionWaitResult::Completed(result),
Self::StartedTask(join_handle) => {
// TODO(jobs): This isn't right.
let result = join_handle.await?;
ExecutionWaitResult::Completed(result?)
}
};
Ok(result)
}
}
/// Represents the result of waiting for an execution to complete.
pub enum ExecutionWaitResult {
/// Indicates that the execution completed.
Completed(ExecutionResult),
/// Indicates that the execution was stopped.
Stopped(processes::ChildProcess),
}
+550
View File
@@ -0,0 +1,550 @@
//! Module defining the core shell structure and behavior.
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::{
ExecutionControlFlow, ExecutionResult, builtins, env::ShellEnvironment, error, extensions,
functions, interfaces, jobs, keywords, openfiles, options::RuntimeOptions, pathcache,
wellknownvars,
};
/// Type for storing a key bindings helper.
pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;
/// Type alias for shell file descriptors.
pub type ShellFd = i32;
// NOTE: The submodule files below (e.g., `shell/traps.rs`, `shell/callstack.rs`) contain
// `impl Shell<SE>` blocks that provide methods coordinating with types defined in the
// corresponding top-level modules (e.g., `traps.rs`, `callstack.rs`). This is an intentional
// layered architecture: top-level modules define domain types and data structures, while
// shell/ submodules implement Shell methods that operate on those types.
mod builder;
mod builtin_registry;
mod callstack;
mod completion;
mod env;
mod execution;
mod expansion;
mod fs;
mod funcs;
mod history;
mod initscripts;
mod io;
mod job_control;
mod parsing;
mod prompts;
mod readline;
mod state;
mod traps;
pub use builder::{CreateOptions, ShellBuilder, ShellBuilderState};
pub use initscripts::{ProfileLoadBehavior, RcLoadBehavior};
pub use state::ShellState;
/// Represents an instance of a shell.
///
/// # Type Parameters
///
/// * `SE` - The shell extensions implementation to use. These extensions are statically injected
/// into the shell at compile time to provide custom behavior. When unspecified, defaults to
/// `DefaultShellExtensions`, which provide standard behavior.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Shell<SE: extensions::ShellExtensions = extensions::DefaultShellExtensions> {
/// Injected error behavior.
#[cfg_attr(feature = "serde", serde(skip, default = "default_error_formatter"))]
error_formatter: SE::ErrorFormatter,
/// Trap handler configuration for the shell.
traps: crate::traps::TrapHandlerConfig,
/// Manages files opened and accessible via redirection operators.
open_files: openfiles::OpenFiles,
/// The current working directory.
working_dir: PathBuf,
/// The shell environment, containing shell variables.
env: ShellEnvironment,
/// Shell function definitions.
funcs: functions::FunctionEnv,
/// Runtime shell options.
options: RuntimeOptions,
/// State of managed jobs.
/// TODO(serde): Need to warn somehow that jobs cannot be serialized.
#[cfg_attr(feature = "serde", serde(skip))]
jobs: jobs::JobManager,
/// Shell aliases.
aliases: HashMap<String, String>,
/// The status of the last completed command.
last_exit_status: u8,
/// Tracks changes to `last_exit_status`.
last_exit_status_change_count: usize,
/// The status of each of the commands in the last pipeline.
last_pipeline_statuses: Vec<u8>,
/// Clone depth from the original ancestor shell.
depth: usize,
/// Shell name
name: Option<String>,
/// Positional shell arguments (not including shell name).
args: Vec<String>,
/// Shell version
version: Option<String>,
/// Detailed display string for the shell
product_display_str: Option<String>,
/// Function/script call stack.
call_stack: crate::callstack::CallStack,
/// Directory stack used by pushd et al.
directory_stack: Vec<PathBuf>,
/// Completion configuration.
completion_config: crate::completion::Config,
/// Shell built-in commands.
#[cfg_attr(feature = "serde", serde(skip))]
builtins: HashMap<String, builtins::Registration<SE>>,
/// Shell program location cache.
program_location_cache: pathcache::PathCache,
/// Last "SECONDS" captured time.
last_stopwatch_time: std::time::SystemTime,
/// Last "SECONDS" offset requested.
last_stopwatch_offset: u32,
/// Parser implementation to use.
#[cfg_attr(feature = "serde", serde(skip))]
parser_impl: crate::parser::ParserImpl,
/// Key bindings for the shell, optionally implemented by an interactive shell.
#[cfg_attr(feature = "serde", serde(skip))]
key_bindings: Option<KeyBindingsHelper>,
/// History of commands executed in the shell.
history: Option<crate::history::History>,
}
impl<SE: extensions::ShellExtensions> Clone for Shell<SE> {
fn clone(&self) -> Self {
Self {
error_formatter: self.error_formatter.clone(),
traps: self.traps.clone(),
open_files: self.open_files.clone(),
working_dir: self.working_dir.clone(),
env: self.env.clone(),
funcs: self.funcs.clone(),
options: self.options.clone(),
jobs: jobs::JobManager::new(),
aliases: self.aliases.clone(),
last_exit_status: self.last_exit_status,
last_exit_status_change_count: self.last_exit_status_change_count,
last_pipeline_statuses: self.last_pipeline_statuses.clone(),
name: self.name.clone(),
args: self.args.clone(),
version: self.version.clone(),
product_display_str: self.product_display_str.clone(),
call_stack: {
// Subshells must not inherit the parent's "currently handling signal X"
// state; otherwise a trap handler that spawns a subshell would see itself
// as already inside that handler and skip re-entrant delivery.
let mut cs = self.call_stack.clone();
cs.clear_active_trap_signals();
cs
},
directory_stack: self.directory_stack.clone(),
completion_config: self.completion_config.clone(),
builtins: self.builtins.clone(),
program_location_cache: self.program_location_cache.clone(),
last_stopwatch_time: self.last_stopwatch_time,
last_stopwatch_offset: self.last_stopwatch_offset,
parser_impl: self.parser_impl,
key_bindings: self.key_bindings.clone(),
history: self.history.clone(),
depth: self.depth + 1,
}
}
}
impl<SE: extensions::ShellExtensions> AsRef<Self> for Shell<SE> {
fn as_ref(&self) -> &Self {
self
}
}
impl<SE: extensions::ShellExtensions> AsMut<Self> for Shell<SE> {
fn as_mut(&mut self) -> &mut Self {
self
}
}
impl<SE: extensions::ShellExtensions> Shell<SE> {
/// Returns a new shell instance created with the given options.
/// Does *not* load any configuration files (e.g., bashrc).
///
/// # Arguments
///
/// * `options` - The options to use when creating the shell.
pub(crate) fn new(options: CreateOptions<SE>) -> Result<Self, error::Error> {
// Compute runtime options before moving fields out of `options`.
let runtime_options = RuntimeOptions::defaults_from(&options);
// Instantiate the shell with some defaults.
let mut shell = Self {
error_formatter: options.error_formatter,
open_files: openfiles::OpenFiles::new(),
options: runtime_options,
name: options.shell_name,
args: options.shell_args.unwrap_or_default(),
version: options.shell_version,
product_display_str: options.shell_product_display_str,
working_dir: options.working_dir.map_or_else(std::env::current_dir, Ok)?,
builtins: options.builtins,
parser_impl: options.parser,
key_bindings: options.key_bindings,
..Self::default()
};
// Add in any open files provided.
shell.open_files.update_from(options.fds.into_iter());
// TODO(patterns): Without this a script that sets extglob will fail because we
// parse the entire script with the same settings.
shell.options.extended_globbing = true;
// If requested, seed parameters from environment.
if !options.do_not_inherit_env {
wellknownvars::inherit_env_vars(&mut shell)?;
}
// If requested, set well-known variables.
if !options.skip_well_known_vars {
wellknownvars::init_well_known_vars(&mut shell)?;
}
// Set any provided variables.
for (var_name, var_value) in options.vars {
shell.env.set_global(var_name, var_value)?;
}
// Set up history, if relevant. Do NOT fail if we can't load history.
if shell.options.enable_command_history {
shell.history = shell
.load_history()
.unwrap_or_default()
.or_else(|| Some(crate::history::History::default()));
}
Ok(shell)
}
}
impl<SE: extensions::ShellExtensions> Shell<SE> {
/// Increments the interactive line offset in the shell by the indicated number
/// of lines.
///
/// # Arguments
///
/// * `delta` - The number of lines to increment the current line offset by.
pub fn increment_interactive_line_offset(&mut self, delta: usize) {
self.call_stack.increment_current_line_offset(delta);
}
/// Updates the currently executing command in the shell.
pub fn set_current_cmd(&mut self, cmd: &impl brush_parser::ast::Node) {
self.call_stack
.set_current_pos(cmd.location().map(|span| span.start));
}
/// Updates the `$_` shell variable (last-argument of the previous simple
/// command).
///
/// Passes `Some(last_arg)` to record the last argument of the just-executed
/// command, or `None` to clear `$_` (used for assignment-only statements,
/// which bash treats as having no "last argument").
///
/// The update is applied in-place so that attributes on `_` (notably
/// `readonly`) are preserved: attempting to update a readonly `_` is a
/// silent no-op, matching bash's observable stdout behavior.
pub(crate) fn update_last_arg_variable(&mut self, last_arg: Option<String>) {
// Bash refuses to update a readonly `_`, emitting an error to stderr
// on each attempt. We silently skip the update here — the observable
// stdout effect ($_ stays unchanged) matches bash; the missing stderr
// diagnostics are harmless.
if self
.env
.get_using_policy("_", crate::env::EnvironmentLookup::Anywhere)
.is_some_and(|v| v.is_readonly())
{
return;
}
// Replace the variable entirely (fresh, non-exported). This matches
// bash, which never exports `_` — even under `set -a` — and always
// clears any previously-set attributes (except readonly, handled
// above).
let value = last_arg.unwrap_or_default();
let _ = self
.env
.set_global("_", crate::variables::ShellVariable::new(value));
}
/// Applies errexit semantics to a result if enabled and appropriate.
/// This should be called at "statement boundaries" where errexit should be checked.
///
/// # Arguments
///
/// * `result` - The execution result to potentially modify.
pub const fn apply_errexit_if_enabled(&self, result: &mut ExecutionResult) {
if self.options.exit_on_nonzero_command_exit
&& !result.is_success()
&& result.is_normal_flow()
{
result.next_control_flow = ExecutionControlFlow::ExitShell;
}
}
/// Returns the keywords that are reserved by the shell.
pub(crate) fn get_keywords(&self) -> Vec<&str> {
if self.options.sh_mode {
keywords::SH_MODE_KEYWORDS.iter().copied().collect()
} else {
keywords::KEYWORDS.iter().copied().collect()
}
}
/// Checks if the given string is a keyword reserved in this shell.
///
/// # Arguments
///
/// * `s` - The string to check.
pub fn is_keyword(&self, s: &str) -> bool {
if self.options.sh_mode {
keywords::SH_MODE_KEYWORDS.contains(s)
} else {
keywords::KEYWORDS.contains(s)
}
}
pub(crate) const fn last_exit_status_change_count(&self) -> usize {
self.last_exit_status_change_count
}
}
#[inherent::inherent]
impl<SE: extensions::ShellExtensions> ShellState for Shell<SE> {
/// Returns whether or not this shell is a subshell.
pub fn is_subshell(&self) -> bool {
self.depth > 0
}
/// Returns the last "SECONDS" captured time.
pub fn last_stopwatch_time(&self) -> std::time::SystemTime {
self.last_stopwatch_time
}
/// Returns the last "SECONDS" offset requested.
pub fn last_stopwatch_offset(&self) -> u32 {
self.last_stopwatch_offset
}
/// Returns the shell environment containing variables.
pub fn env(&self) -> &ShellEnvironment {
&self.env
}
/// Returns a mutable reference to the shell environment.
pub fn env_mut(&mut self) -> &mut ShellEnvironment {
&mut self.env
}
/// Returns the shell's runtime options.
pub fn options(&self) -> &RuntimeOptions {
&self.options
}
/// Returns a mutable reference to the shell's runtime options.
pub fn options_mut(&mut self) -> &mut RuntimeOptions {
&mut self.options
}
/// Returns the shell's aliases.
pub fn aliases(&self) -> &HashMap<String, String> {
&self.aliases
}
/// Returns a mutable reference to the shell's aliases.
pub fn aliases_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.aliases
}
/// Returns the shell's job manager.
pub fn jobs(&self) -> &jobs::JobManager {
&self.jobs
}
/// Returns a mutable reference to the shell's job manager.
pub fn jobs_mut(&mut self) -> &mut jobs::JobManager {
&mut self.jobs
}
/// Returns the shell's trap handler configuration.
pub fn traps(&self) -> &crate::traps::TrapHandlerConfig {
&self.traps
}
/// Returns a mutable reference to the shell's trap handler configuration.
pub fn traps_mut(&mut self) -> &mut crate::traps::TrapHandlerConfig {
&mut self.traps
}
/// Returns the shell's directory stack.
pub fn directory_stack(&self) -> &[PathBuf] {
&self.directory_stack
}
/// Returns a mutable reference to the shell's directory stack.
pub fn directory_stack_mut(&mut self) -> &mut Vec<PathBuf> {
&mut self.directory_stack
}
/// Returns the statuses of commands in the last pipeline.
pub fn last_pipeline_statuses(&self) -> &[u8] {
&self.last_pipeline_statuses
}
/// Returns a mutable reference to the statuses of commands in the last pipeline.
pub fn last_pipeline_statuses_mut(&mut self) -> &mut Vec<u8> {
&mut self.last_pipeline_statuses
}
/// Returns the shell's program location cache.
pub fn program_location_cache(&self) -> &pathcache::PathCache {
&self.program_location_cache
}
/// Returns a mutable reference to the shell's program location cache.
pub fn program_location_cache_mut(&mut self) -> &mut pathcache::PathCache {
&mut self.program_location_cache
}
/// Returns the shell's completion configuration.
pub fn completion_config(&self) -> &crate::completion::Config {
&self.completion_config
}
/// Returns a mutable reference to the shell's completion configuration.
pub fn completion_config_mut(&mut self) -> &mut crate::completion::Config {
&mut self.completion_config
}
/// Returns the shell's open files.
pub fn open_files(&self) -> &openfiles::OpenFiles {
&self.open_files
}
/// Returns a mutable reference to the shell's open files.
pub fn open_files_mut(&mut self) -> &mut openfiles::OpenFiles {
&mut self.open_files
}
/// Returns the *current* name of the shell ($0).
/// Influenced by the current call stack.
pub fn current_shell_name(&self) -> Option<Cow<'_, str>> {
for frame in self.call_stack.iter() {
// Executed scripts shadow the shell name.
if frame.frame_type.is_run_script() {
return Some(frame.frame_type.name());
}
}
self.name.as_deref().map(|name| name.into())
}
/// Returns the current subshell depth; 0 is returned if this shell is not a subshell.
pub fn depth(&self) -> usize {
self.depth
}
/// Returns the call stack for the shell.
pub fn call_stack(&self) -> &crate::callstack::CallStack {
&self.call_stack
}
/// Returns the shell's history, if it exists.
pub fn history(&self) -> Option<&crate::history::History> {
self.history.as_ref()
}
/// Returns a mutable reference to the shell's history, if it exists.
pub fn history_mut(&mut self) -> Option<&mut crate::history::History> {
self.history.as_mut()
}
/// Returns the shell's official version string (if available).
pub fn version(&self) -> Option<&str> {
self.version.as_deref()
}
/// Returns the exit status of the last command executed in this shell.
pub fn last_exit_status(&self) -> u8 {
self.last_exit_status
}
/// Updates the last exit status.
pub fn set_last_exit_status(&mut self, status: u8) {
self.last_exit_status = status;
self.last_exit_status_change_count += 1;
}
/// Returns the key bindings helper for the shell.
pub fn key_bindings(&self) -> Option<&KeyBindingsHelper> {
self.key_bindings.as_ref()
}
/// Sets the key bindings helper for the shell.
pub fn set_key_bindings(&mut self, key_bindings: Option<KeyBindingsHelper>) {
self.key_bindings = key_bindings;
}
/// Returns the shell's current working directory.
pub fn working_dir(&self) -> &Path {
&self.working_dir
}
/// Returns a mutable reference to the shell's current working directory.
/// This is only accessible within the crate.
pub(crate) fn working_dir_mut(&mut self) -> &mut PathBuf {
&mut self.working_dir
}
/// Returns the product display name for this shell.
pub fn product_display_str(&self) -> Option<&str> {
self.product_display_str.as_deref()
}
}
#[cfg(feature = "serde")]
fn default_error_formatter<EF: extensions::ErrorFormatter>() -> EF {
EF::default()
}
+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,
}
}
}
+44
View File
@@ -0,0 +1,44 @@
//! Source info.
use std::{path::PathBuf, sync::Arc};
/// Source context.
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SourceInfo {
/// The name of the source.
pub source: String,
/// Optionally indicates a starting location after the beginning of the source.
/// If `None`, the start is the beginning of the source.
pub start: Option<Arc<crate::SourcePosition>>,
}
impl From<&str> for SourceInfo {
fn from(source: &str) -> Self {
Self {
source: source.to_owned(),
start: None,
}
}
}
impl From<PathBuf> for SourceInfo {
fn from(path: PathBuf) -> Self {
Self {
source: path.to_string_lossy().to_string(),
start: None,
}
}
}
impl std::fmt::Display for SourceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.source)?;
if let Some(pos) = &self.start {
write!(f, ":{},{}", pos.line, pos.column)?;
}
Ok(())
}
}
+43
View File
@@ -0,0 +1,43 @@
//! Platform abstraction facilities
#![allow(unused)]
#[cfg(unix)]
pub(crate) mod unix;
#[cfg(unix)]
pub(crate) use unix as platform;
#[cfg(windows)]
pub(crate) mod windows;
#[cfg(windows)]
pub(crate) use windows as platform;
#[cfg(target_family = "wasm")]
pub(crate) mod wasm;
#[cfg(target_family = "wasm")]
pub(crate) use wasm as platform;
#[cfg(not(unix))]
pub(crate) mod stubs;
#[cfg(any(unix, windows))]
pub(crate) mod hostname;
#[cfg(any(unix, windows))]
pub mod tokio_process;
pub mod fs;
pub use platform::async_pipe;
pub use platform::commands;
pub(crate) use platform::env;
pub use platform::fd;
pub use platform::input;
pub(crate) use platform::network;
pub use platform::poll;
pub use platform::process;
pub use platform::resource;
pub use platform::signal;
pub use platform::terminal;
pub(crate) use platform::users;
pub use platform::PlatformError;
+37
View File
@@ -0,0 +1,37 @@
//! Filesystem utilities
pub use super::platform::fs::*;
/// Extension trait for path-related filesystem operations.
pub trait PathExt {
/// Returns true if the path exists and is readable by the current user.
fn readable(&self) -> bool;
/// Returns true if the path exists and is writable by the current user.
fn writable(&self) -> bool;
/// Returns true if the path exists and is executable by the current user.
///
/// On Windows, this returns true if *either* the path itself is a file with
/// a `PATHEXT` extension *or* appending some `PATHEXT` extension resolves
/// to an existing file. To recover the actual on-disk path in the
/// latter case, use [`resolve_executable`] which takes ownership
/// and avoids copies on platforms where no resolution is needed.
fn executable(&self) -> bool;
/// Returns true if the path exists and is a block device.
fn exists_and_is_block_device(&self) -> bool;
/// Returns true if the path exists and is a character device.
fn exists_and_is_char_device(&self) -> bool;
/// Returns true if the path exists and is a FIFO (named pipe).
fn exists_and_is_fifo(&self) -> bool;
/// Returns true if the path exists and is a socket.
fn exists_and_is_socket(&self) -> bool;
/// Returns true if the path exists and has the setgid bit set.
fn exists_and_is_setgid(&self) -> bool;
/// Returns true if the path exists and has the setuid bit set.
fn exists_and_is_setuid(&self) -> bool;
/// Returns true if the path exists and has the sticky bit set.
fn exists_and_is_sticky_bit(&self) -> bool;
/// Returns the device ID and inode number for the path.
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error>;
}
+3
View File
@@ -0,0 +1,3 @@
pub(crate) fn get() -> std::io::Result<std::ffi::OsString> {
hostname::get()
}
+22
View File
@@ -0,0 +1,22 @@
#![allow(dead_code)]
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::needless_pass_by_ref_mut)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::unnecessary_wraps)]
#![allow(clippy::unused_async)]
#![allow(clippy::unused_self)]
pub mod async_pipe;
pub mod commands;
pub(crate) mod env;
pub mod fd;
pub mod fs;
pub mod input;
pub(crate) mod network;
pub(crate) mod pipes;
pub mod poll;
pub mod process;
pub mod resource;
pub mod signal;
pub mod terminal;
pub(crate) mod users;
+30
View File
@@ -0,0 +1,30 @@
//! Async pipe reading utilities for non-Unix platforms.
//!
//! Uses `spawn_blocking` internally for the I/O operation only,
//! not for the entire subshell execution.
use std::io::{self, Read};
pub(crate) struct AsyncPipeReader {
inner: Option<std::io::PipeReader>,
}
impl AsyncPipeReader {
pub(crate) fn new(fd: std::io::PipeReader) -> io::Result<Self> {
Ok(Self { inner: Some(fd) })
}
pub(crate) async fn read_to_string(&mut self) -> io::Result<String> {
let Some(reader) = self.inner.take() else {
return Ok(String::new());
};
tokio::task::spawn_blocking(move || {
let mut s = String::new();
{ reader }.read_to_string(&mut s)?;
Ok(s)
})
.await
.map_err(io::Error::other)?
}
}
+95
View File
@@ -0,0 +1,95 @@
//! Command execution utilities.
use std::ffi::OsStr;
use crate::{ShellFd, error, openfiles};
/// Extension trait for Unix-like command extensions.
pub trait CommandExt {
/// Sets the zeroth argument (argv[0]) of the command.
///
/// # Arguments
///
/// * `arg` - The argument to set as argv[0].
fn arg0<S>(&mut self, arg: S) -> &mut Self
where
S: AsRef<OsStr>;
/// Sets the process group ID of the command.
///
/// # Arguments
///
/// * `pgroup` - The process group ID to set.
fn process_group(&mut self, pgroup: i32) -> &mut Self;
}
impl CommandExt for std::process::Command {
fn arg0<S>(&mut self, _arg: S) -> &mut Self
where
S: AsRef<OsStr>,
{
// NOTE: no-op.
self
}
fn process_group(&mut self, _pgroup: i32) -> &mut Self {
// NOTE: no-op.
self
}
}
/// Extension trait for Unix-like exit status extensions.
pub trait ExitStatusExt {
/// Returns the signal that terminated the process, if any.
fn signal(&self) -> Option<i32>;
}
impl ExitStatusExt for std::process::ExitStatus {
fn signal(&self) -> Option<i32> {
None
}
}
/// Extension trait for injecting file descriptors into commands.
pub trait CommandFdInjectionExt {
/// Injects the given open files as file descriptors into the command.
///
/// # Arguments
///
/// * `open_files` - A mapping of child file descriptors to open files.
fn inject_fds(
&mut self,
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
) -> Result<(), error::Error>;
}
impl CommandFdInjectionExt for std::process::Command {
fn inject_fds(
&mut self,
mut open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
) -> Result<(), error::Error> {
if open_files.next().is_some() {
return Err(error::ErrorKind::NotSupportedOnThisPlatform("fd redirections").into());
}
Ok(())
}
}
/// Extension trait for arranging for commands to take the foreground.
pub trait CommandFgControlExt {
/// Arranges for the command to take the foreground when it is executed.
fn take_foreground(&mut self);
/// Arranges for the command to become a session leader when it is executed.
fn lead_session(&mut self);
}
impl CommandFgControlExt for std::process::Command {
fn take_foreground(&mut self) {
// NOTE: This is a no-op.
}
fn lead_session(&mut self) {
// NOTE: This is a no-op.
}
}
+8
View File
@@ -0,0 +1,8 @@
//! Environment variable retrieval (stub implementation).
/// Retrieves environment variables from the host process.
///
/// Stub implementation that returns no variables.
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
std::iter::empty()
}
+13
View File
@@ -0,0 +1,13 @@
//! File descriptor utilities.
use crate::{ShellFd, error, openfiles};
/// Stub implementation for platforms that do not support enumerating file descriptors.
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
std::iter::empty()
}
/// Stub implementation for platforms that do not support opening file descriptors.
pub fn try_get_file_for_open_fd(_fd: ShellFd) -> Option<openfiles::OpenFile> {
None
}
+136
View File
@@ -0,0 +1,136 @@
//! Filesystem utilities (stubs).
use crate::error;
pub(crate) trait MetadataExt {
fn gid(&self) -> u32 {
0
}
fn uid(&self) -> u32 {
0
}
}
impl MetadataExt for std::fs::Metadata {}
pub(crate) fn get_default_executable_search_paths() -> Vec<std::path::PathBuf> {
vec![]
}
/// Returns the default paths where standard Unix utilities are typically installed.
/// This is a stub implementation that returns an empty vector.
pub fn get_default_standard_utils_paths() -> Vec<std::path::PathBuf> {
vec![]
}
/// Opens a null file that will discard all I/O.
///
/// This is a stub implementation that returns an error.
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("opening null file").into())
}
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
//
// This is a stub implementation that returns no result.
pub fn try_open_special_file(
_path: &std::path::Path,
) -> Option<Result<std::fs::File, std::io::Error>> {
None
}
/// Returns the path to the system-wide shell profile script.
///
/// Stub implementation that returns `None`.
pub fn get_system_profile_path() -> Option<&'static std::path::Path> {
None
}
/// Returns the path to the system-wide shell rc script.
///
/// Stub implementation that returns `None`.
pub fn get_system_rc_path() -> Option<&'static std::path::Path> {
None
}
/// Returns the platform default for case-insensitive pathname expansion.
///
/// In the stub implementation, this returns `false`.
pub const fn default_case_insensitive_path_expansion() -> bool {
false
}
/// Returns true if the string contains a path separator character.
///
/// In the stub implementation, only `/` is considered a path separator.
pub fn contains_path_separator(s: &str) -> bool {
s.contains('/')
}
/// Returns true if the string ends with a path separator character.
///
/// In the stub implementation, only `/` is considered a path separator.
pub fn ends_with_path_separator(s: &str) -> bool {
s.ends_with('/')
}
/// Returns the string with a trailing path separator removed, if present.
///
/// In the stub implementation, only `/` is considered a path separator.
pub fn strip_path_separator_suffix(s: &str) -> &str {
s.strip_suffix('/').unwrap_or(s)
}
/// Finds the byte index of the last path separator in the string.
///
/// In the stub implementation, only `/` is considered a path separator.
pub fn rfind_path_separator(s: &str) -> Option<usize> {
s.rfind('/')
}
/// Splits a string on path separator characters, returning an iterator of components.
///
/// In the stub implementation, only `/` is used as a separator.
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
s.split('/')
}
/// Returns the root path for an absolute pattern, if the first component indicates one.
///
/// In the stub implementation, an empty first component indicates an absolute path.
pub fn pattern_path_root(first_component: &str) -> Option<std::path::PathBuf> {
if first_component.is_empty() {
Some(std::path::PathBuf::from("/"))
} else {
None
}
}
/// Pushes a component onto a path for pattern expansion.
///
/// In the stub implementation, this delegates directly to `PathBuf::push`.
pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
path.push(component);
}
/// Normalizes path separators for shell output.
///
/// In the stub implementation, this is a no-op.
pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Borrowed(s)
}
/// Resolves an owned path to the actual on-disk executable file, if any.
///
/// In the stub implementation, returns the path unchanged if it is
/// executable (per the stub `PathExt`, which considers every path
/// executable).
pub fn resolve_executable(path: std::path::PathBuf) -> Option<std::path::PathBuf> {
use crate::sys::fs::PathExt;
if path.as_path().executable() {
Some(path)
} else {
None
}
}
+16
View File
@@ -0,0 +1,16 @@
//! Terminal input utilities
use crate::{error, interfaces};
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
/// if the key code is not recognized.
///
/// This is a stub implementation that recognizes single-byte non-control
/// characters but does not support terminal-specific key sequences.
pub fn try_get_key_from_key_code(key_code: &[u8]) -> Option<interfaces::Key> {
if key_code.len() == 1 && !key_code[0].is_ascii_control() {
Some(interfaces::Key::Character(key_code[0] as char))
} else {
None
}
}
+3
View File
@@ -0,0 +1,3 @@
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
Ok("".into())
}
+53
View File
@@ -0,0 +1,53 @@
/// Stub implementation of a pipe reader.
#[derive(Clone)]
pub(crate) struct PipeReader {}
impl PipeReader {
/// Tries to clone the reader.
pub fn try_clone(&self) -> std::io::Result<Self> {
Ok((*self).clone())
}
}
impl From<PipeReader> for std::process::Stdio {
fn from(_reader: PipeReader) -> Self {
Self::null()
}
}
impl std::io::Read for PipeReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Ok(0)
}
}
/// Stub implementation o a pipe writer.
#[derive(Clone)]
pub(crate) struct PipeWriter {}
impl PipeWriter {
/// Tries to clone the writer.
pub fn try_clone(&self) -> std::io::Result<Self> {
Ok((*self).clone())
}
}
impl From<PipeWriter> for std::process::Stdio {
fn from(_writer: PipeWriter) -> Self {
Self::null()
}
}
impl std::io::Write for PipeWriter {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Ok(0)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub(crate) fn pipe() -> std::io::Result<(PipeReader, PipeWriter)> {
Ok((PipeReader {}, PipeWriter {}))
}
+15
View File
@@ -0,0 +1,15 @@
//! Stub file descriptor polling utilities for platforms without poll support.
use std::time::Duration;
use crate::openfiles::OpenFile;
/// Stub implementation that always returns an unsupported error.
///
/// Timeout-based reading is not supported on this platform.
pub fn poll_for_input(_file: &OpenFile, _timeout: Duration) -> std::io::Result<bool> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"poll-based timeout is not supported on this platform",
))
}
+34
View File
@@ -0,0 +1,34 @@
//! Process management utilities
pub(crate) type ProcessId = i32;
/// Provides access to a child process.
pub struct Child {
inner: std::process::Child,
}
pub(crate) use std::process::ExitStatus;
pub(crate) use std::process::Output;
impl Child {
/// Returns the process ID of the child process, if available.
pub fn id(&self) -> Option<u32> {
None
}
/// Asynchronously waits for the child process to exit.
pub async fn wait(&mut self) -> std::io::Result<ExitStatus> {
self.inner.wait()
}
/// Asynchronously waits for the child process to exit and collects its
/// output.
pub async fn wait_with_output(self) -> std::io::Result<Output> {
self.inner.wait_with_output()
}
}
pub(crate) fn spawn(mut command: std::process::Command) -> std::io::Result<Child> {
let child = command.spawn()?;
Ok(Child { inner: child })
}
+19
View File
@@ -0,0 +1,19 @@
//! Signal processing utilities
use crate::error;
/// Returns the user and system CPU time used by the current process.
///
/// This is a stub implementation that returns zero durations.
pub fn get_self_user_and_system_time()
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
}
/// Returns the user and system CPU time used by child processes.
///
/// This is a stub implementation that returns zero durations.
pub fn get_children_user_and_system_time()
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
Ok((std::time::Duration::ZERO, std::time::Duration::ZERO))
}
+84
View File
@@ -0,0 +1,84 @@
//! Signal processing utilities
use crate::{error, sys, traps};
/// A stub enum representing system signals on unsupported platforms.
#[allow(unnameable_types)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Signal {}
impl Signal {
/// Returns an iterator over all possible signals.
pub fn iterator() -> impl Iterator<Item = Self> {
std::iter::empty()
}
/// Converts the signal into its corresponding name as a `&'static str`.
pub const fn as_str(self) -> &'static str {
""
}
/// Creates a `Signal` from a string representation.
pub fn from_str(s: &str) -> Result<Self, error::Error> {
Err(error::ErrorKind::InvalidSignal(s.into()).into())
}
}
impl TryFrom<i32> for Signal {
type Error = error::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
Err(error::ErrorKind::InvalidSignal(std::format!("{value}")).into())
}
}
pub(crate) fn continue_process(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("continuing process").into())
}
/// Sends a signal to a specific process.
///
/// This is a stub implementation that returns an error.
pub fn kill_process(
_pid: sys::process::ProcessId,
_signal: traps::TrapSignal,
) -> Result<(), error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("killing process").into())
}
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
Ok(())
}
pub(crate) struct FakeSignal {}
impl FakeSignal {
fn new() -> Self {
Self {}
}
pub async fn recv(&self) {
futures::future::pending::<()>().await;
}
}
pub(crate) fn tstp_signal_listener() -> Result<FakeSignal, error::Error> {
Ok(FakeSignal::new())
}
pub(crate) fn chld_signal_listener() -> Result<FakeSignal, error::Error> {
Ok(FakeSignal::new())
}
pub(crate) async fn await_ctrl_c() -> std::io::Result<()> {
FakeSignal::new().recv().await;
Ok(())
}
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
Ok(())
}
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
Ok(false)
}
+79
View File
@@ -0,0 +1,79 @@
//! Terminal utilities.
use crate::{error, openfiles, sys, terminal};
/// Terminal configuration.
#[derive(Clone, Debug)]
pub struct Config;
#[allow(clippy::unused_self)]
impl Config {
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
/// with the given file descriptor.
///
/// # Arguments
///
/// * `_file` - A reference to the open terminal.
pub fn from_term(_file: &openfiles::OpenFile) -> Result<Self, error::Error> {
Ok(Self)
}
/// Applies the terminal settings to the terminal associated with the given file descriptor.
///
/// # Arguments
///
/// * `_file` - A reference to the open terminal.
pub fn apply_to_term(&self, _file: &openfiles::OpenFile) -> Result<(), error::Error> {
Ok(())
}
/// Applies the given high-level terminal settings to this configuration. Does not modify any
/// terminal itself.
///
/// # Arguments
///
/// * `_settings` - The high-level terminal settings to apply to this configuration.
pub fn update(&mut self, _settings: &terminal::Settings) {}
}
/// Get the process ID of this process's parent.
///
/// This is a stub implementation that returns `None`.
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
None
}
/// Get the process group ID for this process's process group.
///
/// This is a stub implementation that returns `None`.
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
None
}
/// Get the foreground process ID of the attached terminal.
///
/// This is a stub implementation that returns `None`.
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
None
}
/// Move the specified process to the foreground of the attached terminal.
///
/// This is a stub implementation that takes no action.
pub fn move_to_foreground(_pid: sys::process::ProcessId) -> Result<(), error::Error> {
Ok(())
}
/// Moves the current process to the foreground of the attached terminal.
///
/// This is a stub implementation that returns `None`.
pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
Ok(())
}
/// Tries to get the path of the terminal device associated with the attached terminal.
///
/// This is a stub implementation that always returns `None`.
pub fn try_get_terminal_device_path() -> Option<std::path::PathBuf> {
None
}
+50
View File
@@ -0,0 +1,50 @@
use crate::error;
use std::path::PathBuf;
pub(crate) fn get_user_home_dir(_username: &str) -> Option<PathBuf> {
None
}
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
std::env::home_dir()
}
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
None
}
pub(crate) fn is_root() -> bool {
false
}
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current uid").into())
}
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current gid").into())
}
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective uid").into())
}
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting effective gid").into())
}
pub(crate) fn get_current_username() -> Result<String, error::Error> {
Err(error::ErrorKind::NotSupportedOnThisPlatform("getting current username").into())
}
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
Ok(vec![])
}
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
Ok(vec![])
}
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
Ok(vec![])
}
+9
View File
@@ -0,0 +1,9 @@
//! Process management utilities
pub(crate) type ProcessId = i32;
pub(crate) use tokio::process::Child;
pub(crate) fn spawn(command: std::process::Command) -> std::io::Result<Child> {
let mut command = tokio::process::Command::from(command);
command.spawn()
}
+28
View File
@@ -0,0 +1,28 @@
pub mod async_pipe;
pub mod commands;
pub(crate) mod env;
pub mod fd;
pub mod fs;
pub mod input;
pub(crate) mod network;
pub mod poll;
use crate::error;
pub use crate::sys::tokio_process as process;
pub mod resource;
pub mod signal;
pub mod terminal;
pub(crate) mod users;
/// Platform-specific errors.
#[derive(Debug, thiserror::Error)]
pub enum PlatformError {
/// A system error occurred.
#[error("system error: {0}")]
ErrnoError(#[from] nix::errno::Errno),
}
impl From<nix::errno::Errno> for error::ErrorKind {
fn from(err: nix::errno::Errno) -> Self {
PlatformError::ErrnoError(err).into()
}
}
+23
View File
@@ -0,0 +1,23 @@
//! Async pipe reading utilities for Unix.
use std::io;
use std::os::unix::io::OwnedFd;
use tokio::net::unix::pipe;
pub(crate) struct AsyncPipeReader(pipe::Receiver);
impl AsyncPipeReader {
pub(crate) fn new(reader: std::io::PipeReader) -> io::Result<Self> {
Ok(Self(pipe::Receiver::from_file(std::fs::File::from(
OwnedFd::from(reader),
))?))
}
pub(crate) async fn read_to_string(&mut self) -> io::Result<String> {
use tokio::io::AsyncReadExt;
let mut s = String::new();
self.0.read_to_string(&mut s).await?;
Ok(s)
}
}
+104
View File
@@ -0,0 +1,104 @@
//! Command execution utilities.
pub use std::os::unix::process::CommandExt;
pub use std::os::unix::process::ExitStatusExt;
use command_fds::{CommandFdExt, FdMapping};
use crate::ShellFd;
use crate::error;
use crate::openfiles;
/// Extension trait for injecting file descriptors into commands.
pub trait CommandFdInjectionExt {
/// Injects the given open files as file descriptors into the command.
///
/// # Arguments
///
/// * `open_files` - A mapping of child file descriptors to open files.
fn inject_fds(
&mut self,
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
) -> Result<(), error::Error>;
}
impl CommandFdInjectionExt for std::process::Command {
fn inject_fds(
&mut self,
open_files: impl Iterator<Item = (ShellFd, openfiles::OpenFile)>,
) -> Result<(), error::Error> {
let fd_mappings: Vec<FdMapping> = open_files
.map(|(child_fd, open_file)| -> Result<FdMapping, error::Error> {
let parent_fd = open_file.try_clone_to_owned()?;
Ok(FdMapping {
child_fd,
parent_fd,
})
})
.collect::<Result<Vec<_>, _>>()?;
self.fd_mappings(fd_mappings)
.map_err(|_e| error::ErrorKind::ChildCreationFailure)?;
Ok(())
}
}
/// Extension trait for arranging for commands to take the foreground.
pub trait CommandFgControlExt {
/// Arranges for the command to take the foreground when it is executed.
fn take_foreground(&mut self);
/// Arranges for the command to become a session leader when it is executed.
fn lead_session(&mut self);
}
impl CommandFgControlExt for std::process::Command {
fn take_foreground(&mut self) {
// SAFETY:
// This arranges for a provided function to run in the context of
// the forked process before it exec's the target command. In general,
// rust can't guarantee safety of code running in such a context.
unsafe {
self.pre_exec(pre_exec_take_foreground);
}
}
fn lead_session(&mut self) {
// SAFETY:
// This arranges for a provided function to run in the context of
// the forked process before it exec's the target command. In general,
// rust can't guarantee safety of code running in such a context.
unsafe {
self.pre_exec(pre_exec_lead_session);
}
}
}
fn pre_exec_take_foreground() -> Result<(), std::io::Error> {
use crate::sys;
sys::terminal::move_self_to_foreground()?;
Ok(())
}
fn pre_exec_lead_session() -> Result<(), std::io::Error> {
if let Err(e) = nix::unistd::setsid() {
return Err(std::io::Error::other(format!(
"failed to become session leader: {e}"
)));
}
#[cfg(not(target_os = "macos"))]
let control = libc::TIOCSCTTY;
#[cfg(target_os = "macos")]
let control: u64 = libc::TIOCSCTTY.into();
// SAFETY:
// This is calling a libc function to set the controlling terminal.
let result = unsafe { libc::ioctl(0, control, 0) };
if result != 0 {
return Err(std::io::Error::other("failed to set controlling terminal"));
}
Ok(())
}
+8
View File
@@ -0,0 +1,8 @@
//! Environment variable retrieval for Unix platforms.
/// Retrieves environment variables from the host process.
///
/// On Unix, this is a direct passthrough to [`std::env::vars()`].
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
std::env::vars()
}
+87
View File
@@ -0,0 +1,87 @@
//! File descriptor utilities.
use std::os::fd::RawFd;
use crate::{ShellFd, error, openfiles};
cfg_if::cfg_if! {
if #[cfg(any(target_os = "linux", target_os = "android"))] {
const FD_DIR_PATH: &str = "/proc/self/fd";
} else if #[cfg(any(
target_os = "freebsd",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))] {
const FD_DIR_PATH: &str = "/dev/fd";
} else {
/// Returns an iterator over all open file descriptors for the shell.
pub fn iter_fds()
-> Result<impl Iterator<Item = (ShellFd, openfiles::OpenFile)>, error::Error> {
Ok(std::iter::empty())
}
}
}
/// Makes a best-effort attempt to iterate over all open file descriptors
/// for the current process.
///
/// If the platform does not support enumerating file descriptors, an empty iterator
/// is returned. This function will skip any file descriptors that cannot be opened.
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "macos",
target_os = "netbsd",
target_os = "openbsd"
))]
pub fn try_iter_open_fds() -> impl Iterator<Item = (ShellFd, openfiles::OpenFile)> {
std::fs::read_dir(FD_DIR_PATH)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter_map(|entry| {
let fd: RawFd = entry.file_name().to_str()?.parse().ok()?;
// SAFETY:
// We are trying to open the file descriptor we found listed
// in the filesystem, but there's a risk that it's not the same one
// that we enumerated or that it's since been closed. For the purposes
// of this function, either of those outcomes are acceptable. We
// simply skip any fds that we can't open, and the function's purpose
// is to make a best-effort attempt to open all available fds.
let file = unsafe { open_file_by_fd(fd) }.ok()?;
Some((fd, file))
})
}
/// Attempts to retrieve an `OpenFile` representation for the given already-open file descriptor.
///
/// If the file descriptor cannot be opened, `None` is returned. Note that there is no guarantee
/// that the returned file matches the original file descriptor, as the fd may have been closed
/// and potentially re-used in the meantime.
///
/// # Arguments
///
/// * `fd` - The file descriptor to open.
pub fn try_get_file_for_open_fd(fd: RawFd) -> Option<openfiles::OpenFile> {
// SAFETY:
// We are trying to open the file descriptor provided by the caller. There's a risk that the fd
// is invalid or has been closed since it was enumerated. For the purposes of this function,
// we simply return None if we can't open it. There's also a risk that the fd has been closed
// and re-used for a different file; again, for the purposes of this function, we accept that
// risk and document it as part of the function's contract.
unsafe { open_file_by_fd(fd).ok() }
}
unsafe fn open_file_by_fd(fd: RawFd) -> Result<openfiles::OpenFile, error::Error> {
// SAFETY: We are creating a BorrowedFd from a file descriptor. Callers typically
// enumerate available file descriptors from procfs, devfs, or similar, but there's
// still a risk that the fd has become invalid or closed since then -- or that this
// function gets used incorrectly.
let borrowed_fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(fd) };
let owned_fd = borrowed_fd.try_clone_to_owned()?;
Ok(std::fs::File::from(owned_fd).into())
}
+413
View File
@@ -0,0 +1,413 @@
//! Filesystem utilities.
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use crate::error;
pub use std::os::unix::fs::MetadataExt;
#[cfg(target_os = "android")]
// _PATH_DEFPATH in https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/include/paths.h
const ANDROID_DEFPATH: &str = "/product/bin:/apex/com.android.runtime/bin:/apex/com.android.art/bin:/apex/com.android.virt/bin:/system_ext/bin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin";
impl crate::sys::fs::PathExt for Path {
fn readable(&self) -> bool {
nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
}
fn writable(&self) -> bool {
nix::unistd::access(self, nix::unistd::AccessFlags::W_OK).is_ok()
}
fn executable(&self) -> bool {
nix::unistd::access(self, nix::unistd::AccessFlags::X_OK).is_ok()
}
fn exists_and_is_block_device(&self) -> bool {
try_get_file_type(self).is_some_and(|ft| ft.is_block_device())
}
fn exists_and_is_char_device(&self) -> bool {
try_get_file_type(self).is_some_and(|ft| ft.is_char_device())
}
fn exists_and_is_fifo(&self) -> bool {
try_get_file_type(self).is_some_and(|ft: std::fs::FileType| ft.is_fifo())
}
fn exists_and_is_socket(&self) -> bool {
try_get_file_type(self).is_some_and(|ft| ft.is_socket())
}
fn exists_and_is_setgid(&self) -> bool {
const S_ISGID: u32 = 0o2000;
let file_mode = try_get_file_mode(self);
file_mode.is_some_and(|mode| mode & S_ISGID != 0)
}
fn exists_and_is_setuid(&self) -> bool {
const S_ISUID: u32 = 0o4000;
let file_mode = try_get_file_mode(self);
file_mode.is_some_and(|mode| mode & S_ISUID != 0)
}
fn exists_and_is_sticky_bit(&self) -> bool {
const S_ISVTX: u32 = 0o1000;
let file_mode = try_get_file_mode(self);
file_mode.is_some_and(|mode| mode & S_ISVTX != 0)
}
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
let metadata = self.metadata()?;
Ok((metadata.dev(), metadata.ino()))
}
}
fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
path.metadata().map(|metadata| metadata.file_type()).ok()
}
fn try_get_file_mode(path: &Path) -> Option<u32> {
path.metadata().map(|metadata| metadata.mode()).ok()
}
/// Splits a platform-specific PATH-like value into individual paths.
///
/// On Unix, this delegates to [`std::env::split_paths`].
pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
std::env::split_paths(s)
}
pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
#[cfg(target_os = "android")]
{
std::env::split_paths(ANDROID_DEFPATH).collect()
}
#[cfg(not(target_os = "android"))]
{
// standard hard-coded defaults for executable search path
vec![
"/usr/local/sbin".into(),
"/usr/local/bin".into(),
"/usr/sbin".into(),
"/usr/bin".into(),
"/sbin".into(),
"/bin".into(),
]
}
}
/// Retrieves the platform-specific set of paths that should contain standard system
/// utilities. Used by `command -p`, for example.
pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
//
// Try to call confstr(_CS_PATH). If that fails, can't find a string value, or
// finds an empty string, then we'll fall back to hard-coded defaults.
//
if let Ok(Some(cs_path)) = confstr_cs_path()
&& !cs_path.as_os_str().is_empty()
{
return split_paths(&cs_path).collect();
}
#[cfg(target_os = "android")]
{
std::env::split_paths(ANDROID_DEFPATH).collect()
}
#[cfg(not(target_os = "android"))]
{
// standard hard-coded defaults
vec![
"/bin".into(),
"/usr/bin".into(),
"/sbin".into(),
"/usr/sbin".into(),
"/etc".into(),
"/usr/etc".into(),
]
}
}
#[allow(clippy::unnecessary_wraps)]
fn confstr_cs_path() -> Result<Option<PathBuf>, std::io::Error> {
#[cfg(target_os = "android")]
{
Ok(Some(PathBuf::from(ANDROID_DEFPATH)))
}
#[cfg(not(target_os = "android"))]
{
let value = confstr(nix::libc::_CS_PATH)?;
if let Some(value) = value {
let value_str = PathBuf::from(value);
Ok(Some(value_str))
} else {
Ok(None)
}
}
}
/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
/// indicates where all the POSIX.2 standard utilities can be found.
///
/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic way) by nix
/// or similar. Until that exists, we accept the need to make the unsafe call directly.
#[cfg(not(target_os = "android"))]
fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
// SAFETY:
// Calling `confstr` with a null pointer and size 0 is a documented way to query
// the required size of the buffer to hold the value associated with `name`. It
// should not end up causing any undefined behavior.
let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
// When confstr returns 0, it either means there's no value associated with _CS_PATH, or
// _CS_PATH is considered invalid (and not present) on this platform. In both cases, we
// treat it as a non-existent value and return None.
if required_size == 0 {
return Ok(None);
}
let mut buffer = Vec::<u8>::with_capacity(required_size);
// SAFETY:
// We are calling `confstr` with a valid pointer and size that we obtained from the
// allocated buffer. Writing `c_char` (i8 or u8 depending on the platform) into
// `Vec<u8>` is fine, as i8 and u8 have compatible representations, and Rust does
// not support platforms where `c_char` is not 8-bit wide.
let final_size =
unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
if final_size == 0 {
return Err(std::io::Error::last_os_error());
}
// Per the docs on `confstr`, it *may* return a size larger than the provided buffer.
// In our usage we wouldn't expect to see this, as we've first queried the required size.
// However, we defensively check for this case and return an error if it happens.
if final_size > buffer.capacity() {
return Err(std::io::Error::other(
"confstr needed more space than advertised",
));
}
// SAFETY:
// We are trusting `confstr` to have written exactly `final_size` bytes into the buffer.
// We have checked above that it didn't return a value *larger* than the capacity of
// the buffer, and also checked for known error cases. Note that the returned length
// should include the null terminator.
unsafe { buffer.set_len(final_size) };
// The last byte is a null terminator. We assert that it is.
if !matches!(buffer.pop(), Some(0)) {
return Err(std::io::Error::other(
"confstr did not null-terminate the returned string",
));
}
Ok(Some(std::ffi::OsString::from_vec(buffer)))
}
/// Opens a null file that will discard all I/O.
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
let f = std::fs::File::options()
.read(true)
.write(true)
.open("/dev/null")?;
Ok(f)
}
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
pub const fn try_open_special_file(_path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
None
}
/// Returns the path to the system-wide shell profile script.
pub fn get_system_profile_path() -> Option<&'static Path> {
Some(Path::new("/etc/profile"))
}
/// Returns the path to the system-wide shell rc script.
pub fn get_system_rc_path() -> Option<&'static Path> {
Some(Path::new("/etc/bash.bashrc"))
}
/// Returns true if the string contains a path separator character.
///
/// On Unix, only `/` is considered a path separator.
pub fn contains_path_separator(s: &str) -> bool {
s.contains('/')
}
/// Returns true if the string ends with a path separator character.
///
/// On Unix, only `/` is considered a path separator.
pub fn ends_with_path_separator(s: &str) -> bool {
s.ends_with('/')
}
/// Returns the string with a trailing path separator removed, if present.
///
/// On Unix, only `/` is considered a path separator.
pub fn strip_path_separator_suffix(s: &str) -> &str {
s.strip_suffix('/').unwrap_or(s)
}
/// Returns the platform default for case-insensitive pathname expansion.
///
/// On Unix, filesystems are typically case-sensitive, so this returns `false`.
pub const fn default_case_insensitive_path_expansion() -> bool {
false
}
/// Finds the byte index of the last path separator in the string.
///
/// On Unix, only `/` is considered a path separator.
pub fn rfind_path_separator(s: &str) -> Option<usize> {
s.rfind('/')
}
/// Splits a string on path separator characters, returning an iterator of components.
///
/// On Unix, only `/` is used as a separator.
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
s.split('/')
}
/// Returns the root path for an absolute pattern, if the first component indicates one.
///
/// On Unix, an empty first component (from splitting a path like `/foo`) indicates
/// an absolute path rooted at `/`.
pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
if first_component.is_empty() {
Some(PathBuf::from("/"))
} else {
None
}
}
/// Pushes a component onto a path for pattern expansion.
///
/// On Unix, this delegates directly to `PathBuf::push`.
pub fn push_path_for_pattern(path: &mut std::path::PathBuf, component: &str) {
path.push(component);
}
/// Normalizes path separators for shell output.
///
/// On Unix, this is a no-op since paths already use `/`.
pub const fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
std::borrow::Cow::Borrowed(s)
}
/// Resolves an owned path to the actual on-disk executable file, if any.
///
/// On Unix this is a straight passthrough: if the path is executable, the
/// path is returned unchanged (no clone). This keeps `pathsearch::next`
/// allocation-free on the happy path.
///
/// On Windows this function may append a `PATHEXT` extension and return a
/// possibly-different `PathBuf`.
pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
use crate::sys::fs::PathExt;
if path.as_path().executable() {
Some(path)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_separator_helpers() {
assert!(contains_path_separator("foo/bar"));
assert!(!contains_path_separator("foobar"));
// Backslashes are not separators on Unix.
assert!(!contains_path_separator(r"foo\bar"));
assert!(ends_with_path_separator("foo/"));
assert!(!ends_with_path_separator("foo"));
assert!(!ends_with_path_separator(r"foo\"));
assert_eq!(strip_path_separator_suffix("foo/"), "foo");
assert_eq!(strip_path_separator_suffix("foo"), "foo");
assert_eq!(strip_path_separator_suffix(r"foo\"), r"foo\");
assert_eq!(rfind_path_separator("a/b/c"), Some(3));
assert_eq!(rfind_path_separator("abc"), None);
}
#[test]
fn split_path_for_pattern_basic() {
let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
assert_eq!(parts, vec!["", "a", "b"]);
// Backslashes are not split on Unix.
let parts: Vec<_> = split_path_for_pattern(r"a\b").collect();
assert_eq!(parts, vec![r"a\b"]);
}
#[test]
fn pattern_path_root_absolute() {
assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
}
#[test]
fn pattern_path_root_relative() {
assert_eq!(pattern_path_root("foo"), None);
// Drive-letter syntax is not recognized on Unix.
assert_eq!(pattern_path_root("c:"), None);
}
#[test]
fn push_path_for_pattern_appends_child() {
let mut p = PathBuf::from("/home/reuben");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("/home/reuben/foo"));
}
#[test]
fn normalize_path_separators_is_noop() {
use std::borrow::Cow;
assert!(matches!(
normalize_path_separators("/foo/bar"),
Cow::Borrowed("/foo/bar")
));
}
#[test]
fn default_case_insensitive_is_false() {
assert!(!default_case_insensitive_path_expansion());
}
#[test]
fn resolve_executable_returns_input_unchanged() {
// /bin/sh exists and is executable on every supported Unix host.
let path = PathBuf::from("/bin/sh");
let resolved = resolve_executable(path.clone());
assert_eq!(resolved.as_deref(), Some(path.as_path()));
}
#[test]
fn resolve_executable_returns_none_for_nonexistent() {
let path = PathBuf::from("/this/path/should/not/exist/brush-test");
assert!(resolve_executable(path).is_none());
}
#[test]
fn resolve_executable_returns_none_for_non_executable() {
// /etc/hostname (or similar) is a regular file but not executable.
// Use /etc/passwd which is universally present and not executable.
let path = PathBuf::from("/etc/passwd");
assert!(resolve_executable(path).is_none());
}
}
+84
View File
@@ -0,0 +1,84 @@
//! Terminal input utilities
use std::collections::HashMap;
use std::sync::LazyLock;
use terminfo::capability as cap;
use crate::{error, interfaces};
macro_rules! key {
( $terminfo:expr , $our_key:expr, $terminfo_key:ty ) => {{
(
$our_key,
$terminfo
.get::<$terminfo_key>()
.map(|k| k.expand().to_vec()),
)
}};
}
fn build_terminfo_key_map() -> HashMap<Vec<u8>, interfaces::Key> {
let mut map: HashMap<Vec<u8>, interfaces::Key> = HashMap::new();
if let Ok(ti) = terminfo::Database::from_env() {
// Iterate over key capabilities and populate the map
let key_capabilities = [
key!(ti, interfaces::Key::F(1), cap::KeyF1<'_>),
key!(ti, interfaces::Key::F(2), cap::KeyF2<'_>),
key!(ti, interfaces::Key::F(3), cap::KeyF3<'_>),
key!(ti, interfaces::Key::F(4), cap::KeyF4<'_>),
key!(ti, interfaces::Key::F(5), cap::KeyF5<'_>),
key!(ti, interfaces::Key::F(6), cap::KeyF6<'_>),
key!(ti, interfaces::Key::F(7), cap::KeyF7<'_>),
key!(ti, interfaces::Key::F(8), cap::KeyF8<'_>),
key!(ti, interfaces::Key::F(9), cap::KeyF9<'_>),
key!(ti, interfaces::Key::F(10), cap::KeyF10<'_>),
key!(ti, interfaces::Key::F(11), cap::KeyF11<'_>),
key!(ti, interfaces::Key::F(12), cap::KeyF12<'_>),
key!(ti, interfaces::Key::Backspace, cap::KeyBackspace<'_>),
key!(ti, interfaces::Key::Enter, cap::KeyEnter<'_>),
key!(ti, interfaces::Key::Left, cap::KeyLeft<'_>),
key!(ti, interfaces::Key::Right, cap::KeyRight<'_>),
key!(ti, interfaces::Key::Up, cap::KeyUp<'_>),
key!(ti, interfaces::Key::Down, cap::KeyDown<'_>),
key!(ti, interfaces::Key::Home, cap::KeyHome<'_>),
key!(ti, interfaces::Key::End, cap::KeyEnd<'_>),
key!(ti, interfaces::Key::PageUp, cap::KeyPPage<'_>),
key!(ti, interfaces::Key::PageDown, cap::KeyNPage<'_>),
key!(ti, interfaces::Key::BackTab, cap::BackTab<'_>),
// It's not clear if these belong here, because they're not
// strictly "key" capabilities.
key!(ti, interfaces::Key::Up, cap::CursorUp<'_>),
key!(ti, interfaces::Key::Down, cap::CursorDown<'_>),
key!(ti, interfaces::Key::Left, cap::CursorLeft<'_>),
key!(ti, interfaces::Key::Right, cap::CursorRight<'_>),
];
for (key, v) in key_capabilities {
if let Some(Ok(v)) = v {
map.insert(v.clone(), key.clone());
}
}
}
map
}
pub(crate) static TERMINFO_KEY_MAP: LazyLock<HashMap<Vec<u8>, interfaces::Key>> =
LazyLock::new(build_terminfo_key_map);
/// Translates a key code (byte sequence) into a `Key` enum value. Returns `None`
/// if the key code is not recognized.
///
/// # Arguments
///
/// * `key_code`: The byte sequence representing the key code.
pub fn try_get_key_from_key_code(key_code: &[u8]) -> Option<interfaces::Key> {
if let Some(key) = TERMINFO_KEY_MAP.get(key_code) {
Some(key.clone())
} else if key_code.len() == 1 && !key_code[0].is_ascii_control() {
Some(interfaces::Key::Character(key_code[0] as char))
} else {
None
}
}
+3
View File
@@ -0,0 +1,3 @@
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
crate::sys::hostname::get()
}
+108
View File
@@ -0,0 +1,108 @@
//! File descriptor polling utilities for timeout support.
use std::os::fd::BorrowedFd;
use std::time::{Duration, Instant};
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use crate::openfiles::OpenFile;
/// Polls an open file for input readability with a timeout.
///
/// Returns `Ok(true)` if data is available for reading, `Ok(false)` if the timeout
/// elapsed without data becoming available.
///
/// For regular files, always returns `Ok(true)` immediately since they're always
/// "ready" (matching bash behavior where `-t` has no effect on regular files).
///
/// # Arguments
///
/// * `file` - The open file to poll.
/// * `timeout` - Maximum time to wait. Use `Duration::ZERO` to check without blocking.
///
/// # Errors
///
/// Returns an error if polling fails or the file descriptor cannot be borrowed.
pub fn poll_for_input(file: &OpenFile, timeout: Duration) -> std::io::Result<bool> {
let fd = file
.try_borrow_as_fd()
.map_err(|e| std::io::Error::other(e.to_string()))?;
// Regular files are always ready - timeout has no effect (bash behavior).
if is_regular_file(fd) {
return Ok(true);
}
// Convert timeout to deadline for accurate time tracking across EINTR retries.
let deadline = if timeout.is_zero() {
// For zero timeout, use current instant so first check sees zero remaining.
Some(Instant::now())
} else {
Some(Instant::now() + timeout)
};
poll_fd_for_input(fd, deadline)
}
/// Polls a file descriptor for input readability with a deadline.
///
/// Returns `Ok(true)` if data is available, `Ok(false)` if deadline passed.
///
/// # Arguments
///
/// * `fd` - File descriptor to poll
/// * `deadline` - Optional deadline; `None` indicates no deadline.
fn poll_fd_for_input(fd: BorrowedFd<'_>, deadline: Option<Instant>) -> std::io::Result<bool> {
let mut poll_fds = [PollFd::new(fd, PollFlags::POLLIN)];
let mut first_iteration = true;
loop {
// Calculate remaining time on each iteration to handle EINTR correctly.
let timeout_ms = match deadline {
Some(d) => {
let remaining = d.saturating_duration_since(Instant::now());
// On first iteration, always do at least one poll even with zero timeout.
// This allows `-t 0` to check if input is immediately available.
if remaining.is_zero() && !first_iteration {
return Ok(false); // Deadline passed after initial poll.
}
i32::try_from(remaining.as_millis()).unwrap_or(i32::MAX)
}
None => -1, // Block indefinitely.
};
first_iteration = false;
let poll_timeout = PollTimeout::try_from(timeout_ms).unwrap_or(PollTimeout::MAX);
match poll(&mut poll_fds, poll_timeout) {
Ok(0) => return Ok(false), // Timeout
Ok(_) => {
let revents = poll_fds[0].revents().unwrap_or(PollFlags::empty());
// POLLIN means data available. POLLHUP/POLLERR without POLLIN means
// EOF/error - return true so caller reads and gets the proper result.
return Ok(
revents.intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR)
);
}
Err(nix::errno::Errno::EINTR) => (), // Retry on signal with recalculated timeout.
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)),
}
}
}
/// Checks if a file descriptor refers to a regular file.
///
/// Regular files are always "ready" for reading (poll has no effect).
///
/// # Arguments
///
/// * `fd` - File descriptor to check
fn is_regular_file(fd: BorrowedFd<'_>) -> bool {
match nix::sys::stat::fstat(fd) {
Ok(stat) => {
use nix::sys::stat::{SFlag, mode_t};
mode_t::try_from(stat.st_mode)
.is_ok_and(|mode| SFlag::from_bits_truncate(mode).contains(SFlag::S_IFREG))
}
Err(_) => false,
}
}
+31
View File
@@ -0,0 +1,31 @@
//! Resource utilities
use crate::error;
/// Returns the user and system CPU time used by the current process;
/// expressed as a tuple containing user time and system time, in that order.
pub fn get_self_user_and_system_time()
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_SELF)?;
Ok((
convert_rusage_time(usage.user_time()),
convert_rusage_time(usage.system_time()),
))
}
/// Returns the user and system CPU time used by child processes; expressed
/// as a tuple containing user time and system time, in that order.
pub fn get_children_user_and_system_time()
-> Result<(std::time::Duration, std::time::Duration), error::Error> {
let usage = nix::sys::resource::getrusage(nix::sys::resource::UsageWho::RUSAGE_CHILDREN)?;
Ok((
convert_rusage_time(usage.user_time()),
convert_rusage_time(usage.system_time()),
))
}
const fn convert_rusage_time(time: nix::sys::time::TimeVal) -> std::time::Duration {
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_possible_truncation)]
std::time::Duration::new(time.tv_sec() as u64, time.tv_usec() as u32 * 1000)
}
+160
View File
@@ -0,0 +1,160 @@
//! Signal processing utilities
use crate::{error, sys, traps};
pub(crate) use nix::sys::signal::Signal;
pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), nix::sys::signal::SIGCONT)
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
Ok(())
}
/// Sends a signal to a specific process.
///
/// # Arguments
/// * `pid` - The process ID to send the signal to
/// * `signal` - The signal to send (must be a real signal, not a trap signal)
pub fn kill_process(
pid: sys::process::ProcessId,
signal: traps::TrapSignal,
) -> Result<(), error::Error> {
let translated_signal = match signal {
traps::TrapSignal::Signal(signal) => signal,
traps::TrapSignal::Debug
| traps::TrapSignal::Err
| traps::TrapSignal::Exit
| traps::TrapSignal::Return => {
return Err(error::ErrorKind::InvalidSignal(signal.to_string()).into());
}
};
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), translated_signal)
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;
Ok(())
}
pub(crate) fn lead_new_process_group() -> Result<(), error::Error> {
nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))?;
Ok(())
}
pub(crate) fn tstp_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::from_raw(
nix::libc::SIGTSTP,
))?;
Ok(signal)
}
pub(crate) fn chld_signal_listener() -> Result<tokio::signal::unix::Signal, error::Error> {
let signal = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::child())?;
Ok(signal)
}
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
pub(crate) fn mask_sigttou() -> Result<(), error::Error> {
let ignore = nix::sys::signal::SigAction::new(
nix::sys::signal::SigHandler::SigIgn,
nix::sys::signal::SaFlags::empty(),
nix::sys::signal::SigSet::empty(),
);
// SAFETY:
// Setting the signal action should be safe here. The unsafe concerns
// for calling `sigaction` are primarily around ensuring that any provided
// signal handler functions are only performing operations that are
// safe to do in a signal handler context. Here we are not providing
// a custom handler, just asking the OS to ignore the signal.
unsafe { nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGTTOU, &ignore) }?;
Ok(())
}
pub(crate) fn poll_for_stopped_children() -> Result<bool, error::Error> {
let mut found_stopped = false;
loop {
let wait_status = waitid_all(
nix::sys::wait::WaitPidFlag::WUNTRACED | nix::sys::wait::WaitPidFlag::WNOHANG,
);
match wait_status {
Ok(nix::sys::wait::WaitStatus::Stopped(_stopped_pid, _signal)) => {
found_stopped = true;
}
Ok(_) => break,
Err(nix::errno::Errno::ECHILD) => break,
Err(e) => return Err(e.into()),
}
}
Ok(found_stopped)
}
#[cfg(not(target_os = "macos"))]
fn waitid_all(
flags: nix::sys::wait::WaitPidFlag,
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
nix::sys::wait::waitid(nix::sys::wait::Id::All, flags)
}
//
// N.B. These functions were mostly copied from nix::sys::wait (https://github.com/nix-rust/nix, MIT license)
// to enable use of the `waitid` call on macOS. Ideally nix would expose it on macOS and we would
// remove this code.
//
#[cfg(target_os = "macos")]
fn waitid_all(
flags: nix::sys::wait::WaitPidFlag,
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
// SAFETY:
// Code copied from nix::sys::wait implementation of waitid for other platforms.
// The siginfo structure is valid when filled with zeroes. Memory is zeroed
// rather than uninitialized, as not all platforms initialize the memory in
// the StillAlive case.
let mut siginfo: nix::libc::siginfo_t = unsafe { std::mem::zeroed() };
// SAFETY:
// Code copied from nix::sys::wait implementation of waitid for other platforms.
nix::errno::Errno::result(unsafe {
nix::libc::waitid(nix::libc::P_ALL, 0, &raw mut siginfo, flags.bits())
})?;
siginfo_to_wait_status(siginfo)
}
#[cfg(target_os = "macos")]
fn siginfo_to_wait_status(
siginfo: nix::libc::siginfo_t,
) -> Result<nix::sys::wait::WaitStatus, nix::errno::Errno> {
// SAFETY:
// Code copied from nix::sys::wait implementation of waitid for other platforms.
let si_pid = unsafe { siginfo.si_pid() };
if si_pid == 0 {
return Ok(nix::sys::wait::WaitStatus::StillAlive);
}
let pid = nix::unistd::Pid::from_raw(si_pid);
// SAFETY:
// Code copied from nix::sys::wait implementation of waitid for other platforms.
let si_status = unsafe { siginfo.si_status() };
let status = match siginfo.si_code {
nix::libc::CLD_EXITED => nix::sys::wait::WaitStatus::Exited(pid, si_status),
nix::libc::CLD_KILLED | nix::libc::CLD_DUMPED => nix::sys::wait::WaitStatus::Signaled(
pid,
nix::sys::signal::Signal::try_from(si_status)?,
siginfo.si_code == nix::libc::CLD_DUMPED,
),
nix::libc::CLD_STOPPED => {
nix::sys::wait::WaitStatus::Stopped(pid, nix::sys::signal::Signal::try_from(si_status)?)
}
nix::libc::CLD_CONTINUED => nix::sys::wait::WaitStatus::Continued(pid),
_ => return Err(nix::errno::Errno::EINVAL),
};
Ok(status)
}
+119
View File
@@ -0,0 +1,119 @@
//! Terminal utilities.
use crate::{error, openfiles, sys, terminal};
use std::{io::IsTerminal, os::fd::AsFd, path::PathBuf};
/// Terminal configuration.
#[derive(Clone, Debug)]
pub struct Config {
termios: nix::sys::termios::Termios,
}
impl Config {
/// Creates a new `Config` from the actual terminal attributes of the terminal associated
/// with the given file descriptor.
///
/// # Arguments
///
/// * `file` - A reference to the open terminal.
pub fn from_term(file: &openfiles::OpenFile) -> Result<Self, error::Error> {
let fd = file.try_borrow_as_fd()?;
let termios = nix::sys::termios::tcgetattr(fd)?;
Ok(Self { termios })
}
/// Applies the terminal settings to the terminal associated with the given file descriptor.
///
/// # Arguments
///
/// * `file` - A reference to the open terminal.
pub fn apply_to_term(&self, file: &openfiles::OpenFile) -> Result<(), error::Error> {
let fd = file.try_borrow_as_fd()?;
nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.termios)?;
Ok(())
}
/// Applies the given high-level terminal settings to this configuration. Does not modify any
/// terminal itself.
///
/// # Arguments
///
/// * `settings` - The high-level terminal settings to apply to this configuration.
pub fn update(&mut self, settings: &terminal::Settings) {
if let Some(echo_input) = &settings.echo_input {
if *echo_input {
self.termios.local_flags |= nix::sys::termios::LocalFlags::ECHO;
} else {
self.termios.local_flags -= nix::sys::termios::LocalFlags::ECHO;
}
}
if let Some(line_input) = &settings.line_input {
if *line_input {
self.termios.local_flags |= nix::sys::termios::LocalFlags::ICANON;
} else {
self.termios.local_flags -= nix::sys::termios::LocalFlags::ICANON;
}
}
if let Some(interrupt_signals) = &settings.interrupt_signals {
if *interrupt_signals {
self.termios.local_flags |= nix::sys::termios::LocalFlags::ISIG;
} else {
self.termios.local_flags -= nix::sys::termios::LocalFlags::ISIG;
}
}
if let Some(output_nl_as_nlcr) = &settings.output_nl_as_nlcr {
if *output_nl_as_nlcr {
self.termios.output_flags |=
nix::sys::termios::OutputFlags::OPOST | nix::sys::termios::OutputFlags::ONLCR;
} else {
self.termios.output_flags -= nix::sys::termios::OutputFlags::ONLCR;
}
}
}
}
/// Get the process ID of this process's parent.
pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
Some(nix::unistd::getppid().as_raw())
}
/// Get the process group ID for this process's process group.
pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
Some(nix::unistd::getpgrp().as_raw())
}
/// Get the foreground process ID of the attached terminal.
pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
nix::unistd::tcgetpgrp(std::io::stdin())
.ok()
.map(|pgid| pgid.as_raw())
}
/// Move the specified process to the foreground of the attached terminal.
pub fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
Ok(())
}
/// Moves the current process to the foreground of the attached terminal.
// This function needs to return `std::io::Error` so that the OS error code can be recovered.
pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
if std::io::stdin().is_terminal() {
let pgid = nix::unistd::getpgid(None)?;
// TODO(jobs): This sometimes fails with ENOTTY even though we checked that stdin is a
// terminal. We should investigate why this is happening.
let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid);
}
Ok(())
}
/// Tries to get the path of the terminal device associated with the attached terminal.
/// Returns `None` if there is no terminal attached or the lookup failed.
pub fn try_get_terminal_device_path() -> Option<PathBuf> {
nix::unistd::ttyname(std::io::stdin()).ok()
}
+90
View File
@@ -0,0 +1,90 @@
use crate::{error, trace_categories};
use std::path::PathBuf;
use uzers::os::unix::UserExt;
pub(crate) fn is_root() -> bool {
uzers::get_current_uid() == 0
}
pub(crate) fn get_user_home_dir(username: &str) -> Option<PathBuf> {
if let Some(user_info) = uzers::get_user_by_name(username) {
return Some(user_info.home_dir().to_path_buf());
}
None
}
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
if let Some(username) = uzers::get_current_username()
&& let Some(user_info) = uzers::get_user_by_name(&username)
{
return Some(user_info.home_dir().to_path_buf());
}
None
}
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
if let Some(username) = uzers::get_current_username()
&& let Some(user_info) = uzers::get_user_by_name(&username)
{
return Some(user_info.shell().to_path_buf());
}
None
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
Ok(uzers::get_current_uid())
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
Ok(uzers::get_current_gid())
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
Ok(uzers::get_effective_uid())
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
Ok(uzers::get_effective_gid())
}
pub(crate) fn get_current_username() -> Result<String, error::Error> {
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
Ok(username.to_string_lossy().to_string())
}
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
let groups = get_current_user_groups()?;
Ok(groups.into_iter().map(|g| g.gid()).collect())
}
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
// TODO(#475): uzers::all_users() is available but unsafe; for now we just return the current
// user. That's better than nothing.
let user = get_current_username()?;
Ok(vec![user])
}
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
// TODO(#475): uzers::all_groups() is available but unsafe; for now we just return the current
// user's groups. That's better than nothing.
let groups = get_current_user_groups()?;
let group_names = groups
.into_iter()
.map(|g| g.name().to_string_lossy().to_string());
Ok(group_names.collect())
}
fn get_current_user_groups() -> Result<Vec<uzers::Group>, error::Error> {
let username = uzers::get_current_username().ok_or_else(|| error::ErrorKind::NoCurrentUser)?;
let gid = uzers::get_current_gid();
let groups = uzers::get_user_groups(&username, gid).unwrap_or_default();
Ok(groups)
}
+65
View File
@@ -0,0 +1,65 @@
//! Filesystem utilities for WASM.
pub use crate::sys::stubs::fs::*;
impl crate::sys::fs::PathExt for std::path::Path {
fn readable(&self) -> bool {
true
}
fn writable(&self) -> bool {
true
}
fn executable(&self) -> bool {
true
}
fn exists_and_is_block_device(&self) -> bool {
false
}
fn exists_and_is_char_device(&self) -> bool {
false
}
fn exists_and_is_fifo(&self) -> bool {
false
}
fn exists_and_is_socket(&self) -> bool {
false
}
fn exists_and_is_setgid(&self) -> bool {
false
}
fn exists_and_is_setuid(&self) -> bool {
false
}
fn exists_and_is_sticky_bit(&self) -> bool {
false
}
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
Ok((0, 0))
}
}
/// Splits a PATH-like value into individual paths.
///
/// On WASM, `std::env::split_paths` is not available, so this
/// implementation splits by the `:` separator.
pub fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(
s: &T,
) -> impl Iterator<Item = std::path::PathBuf> {
s.as_ref()
.to_str()
.unwrap_or_default()
.split(':')
.map(std::path::PathBuf::from)
.collect::<Vec<_>>()
.into_iter()
}
+18
View File
@@ -0,0 +1,18 @@
pub use crate::sys::stubs::async_pipe;
pub use crate::sys::stubs::commands;
pub(crate) use crate::sys::stubs::env;
pub use crate::sys::stubs::fd;
pub(crate) mod fs;
pub use crate::sys::stubs::input;
pub(crate) use crate::sys::stubs::network;
pub(crate) use crate::sys::stubs::pipes;
pub use crate::sys::stubs::poll;
pub use crate::sys::stubs::process;
pub use crate::sys::stubs::resource;
pub use crate::sys::stubs::signal;
pub use crate::sys::stubs::terminal;
pub(crate) use crate::sys::stubs::users;
/// Platform-specific errors.
#[derive(Debug, thiserror::Error)]
pub enum PlatformError {}
+23
View File
@@ -0,0 +1,23 @@
pub use crate::sys::stubs::async_pipe;
pub use crate::sys::stubs::commands;
pub(crate) mod env;
pub use crate::sys::stubs::fd;
pub(crate) mod fs;
pub use crate::sys::stubs::input;
pub(crate) mod network;
pub use crate::sys::stubs::poll;
pub use crate::sys::stubs::resource;
/// Signal processing utilities
pub mod signal {
pub(crate) use crate::sys::stubs::signal::*;
pub(crate) use tokio::signal::ctrl_c as await_ctrl_c;
}
pub use crate::sys::stubs::terminal;
pub use crate::sys::tokio_process as process;
pub(crate) mod users;
/// Platform-specific errors.
#[derive(Debug, thiserror::Error)]
pub enum PlatformError {}
+207
View File
@@ -0,0 +1,207 @@
//! Environment variable retrieval for Windows.
//!
//! On Windows, well-known environment variable names are normalized to their
//! canonical POSIX forms (e.g. `Path` → `PATH`), and `HOME` is synthesized
//! from `USERPROFILE` or `HOMEDRIVE`+`HOMEPATH` if not already present.
use std::collections::BTreeMap;
/// Retrieves environment variables from the host process, applying
/// Windows-specific fixups.
///
/// Normalizes well-known variable names to POSIX conventions, copies
/// `TEMP`/`TMP` to `TMPDIR` (preserving originals for native Windows apps),
/// and synthesizes `HOME` if it is not natively defined.
///
/// A [`BTreeMap`] is used (rather than `HashMap`) so iteration order is
/// deterministic across runs. If two source variables collide under the same
/// canonical name (e.g. both `Path` and `PATH` are set to different values),
/// the conflict is logged and the last-seen value wins.
pub(crate) fn get_host_env_vars() -> impl Iterator<Item = (String, String)> {
collect_host_env_vars(std::env::vars())
}
/// Collects and normalizes a set of environment variables. Exposed as a
/// pure function (taking the source iterator) so it can be unit-tested
/// without touching the process environment.
fn collect_host_env_vars<I>(source: I) -> std::collections::btree_map::IntoIter<String, String>
where
I: IntoIterator<Item = (String, String)>,
{
let mut vars = BTreeMap::new();
// Normalize host env vars and inject them into the map.
for (k, v) in source {
let normalized = normalize_env_name(&k);
if let Some(existing) = vars.get(&normalized)
&& existing != &v
{
tracing::warn!(
"environment variable collision under canonical name {normalized}: \
two different values were supplied (last-write wins)"
);
}
vars.insert(normalized, v);
}
// Synthesize HOME from Windows-native variables if not already present.
if !vars.contains_key("HOME") {
let home = vars.get("USERPROFILE").cloned().or_else(|| {
let d = vars.get("HOMEDRIVE")?;
let p = vars.get("HOMEPATH")?;
Some(format!("{d}{p}"))
});
if let Some(home) = home {
vars.insert("HOME".to_string(), home);
}
}
// Copy TEMP/TMP to TMPDIR if TMPDIR doesn't already exist.
if !vars.contains_key("TMPDIR")
&& let Some(tmp) = vars.get("TEMP").or_else(|| vars.get("TMP")).cloned()
{
vars.insert("TMPDIR".to_string(), tmp);
}
vars.into_iter()
}
/// Normalizes the case of well-known environment variable names to their
/// canonical POSIX forms (`Path` → `PATH`, `home` → `HOME`).
///
/// # Arguments
///
/// * `name` - The environment variable name to normalize.
fn normalize_env_name(name: &str) -> String {
// Normalize well-known variable names so that later lookups by
// canonical (uppercase) spelling always succeed regardless of the
// host's original casing.
const WELL_KNOWN: &[&str] = &[
"PATH",
"HOME",
"USERPROFILE",
"HOMEDRIVE",
"HOMEPATH",
"TEMP",
"TMP",
"TMPDIR",
];
for &canonical in WELL_KNOWN {
if name.eq_ignore_ascii_case(canonical) {
return canonical.to_string();
}
}
name.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn run(source: &[(&str, &str)]) -> BTreeMap<String, String> {
collect_host_env_vars(
source
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string())),
)
.collect()
}
#[test]
fn normalize_env_name_canonicalizes_well_known() {
assert_eq!(normalize_env_name("Path"), "PATH");
assert_eq!(normalize_env_name("path"), "PATH");
assert_eq!(normalize_env_name("PATH"), "PATH");
assert_eq!(normalize_env_name("Home"), "HOME");
assert_eq!(normalize_env_name("UserProfile"), "USERPROFILE");
assert_eq!(normalize_env_name("Temp"), "TEMP");
assert_eq!(normalize_env_name("Tmp"), "TMP");
assert_eq!(normalize_env_name("TmpDir"), "TMPDIR");
}
#[test]
fn normalize_env_name_leaves_unknown_alone() {
assert_eq!(normalize_env_name("FOO"), "FOO");
assert_eq!(normalize_env_name("myVar"), "myVar");
// Does not uppercase unknown names.
assert_eq!(normalize_env_name("AppData"), "AppData");
}
#[test]
fn synthesizes_home_from_userprofile() {
let vars = run(&[("UserProfile", r"C:\Users\reuben")]);
assert_eq!(
vars.get("HOME").map(String::as_str),
Some(r"C:\Users\reuben")
);
assert_eq!(
vars.get("USERPROFILE").map(String::as_str),
Some(r"C:\Users\reuben")
);
}
#[test]
fn synthesizes_home_from_homedrive_homepath_when_no_userprofile() {
let vars = run(&[("HomeDrive", "C:"), ("HomePath", r"\Users\reuben")]);
assert_eq!(
vars.get("HOME").map(String::as_str),
Some(r"C:\Users\reuben")
);
}
#[test]
fn preserves_existing_home() {
let vars = run(&[("HOME", "/already/set"), ("UserProfile", r"C:\Users\other")]);
assert_eq!(vars.get("HOME").map(String::as_str), Some("/already/set"));
}
#[test]
fn copies_temp_to_tmpdir() {
let vars = run(&[("Temp", r"C:\Windows\Temp")]);
assert_eq!(
vars.get("TMPDIR").map(String::as_str),
Some(r"C:\Windows\Temp")
);
}
#[test]
fn prefers_temp_over_tmp_for_tmpdir() {
let vars = run(&[("TEMP", "one"), ("TMP", "two")]);
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("one"));
}
#[test]
fn falls_back_to_tmp_when_no_temp() {
let vars = run(&[("TMP", "two")]);
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("two"));
}
#[test]
fn preserves_existing_tmpdir() {
let vars = run(&[("TMPDIR", "original"), ("TEMP", "other")]);
assert_eq!(vars.get("TMPDIR").map(String::as_str), Some("original"));
}
#[test]
fn deterministic_iteration_order() {
// BTreeMap iteration order is determined by the sorted key order,
// so the outputs must be identical across invocations regardless of
// input ordering.
let a = run(&[("Path", "first"), ("ZETA", "zz"), ("Alpha", "aa")]);
let b = run(&[("ZETA", "zz"), ("Alpha", "aa"), ("Path", "first")]);
let keys_a: Vec<_> = a.keys().collect();
let keys_b: Vec<_> = b.keys().collect();
assert_eq!(keys_a, keys_b);
}
#[test]
fn collision_last_write_wins() {
// When two source names normalize to the same canonical key,
// the later one should overwrite the earlier one (and it should not
// panic).
let vars = run(&[("Path", "first"), ("PATH", "second")]);
assert_eq!(vars.get("PATH").map(String::as_str), Some("second"));
}
}
+455
View File
@@ -0,0 +1,455 @@
//! Filesystem utilities for Windows.
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use crate::error;
// Selectively re-export items from stubs that we don't override.
pub(crate) use crate::sys::stubs::fs::MetadataExt;
/// Cached list of executable extensions from the `PATHEXT` environment
/// variable. Each entry retains its leading dot (e.g. `".exe"`) and is stored
/// lowercased so case-insensitive comparisons can be done without allocating.
///
/// NOTE: This is cached for the process lifetime. Changes to `PATHEXT` made
/// inside the running shell are not reflected here. Bash itself has no
/// `PATHEXT` semantics, so this is generally acceptable for now.
static PATHEXT_EXTENSIONS: LazyLock<Vec<String>> = LazyLock::new(|| {
std::env::var("PATHEXT")
.unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string())
.split(';')
.filter(|s| !s.is_empty())
.map(|s| s.to_ascii_lowercase())
.collect()
});
/// Returns the stem of a PATHEXT entry (with any leading `.` removed).
///
/// `PATHEXT` canonically stores entries like `.EXE`, but tolerant parsing
/// accepts entries without the leading dot too.
fn pathext_entry_stem(entry: &str) -> &str {
entry.strip_prefix('.').unwrap_or(entry)
}
/// Returns true if the path's extension is in the PATHEXT list.
///
/// Performs case-insensitive comparison against the cached PATHEXT entries
/// without allocating.
fn has_executable_extension(path: &Path) -> bool {
path.extension().is_some_and(|ext| {
PATHEXT_EXTENSIONS
.iter()
.any(|e| ext.eq_ignore_ascii_case(pathext_entry_stem(e)))
})
}
/// Returns true if `path` is, by itself, an existing executable file.
///
/// Used both for the initial check in [`resolve_executable`] and for
/// [`PathExt::executable`].
fn is_executable_file(path: &Path) -> bool {
has_executable_extension(path) && path.is_file()
}
/// Resolves an owned path to the actual on-disk executable file, if any.
///
/// If the path is already a file with a `PATHEXT` extension, it is returned
/// unchanged (no allocation). Otherwise, each `PATHEXT` extension is appended
/// in turn and the first existing file is returned.
pub fn resolve_executable(path: PathBuf) -> Option<PathBuf> {
if is_executable_file(&path) {
return Some(path);
}
// Try appending each PATHEXT extension.
for ext in PATHEXT_EXTENSIONS.iter() {
let mut name = path.as_os_str().to_owned();
name.push(ext);
let candidate = PathBuf::from(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
impl crate::sys::fs::PathExt for Path {
fn readable(&self) -> bool {
self.exists()
}
fn writable(&self) -> bool {
self.metadata().is_ok_and(|m| !m.permissions().readonly())
}
fn executable(&self) -> bool {
if is_executable_file(self) {
return true;
}
// Try each PATHEXT extension without allocating a separate PathBuf
// per candidate until one exists.
PATHEXT_EXTENSIONS.iter().any(|ext| {
let mut name = self.as_os_str().to_owned();
name.push(ext);
Self::new(&name).is_file()
})
}
fn exists_and_is_block_device(&self) -> bool {
false
}
fn exists_and_is_char_device(&self) -> bool {
false
}
fn exists_and_is_fifo(&self) -> bool {
false
}
fn exists_and_is_socket(&self) -> bool {
false
}
fn exists_and_is_setgid(&self) -> bool {
false
}
fn exists_and_is_setuid(&self) -> bool {
false
}
fn exists_and_is_sticky_bit(&self) -> bool {
false
}
fn get_device_and_inode(&self) -> Result<(u64, u64), crate::error::Error> {
// TODO(windows): implement using file index / volume serial number.
Err(error::ErrorKind::NotSupportedOnThisPlatform("get_device_and_inode").into())
}
}
/// Splits a platform-specific PATH-like value into individual paths.
///
/// On Windows, this delegates to [`std::env::split_paths`].
pub fn split_paths<T: AsRef<OsStr> + ?Sized>(s: &T) -> std::env::SplitPaths<'_> {
std::env::split_paths(s)
}
/// Opens a null file that will discard all I/O.
pub fn open_null_file() -> Result<std::fs::File, error::Error> {
let f = std::fs::File::options()
.read(true)
.write(true)
.open("NUL")?;
Ok(f)
}
/// Gives the platform an opportunity to handle a special file path (e.g. `/dev/null`).
pub fn try_open_special_file(path: &Path) -> Option<Result<std::fs::File, std::io::Error>> {
if path.ends_with("dev/null") && path.is_absolute() {
Some(open_null_file().map_err(std::io::Error::other))
} else {
None
}
}
/// Returns the default paths where executables are typically found on Windows.
pub(crate) fn get_default_executable_search_paths() -> Vec<PathBuf> {
default_system_paths()
}
/// Returns the default paths where standard system utilities are found on Windows.
pub fn get_default_standard_utils_paths() -> Vec<PathBuf> {
default_system_paths()
}
fn default_system_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(sysroot) = std::env::var("SystemRoot") {
paths.push(PathBuf::from(&sysroot).join("system32"));
paths.push(PathBuf::from(&sysroot));
paths.push(PathBuf::from(&sysroot).join("System32").join("Wbem"));
paths.push(
PathBuf::from(&sysroot)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0"),
);
}
if let Ok(userprofile) = std::env::var("USERPROFILE") {
paths.push(
PathBuf::from(userprofile)
.join("AppData")
.join("Local")
.join("Microsoft")
.join("WindowsApps"),
);
}
paths
}
/// Returns the path to the system-wide shell profile script.
///
/// On Windows, no system profile is loaded by default.
pub const fn get_system_profile_path() -> Option<&'static Path> {
None
}
/// Returns the path to the system-wide shell rc script.
///
/// On Windows, no system rc file is loaded by default.
pub const fn get_system_rc_path() -> Option<&'static Path> {
None
}
/// Returns the platform default for case-insensitive pathname expansion.
///
/// On Windows, filesystems are typically case-insensitive, so this returns `true`.
pub const fn default_case_insensitive_path_expansion() -> bool {
true
}
/// Path separator characters on Windows.
const PATH_SEPARATORS: [char; 2] = ['/', '\\'];
/// Returns true if the string contains a path separator character.
///
/// On Windows, both `/` and `\` are considered path separators.
pub fn contains_path_separator(s: &str) -> bool {
s.contains(PATH_SEPARATORS)
}
/// Returns true if the string ends with a path separator character.
///
/// On Windows, both `/` and `\` are considered path separators.
pub fn ends_with_path_separator(s: &str) -> bool {
s.ends_with(PATH_SEPARATORS)
}
/// Returns the string with a trailing path separator removed, if present.
///
/// On Windows, both `/` and `\` are considered path separators.
pub fn strip_path_separator_suffix(s: &str) -> &str {
s.strip_suffix(PATH_SEPARATORS).unwrap_or(s)
}
/// Finds the byte index of the last path separator in the string.
///
/// On Windows, both `/` and `\` are considered path separators.
pub fn rfind_path_separator(s: &str) -> Option<usize> {
s.rfind(PATH_SEPARATORS)
}
/// Splits a string on path separator characters, returning an iterator of components.
///
/// On Windows, both `/` and `\` are used as separators.
pub fn split_path_for_pattern(s: &str) -> impl Iterator<Item = &str> {
s.split(PATH_SEPARATORS)
}
/// Returns the root path for an absolute pattern, if the first component indicates one.
///
/// On Windows, recognizes both a leading separator (empty first component from splitting
/// a path like `/foo`) and a drive-letter prefix like `C:` as absolute.
///
/// TODO(windows): UNC paths like `\\server\share\foo` are not yet handled
/// specially; they split into `["", "", "server", "share", "foo"]`, and the
/// leading empty component causes them to be treated as if they were rooted
/// at `/`, which drops the server/share portion. Supporting UNC requires
/// peeking further into the component list.
pub fn pattern_path_root(first_component: &str) -> Option<PathBuf> {
if first_component.is_empty() {
// Leading separator, e.g. `/foo` split into ["", "foo"].
Some(PathBuf::from("/"))
} else if first_component.len() == 2
&& first_component.as_bytes()[0].is_ascii_alphabetic()
&& first_component.as_bytes()[1] == b':'
{
// Drive letter prefix, e.g. `c:/foo` split into ["c:", "foo"].
let mut root = String::with_capacity(3);
root.push_str(first_component);
root.push('/');
Some(PathBuf::from(root))
} else {
None
}
}
/// Pushes a component onto a path for pattern expansion.
///
/// On Windows, `PathBuf::push` has special drive-letter and root-replacement
/// semantics that conflict with shell path construction (e.g. pushing `C:foo`
/// onto `D:\bar` replaces the whole path). This function always appends the
/// component as a child, operating on the underlying `OsString` so non-UTF-8
/// content in the path is preserved and no reallocation is needed.
pub fn push_path_for_pattern(path: &mut PathBuf, component: &str) {
// Separator characters are ASCII, and WTF-8-encoded OsStr bytes are a
// superset of UTF-8, so checking the last byte directly is safe.
let bytes = path.as_os_str().as_encoded_bytes();
let needs_sep = !bytes.is_empty() && !matches!(bytes.last(), Some(b'/' | b'\\'));
let buf = path.as_mut_os_string();
if needs_sep {
buf.push("/");
}
buf.push(component);
}
/// Normalizes path separators for shell output.
///
/// On Windows, replaces `\` with `/` since backslash is the shell escape character.
pub fn normalize_path_separators(s: &str) -> std::borrow::Cow<'_, str> {
if s.contains('\\') {
std::borrow::Cow::Owned(s.replace('\\', "/"))
} else {
std::borrow::Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_separator_helpers_both_slashes() {
assert!(contains_path_separator("foo/bar"));
assert!(contains_path_separator(r"foo\bar"));
assert!(contains_path_separator(r"mixed/and\back"));
assert!(!contains_path_separator("foobar"));
assert!(ends_with_path_separator("foo/"));
assert!(ends_with_path_separator(r"foo\"));
assert!(!ends_with_path_separator("foo"));
assert_eq!(strip_path_separator_suffix("foo/"), "foo");
assert_eq!(strip_path_separator_suffix(r"foo\"), "foo");
assert_eq!(strip_path_separator_suffix("foo"), "foo");
assert_eq!(rfind_path_separator("a/b/c"), Some(3));
assert_eq!(rfind_path_separator(r"a\b\c"), Some(3));
assert_eq!(rfind_path_separator(r"a/b\c"), Some(3));
assert_eq!(rfind_path_separator("abc"), None);
}
#[test]
fn split_path_for_pattern_both_slashes() {
let parts: Vec<_> = split_path_for_pattern("a/b/c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern(r"a\b\c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern(r"a/b\c").collect();
assert_eq!(parts, vec!["a", "b", "c"]);
let parts: Vec<_> = split_path_for_pattern("/a/b").collect();
assert_eq!(parts, vec!["", "a", "b"]);
}
#[test]
fn pattern_path_root_leading_separator() {
assert_eq!(pattern_path_root(""), Some(PathBuf::from("/")));
}
#[test]
fn pattern_path_root_drive_letters() {
assert_eq!(pattern_path_root("c:"), Some(PathBuf::from("c:/")));
assert_eq!(pattern_path_root("C:"), Some(PathBuf::from("C:/")));
assert_eq!(pattern_path_root("Z:"), Some(PathBuf::from("Z:/")));
}
#[test]
fn pattern_path_root_rejects_non_drive_two_char_prefix() {
// "1:" is not a valid drive letter — must be alphabetic.
assert_eq!(pattern_path_root("1:"), None);
// Longer drive-like strings are not treated as roots.
assert_eq!(pattern_path_root("cd"), None);
assert_eq!(pattern_path_root("c:\\"), None);
assert_eq!(pattern_path_root("foo"), None);
}
#[test]
fn push_path_for_pattern_appends_with_forward_slash() {
let mut p = PathBuf::from(r"C:\Users\reuben");
push_path_for_pattern(&mut p, "foo");
// Forward slash is used as the appended separator, yielding mixed
// separators — acceptable because `normalize_path_separators` is
// applied downstream before display.
assert_eq!(p, PathBuf::from(r"C:\Users\reuben/foo"));
}
#[test]
fn push_path_for_pattern_no_double_separator() {
let mut p = PathBuf::from("C:/Users/reuben/");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("C:/Users/reuben/foo"));
let mut p = PathBuf::from(r"C:\Users\reuben\");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from(r"C:\Users\reuben\foo"));
}
#[test]
fn push_path_for_pattern_onto_drive_root() {
let mut p = PathBuf::from("c:/");
push_path_for_pattern(&mut p, "foo");
assert_eq!(p, PathBuf::from("c:/foo"));
}
#[test]
fn push_path_for_pattern_onto_empty() {
let mut p = PathBuf::new();
push_path_for_pattern(&mut p, "foo");
// Empty path stays un-prefixed — we only add a separator between
// existing content and the new component.
assert_eq!(p, PathBuf::from("foo"));
}
#[test]
fn normalize_path_separators_converts_backslashes() {
use std::borrow::Cow;
// Already-forward-slashed input is borrowed (no allocation).
assert!(matches!(
normalize_path_separators("c:/foo/bar"),
Cow::Borrowed("c:/foo/bar")
));
// Mixed or backslashed input becomes owned and fully forward-slashed.
let normalized = normalize_path_separators(r"c:\foo\bar");
assert_eq!(normalized.as_ref(), "c:/foo/bar");
let normalized = normalize_path_separators(r"c:\foo/bar");
assert_eq!(normalized.as_ref(), "c:/foo/bar");
}
#[test]
fn default_case_insensitive_is_true() {
assert!(default_case_insensitive_path_expansion());
}
#[test]
fn has_executable_extension_is_case_insensitive() {
// Force the PATHEXT cache for this test's defaults.
assert!(has_executable_extension(Path::new("foo.exe")));
assert!(has_executable_extension(Path::new("foo.EXE")));
assert!(has_executable_extension(Path::new("foo.Cmd")));
assert!(!has_executable_extension(Path::new("foo.txt")));
assert!(!has_executable_extension(Path::new("foo")));
}
#[test]
fn pathext_entry_stem_strips_dot() {
assert_eq!(pathext_entry_stem(".exe"), "exe");
assert_eq!(pathext_entry_stem(".cmd"), "cmd");
// Tolerant: entries without a leading dot are returned as-is.
assert_eq!(pathext_entry_stem("exe"), "exe");
assert_eq!(pathext_entry_stem(""), "");
}
#[test]
fn resolve_executable_for_nonexistent_returns_none() {
// A path that cannot exist on any test host.
let path = PathBuf::from(r"C:\__brush_test_definitely_missing__");
assert!(resolve_executable(path).is_none());
}
}
+3
View File
@@ -0,0 +1,3 @@
pub(crate) fn get_hostname() -> std::io::Result<std::ffi::OsString> {
crate::sys::hostname::get()
}
+86
View File
@@ -0,0 +1,86 @@
#![allow(clippy::missing_const_for_fn)]
#![allow(clippy::unnecessary_wraps)]
use crate::error;
use std::path::PathBuf;
use std::sync::LazyLock;
/// Placeholder UID for non-elevated Windows processes.
///
/// Real Unix-style UIDs don't exist on Windows; this value is a
/// conventional non-root sentinel (matching the typical first
/// regular-user UID on Linux).
const NON_ELEVATED_UID: u32 = 1000;
/// Placeholder GID for non-elevated Windows processes (see [`NON_ELEVATED_UID`]).
const NON_ELEVATED_GID: u32 = 1000;
/// Cached elevation status. The underlying check queries the process token,
/// which can't change after process start, so it's safe to memoize.
static IS_ELEVATED: LazyLock<bool> = LazyLock::new(|| {
check_elevation::is_elevated().unwrap_or_else(|err| {
tracing::warn!("failed to determine process elevation: {err}");
false
})
});
pub(crate) fn get_user_home_dir(_username: &str) -> Option<PathBuf> {
// std::env::home_dir() doesn't support getting home dir for arbitrary users
// For now, we only support getting the current user's home dir
None
}
pub(crate) fn get_current_user_home_dir() -> Option<PathBuf> {
std::env::home_dir()
}
pub(crate) fn get_current_user_default_shell() -> Option<PathBuf> {
None
}
fn is_elevated() -> bool {
*IS_ELEVATED
}
pub(crate) fn is_root() -> bool {
is_elevated()
}
pub(crate) fn get_current_uid() -> Result<u32, error::Error> {
Ok(if is_elevated() { 0 } else { NON_ELEVATED_UID })
}
pub(crate) fn get_current_gid() -> Result<u32, error::Error> {
Ok(if is_elevated() { 0 } else { NON_ELEVATED_GID })
}
pub(crate) fn get_effective_uid() -> Result<u32, error::Error> {
Ok(if is_elevated() { 0 } else { NON_ELEVATED_UID })
}
pub(crate) fn get_effective_gid() -> Result<u32, error::Error> {
Ok(if is_elevated() { 0 } else { NON_ELEVATED_GID })
}
pub(crate) fn get_current_username() -> Result<String, error::Error> {
let username = whoami::username().map_err(std::io::Error::from)?;
Ok(username)
}
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
// TODO(windows): implement some version of this for Windows
Ok(vec![])
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
// TODO(windows): implement some version of this for Windows
Ok(vec![])
}
#[expect(clippy::unnecessary_wraps)]
pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
// TODO(windows): implement some version of this for Windows
Ok(vec![])
}
+99
View File
@@ -0,0 +1,99 @@
//! Terminal control utilities.
use crate::{error, openfiles, sys};
/// Encapsulates the state of a controlled terminal.
pub struct TerminalControl {
prev_fg_pid: Option<sys::process::ProcessId>,
}
impl TerminalControl {
/// Acquire the terminal for the shell.
pub fn acquire() -> Result<Self, error::Error> {
// Mask out SIGTTOU *first*. If `lead_new_process_group` succeeds in
// moving us into a new process group, the subsequent `tcsetpgrp` call
// in `move_self_to_foreground` is a "write to the controlling
// terminal from a background process," which the kernel signals with
// SIGTTOU. The default action for SIGTTOU is to stop the process,
// leaving brush (and any downstream reads from the terminal) hung.
// Installing the SIG_IGN handler before the tcsetpgrp makes that call
// succeed instead of stopping us.
sys::signal::mask_sigttou()?;
let prev_fg_pid = sys::terminal::get_foreground_pid();
// Break out into new process group.
// TODO(jobs): Investigate why this sometimes fails with EPERM.
let _ = sys::signal::lead_new_process_group();
// Take ownership.
sys::terminal::move_self_to_foreground()?;
Ok(Self { prev_fg_pid })
}
fn try_release(&mut self) {
// Restore the previous foreground process group.
if let Some(pid) = self.prev_fg_pid
&& sys::terminal::move_to_foreground(pid).is_ok()
{
self.prev_fg_pid = None;
}
}
}
impl Drop for TerminalControl {
fn drop(&mut self) {
self.try_release();
}
}
/// Describes high-level terminal settings that can be requested.
#[derive(Default, bon::Builder)]
pub struct Settings {
/// Whether to enable input echoing.
pub echo_input: Option<bool>,
/// Whether to enable line input (sometimes known as canonical mode).
pub line_input: Option<bool>,
/// Whether to disable interrupt signals and instead yield the control characters.
pub interrupt_signals: Option<bool>,
/// Whether to output newline characters as CRLF pairs.
pub output_nl_as_nlcr: Option<bool>,
}
/// Guard that automatically restores terminal settings on drop.
pub struct AutoModeGuard {
initial: sys::terminal::Config,
file: openfiles::OpenFile,
}
impl AutoModeGuard {
/// Creates a new `AutoModeGuard` for the given file.
///
/// # Arguments
///
/// * `file` - The file representing the terminal to control.
pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
let initial = sys::terminal::Config::from_term(&file)?;
Ok(Self { initial, file })
}
/// Applies the given terminal settings.
///
/// # Arguments
///
/// * `settings` - The terminal settings to apply.
pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
let mut config = sys::terminal::Config::from_term(&self.file)?;
config.update(settings);
config.apply_to_term(&self.file)?;
Ok(())
}
}
impl Drop for AutoModeGuard {
fn drop(&mut self) {
let _ = self.initial.apply_to_term(&self.file);
}
}
+37
View File
@@ -0,0 +1,37 @@
//! Shell test conditional expressions
use crate::{ExecutionParameters, Shell, error, extendedtests, extensions};
/// Evaluate the given test expression within the provided shell and
/// execution context. Returns true if the expression evaluates to true,
/// false otherwise.
///
/// # Arguments
///
/// * `expr` - The test expression to evaluate.
/// * `shell` - The shell context in which to evaluate the expression.
/// * `params` - The execution parameters to use during evaluation.
pub fn eval_expr(
expr: &brush_parser::ast::TestExpr,
shell: &mut Shell<impl extensions::ShellExtensions>,
params: &ExecutionParameters,
) -> Result<bool, error::Error> {
match expr {
brush_parser::ast::TestExpr::False => Ok(false),
brush_parser::ast::TestExpr::Literal(s) => Ok(!s.is_empty()),
brush_parser::ast::TestExpr::And(left, right) => {
Ok(eval_expr(left, shell, params)? && eval_expr(right, shell, params)?)
}
brush_parser::ast::TestExpr::Or(left, right) => {
Ok(eval_expr(left, shell, params)? || eval_expr(right, shell, params)?)
}
brush_parser::ast::TestExpr::Not(expr) => Ok(!eval_expr(expr, shell, params)?),
brush_parser::ast::TestExpr::Parenthesized(expr) => eval_expr(expr, shell, params),
brush_parser::ast::TestExpr::UnaryTest(op, operand) => {
extendedtests::apply_unary_predicate_to_str(op, operand, shell, params)
}
brush_parser::ast::TestExpr::BinaryTest(op, left, right) => {
extendedtests::apply_binary_predicate_to_strs(op, left.as_str(), right.as_str(), shell)
}
}
}
+137
View File
@@ -0,0 +1,137 @@
//! Command timing
use crate::error;
struct StopwatchTime {
now: std::time::SystemTime,
self_user: std::time::Duration,
self_system: std::time::Duration,
children_user: std::time::Duration,
children_system: std::time::Duration,
}
impl StopwatchTime {
#[allow(clippy::unchecked_time_subtraction)]
fn minus(&self, other: &Self) -> Result<StopwatchTiming, error::Error> {
let user = (self.self_user - other.self_user) + (self.children_user - other.children_user);
let system =
(self.self_system - other.self_system) + (self.children_system - other.children_system);
Ok(StopwatchTiming {
wall: self.now.duration_since(other.now)?,
user,
system,
})
}
}
pub(crate) struct Stopwatch {
start: StopwatchTime,
}
impl Stopwatch {
pub fn stop(&self) -> Result<StopwatchTiming, error::Error> {
let end = get_current_stopwatch_time()?;
end.minus(&self.start)
}
}
pub(crate) struct StopwatchTiming {
pub wall: std::time::Duration,
pub user: std::time::Duration,
pub system: std::time::Duration,
}
pub(crate) fn start_timing() -> Result<Stopwatch, error::Error> {
Ok(Stopwatch {
start: get_current_stopwatch_time()?,
})
}
fn get_current_stopwatch_time() -> Result<StopwatchTime, error::Error> {
let now = std::time::SystemTime::now();
let (self_user, self_system) = crate::sys::resource::get_self_user_and_system_time()?;
let (children_user, children_system) =
crate::sys::resource::get_children_user_and_system_time()?;
Ok(StopwatchTime {
now,
self_user,
self_system,
children_user,
children_system,
})
}
/// Format the given duration in a non-POSIX-y way.
///
/// # Arguments
///
/// * `duration` - The duration to format.
pub fn format_duration_non_posixly(duration: &std::time::Duration) -> String {
let minutes = duration.as_secs() / 60;
let seconds = duration.as_secs() % 60;
let millis = duration.subsec_millis();
format!("{minutes}m{seconds}.{millis:03}s")
}
/// Format the given duration in a POSIX-y way.
///
/// # Arguments
///
/// * `duration` - The duration to format.
pub fn format_duration_posixly(duration: &std::time::Duration) -> String {
let seconds = duration.as_secs();
let ten_millis = duration.subsec_millis() / 10;
format!("{seconds}.{ten_millis:02}")
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_format_time() {
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(0)),
"0m0.000s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(1)),
"0m0.001s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(123)),
"0m0.123s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(1234)),
"0m1.234s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(12345)),
"0m12.345s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(123_456)),
"2m3.456s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_millis(1_234_567)),
"20m34.567s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_micros(1)),
"0m0.000s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_micros(999)),
"0m0.000s"
);
assert_eq!(
format_duration_non_posixly(&Duration::from_micros(1001)),
"0m0.001s"
);
}
}
+20
View File
@@ -0,0 +1,20 @@
//! Trace utilities
/// Trace category for command execution.
pub const COMMANDS: &str = "commands";
/// Trace category for completion.
pub const COMPLETION: &str = "completion";
/// Trace category for word expansion.
pub const EXPANSION: &str = "expansion";
/// Trace category for function calls.
pub const FUNCTIONS: &str = "functions";
/// Trace category for user input.
pub const INPUT: &str = "input";
/// Trace category for job control.
pub const JOBS: &str = "jobs";
/// Trace category for parsing.
pub const PARSE: &str = "parse";
/// Trace category for shell patterns.
pub const PATTERN: &str = "pattern";
/// Trace category for unimplemented behavior.
pub const UNIMPLEMENTED: &str = "unimplemented";
+235
View File
@@ -0,0 +1,235 @@
//! Facilities for configuring trap handlers.
use std::str::FromStr;
use std::{collections::HashMap, fmt::Display};
use itertools::Itertools as _;
use crate::{error, sys};
/// Type of signal that can be trapped in the shell.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TrapSignal {
/// A system signal.
Signal(sys::signal::Signal),
/// The `DEBUG` trap.
Debug,
/// The `ERR` trap.
Err,
/// The `EXIT` trap.
Exit,
/// The `RETURN` trp.
Return,
}
#[cfg(feature = "serde")]
impl serde::Serialize for TrapSignal {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for TrapSignal {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::try_from(s.as_str()).map_err(serde::de::Error::custom)
}
}
impl Display for TrapSignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl TrapSignal {
/// Returns all possible values of [`TrapSignal`].
pub fn iterator() -> impl Iterator<Item = Self> {
const SIGNALS: &[TrapSignal] = &[TrapSignal::Debug, TrapSignal::Err, TrapSignal::Exit];
let iter = itertools::chain!(
SIGNALS.iter().copied(),
sys::signal::Signal::iterator().map(TrapSignal::Signal)
);
iter
}
/// Converts [`TrapSignal`] into its corresponding signal name as a [`&'static str`](str)
pub const fn as_str(self) -> &'static str {
match self {
Self::Signal(s) => s.as_str(),
Self::Debug => "DEBUG",
Self::Err => "ERR",
Self::Exit => "EXIT",
Self::Return => "RETURN",
}
}
}
/// Formats [`Iterator<Item = TrapSignal>`](TrapSignal) to the provided writer.
///
/// # Arguments
///
/// * `f` - Any type that implements [`std::io::Write`].
/// * `it` - An iterator over the signals that will be formatted into the `f`.
pub fn format_signals(
mut f: impl std::io::Write,
it: impl Iterator<Item = TrapSignal>,
) -> Result<(), error::Error> {
let it = it
.filter_map(|s| i32::try_from(s).ok().map(|n| (s, n)))
.sorted_by(|a, b| Ord::cmp(&a.1, &b.1))
.format_with("\n", |s, f| f(&format_args!("{}) {}", s.1, s.0)));
write!(f, "{it}")?;
Ok(())
}
// implement s.parse::<TrapSignal>()
impl FromStr for TrapSignal {
type Err = error::Error;
fn from_str(s: &str) -> Result<Self, <Self as FromStr>::Err> {
if let Ok(n) = s.parse::<i32>() {
Self::try_from(n)
} else {
Self::try_from(s)
}
}
}
// from a signal number
impl TryFrom<i32> for TrapSignal {
type Error = error::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
// NOTE: DEBUG and ERR are real-time signals, defined based on NSIG or SIGRTMAX (is not
// available on bsd-like systems),
// and don't have persistent numbers across platforms, so we skip them here.
Ok(match value {
0 => Self::Exit,
value => Self::Signal(
sys::signal::Signal::try_from(value)
.map_err(|_| error::ErrorKind::InvalidSignal(value.to_string()))?,
),
})
}
}
// from a signal name
impl TryFrom<&str> for TrapSignal {
type Error = error::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
#[allow(unused_mut, reason = "only mutated on some platforms")]
let mut s = value.to_ascii_uppercase();
Ok(match s.as_str() {
"DEBUG" => Self::Debug,
"ERR" => Self::Err,
"EXIT" => Self::Exit,
"RETURN" => Self::Return,
_ => {
// Bash compatibility:
// support for signal names without the `SIG` prefix, for example `HUP` -> `SIGHUP`
if !s.starts_with("SIG") {
s.insert_str(0, "SIG");
}
sys::signal::Signal::from_str(s.as_str())
.map(TrapSignal::Signal)
.map_err(|_| error::ErrorKind::InvalidSignal(value.into()))?
}
})
}
}
/// Error type used when failing to convert a `TrapSignal` to a number.
#[derive(Debug, Clone, Copy)]
pub struct TrapSignalNumberError;
impl TryFrom<TrapSignal> for i32 {
type Error = TrapSignalNumberError;
fn try_from(value: TrapSignal) -> Result<Self, Self::Error> {
Ok(match value {
TrapSignal::Signal(s) => s as Self,
TrapSignal::Exit => 0,
_ => return Err(TrapSignalNumberError),
})
}
}
/// A handler for a trap signal.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrapHandler {
/// The source text of the command to invoke.
pub command: String,
/// Source information for where the trap handler was defined.
pub source_info: crate::SourceInfo,
}
/// Configuration for trap handlers in the shell.
#[derive(Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TrapHandlerConfig {
/// Registered handlers for traps; maps signal type to command.
handlers: HashMap<TrapSignal, TrapHandler>,
}
impl TrapHandlerConfig {
/// Iterates over the registered handlers for trap signals.
pub fn iter_handlers(&self) -> impl Iterator<Item = (TrapSignal, &TrapHandler)> {
self.handlers
.iter()
.map(|(signal, handler)| (*signal, handler))
}
/// Tries to find the handler associated with the given signal.
///
/// # Arguments
///
/// * `signal_type` - The type of signal to get the handler for.
pub fn get_handler(&self, signal_type: TrapSignal) -> Option<&TrapHandler> {
self.handlers.get(&signal_type)
}
/// Returns whether a handler is registered for the given signal.
pub fn handles(&self, signal_type: TrapSignal) -> bool {
self.handlers.contains_key(&signal_type)
}
/// Registers a handler for a trap signal.
///
/// # Arguments
///
/// * `signal_type` - The type of signal to register a handler for.
/// * `command` - The command to execute when the signal is trapped.
/// * `source_info` - The source info for where the trap handler was defined.
pub fn register_handler(
&mut self,
signal_type: TrapSignal,
command: String,
source_info: crate::SourceInfo,
) {
let _ = self.handlers.insert(
signal_type,
TrapHandler {
command,
source_info,
},
);
}
/// Removes handlers for a trap signal.
///
/// # Arguments
///
/// * `signal_type` - The type of signal to remove handlers for.
pub fn remove_handlers(&mut self, signal_type: TrapSignal) {
self.handlers.remove(&signal_type);
}
}
File diff suppressed because it is too large Load Diff
+724
View File
@@ -0,0 +1,724 @@
use std::collections::BTreeMap;
use std::path::PathBuf;
use rand::RngExt as _;
use crate::shell::ShellState;
use crate::{Shell, ShellValue, ShellVariable, error, extensions, sys, variables};
const BASH_MAJOR: u32 = 5;
const BASH_MINOR: u32 = 2;
const BASH_PATCH: u32 = 37;
const BASH_BUILD: u32 = 1;
const BASH_RELEASE: &str = "release";
const BASH_MACHINE: &str = "unknown";
const DEFAULT_LINENO: usize = 1;
/// Inherit environment variables from the host process into the shell's environment.
///
/// # Arguments
///
/// * `shell` - The shell instance to inherit environment variables into.
pub(crate) fn inherit_env_vars(
shell: &mut Shell<impl extensions::ShellExtensions>,
) -> Result<(), error::Error> {
for (k, v) in sys::env::get_host_env_vars() {
// See if it's a function exported by an ancestor process.
if let Some(func_name) = k.strip_prefix("BASH_FUNC_")
&& let Some(func_name) = func_name.strip_suffix("%%")
{
// Intentionally best-effort; don't fail out of the shell if we can't
// parse an incoming function.
if shell.define_func_from_str(func_name, v.as_str()).is_ok()
&& let Some(func) = shell.func_mut(func_name)
{
func.export();
}
continue;
}
// Special case OLDPWD for bash compatibility.
if k == "OLDPWD" {
continue;
}
let mut var = ShellVariable::new(ShellValue::String(v));
var.export();
shell.env_mut().set_global(k, var)?;
}
Ok(())
}
#[expect(clippy::too_many_lines)]
pub(crate) fn init_well_known_vars(
shell: &mut Shell<impl extensions::ShellExtensions>,
) -> Result<(), error::Error> {
let shell_version = shell.version().map(ToString::to_string);
shell.env_mut().set_global(
"BRUSH_VERSION",
ShellVariable::new(shell_version.unwrap_or_default()),
)?;
// BASH
if let Some(shell_name) = shell.current_shell_name().map(|s| s.to_string()) {
shell
.env_mut()
.set_global("BASH", ShellVariable::new(shell_name.clone()))?;
// Initialize $_ to the shell name ($0).
shell.update_last_arg_variable(Some(shell_name));
}
// BASHOPTS
let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic {
getter: |shell| shell.options().shopt_optstr().into(),
setter: |_| (),
});
bashopts_var.set_readonly();
shell.env_mut().set_global("BASHOPTS", bashopts_var)?;
// BASHPID
#[cfg(not(target_family = "wasm"))]
{
let mut bashpid_var =
ShellVariable::new(ShellValue::String(std::process::id().to_string()));
bashpid_var.treat_as_integer();
shell.env_mut().set_global("BASHPID", bashpid_var)?;
}
// BASH_ALIASES
shell.env_mut().set_global(
"BASH_ALIASES",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
let values = variables::ArrayLiteral(
shell
.aliases()
.iter()
.map(|(k, v)| (Some(k.to_owned()), v.to_owned()))
.collect::<Vec<_>>(),
);
ShellValue::associative_array_from_literals(values)
.unwrap_or_else(|_error| ShellValue::AssociativeArray(BTreeMap::new()))
},
setter: |_| (),
}),
)?;
// BASH_ARGC
shell.env_mut().set_global(
"BASH_ARGC",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_bash_argc_value(shell),
setter: |_| (),
}),
)?;
// BASH_ARGV
shell.env_mut().set_global(
"BASH_ARGV",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_bash_argv_value(shell),
setter: |_| (),
}),
)?;
// BASH_ARGV0
shell.env_mut().set_global(
"BASH_ARGV0",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
let argv0 = shell.current_shell_name().unwrap_or_default();
argv0.to_string().into()
},
// TODO(vars): implement updating BASH_ARGV0
setter: |_| (),
}),
)?;
// TODO(vars): implement mutation of BASH_CMDS
shell.env_mut().set_global(
"BASH_CMDS",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
shell
.program_location_cache()
.to_value()
.unwrap_or_else(|_error| ShellValue::AssociativeArray(BTreeMap::new()))
},
setter: |_| (),
}),
)?;
// TODO(vars): implement BASH_COMMAND
// TODO(vars): implement BASH_EXECUTION_STRING
// BASH_LINENO
shell.env_mut().set_global(
"BASH_LINENO",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_bash_lineno_value(shell),
setter: |_| (),
}),
)?;
// BASH_SOURCE
shell.env_mut().set_global(
"BASH_SOURCE",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_bash_source_value(shell),
setter: |_| (),
}),
)?;
// BASH_SUBSHELL
shell.env_mut().set_global(
"BASH_SUBSHELL",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| shell.depth().to_string().into(),
setter: |_| (),
}),
)?;
// BASH_VERSINFO
let mut bash_versinfo_var = ShellVariable::new(ShellValue::indexed_array_from_strs(
[
BASH_MAJOR.to_string().as_str(),
BASH_MINOR.to_string().as_str(),
BASH_PATCH.to_string().as_str(),
BASH_BUILD.to_string().as_str(),
BASH_RELEASE,
BASH_MACHINE,
]
.as_slice(),
));
bash_versinfo_var.set_readonly();
shell
.env_mut()
.set_global("BASH_VERSINFO", bash_versinfo_var)?;
// BASH_VERSION
// This is the Bash interface version. See BRUSH_VERSION for its implementation version.
shell.env_mut().set_global(
"BASH_VERSION",
ShellVariable::new(std::format!(
"{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}"
)),
)?;
// COMP_WORDBREAKS
let mut default_comp_wordbreaks = String::from(" \t\n\"\'><=;|&(:");
if shell.options().enable_hostname_completion {
default_comp_wordbreaks.push('@');
}
shell.env_mut().set_global(
"COMP_WORDBREAKS",
ShellVariable::new(default_comp_wordbreaks),
)?;
// DIRSTACK
shell.env_mut().set_global(
"DIRSTACK",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
shell
.directory_stack()
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>()
.into()
},
setter: |_| (),
}),
)?;
// EPOCHREALTIME
shell.env_mut().set_global(
"EPOCHREALTIME",
ShellVariable::new(ShellValue::Dynamic {
getter: |_shell| {
let now = std::time::SystemTime::now();
let since_epoch = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
since_epoch.as_secs_f64().to_string().into()
},
setter: |_| (),
}),
)?;
// EPOCHSECONDS
shell.env_mut().set_global(
"EPOCHSECONDS",
ShellVariable::new(ShellValue::Dynamic {
getter: |_shell| {
let now = std::time::SystemTime::now();
let since_epoch = now
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
since_epoch.as_secs().to_string().into()
},
setter: |_| (),
}),
)?;
// EUID
if let Ok(euid) = sys::users::get_effective_uid() {
let mut euid_var = ShellVariable::new(ShellValue::String(format!("{euid}")));
euid_var.treat_as_integer().set_readonly();
shell.env_mut().set_global("EUID", euid_var)?;
}
// FUNCNAME
shell.env_mut().set_global(
"FUNCNAME",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_funcname_value(shell),
setter: |_| (),
}),
)?;
// GROUPS
// N.B. We could compute this up front, but we choose to make it dynamic so that we
// don't have to make costly system calls if the user never accesses it.
shell.env_mut().set_global(
"GROUPS",
ShellVariable::new(ShellValue::Dynamic {
getter: |_shell| {
let groups = get_current_user_gids();
ShellValue::indexed_array_from_strings(
groups.into_iter().map(|gid| gid.to_string()),
)
},
setter: |_| (),
}),
)?;
// HISTCMD
let mut histcmd_var = ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
shell
.history()
.map_or_else(|| "0".into(), |h| h.count().to_string().into())
},
setter: |_| (),
});
histcmd_var.treat_as_integer();
shell.env_mut().set_global("HISTCMD", histcmd_var)?;
// HISTFILE (if not already set)
if !shell.env().is_set("HISTFILE")
&& let Some(home_dir) = shell.home_dir()
{
let histfile = home_dir.join(".brush_history");
shell.env_mut().set_global(
"HISTFILE",
ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
)?;
}
// HOSTNAME
shell.env_mut().set_global(
"HOSTNAME",
ShellVariable::new(
sys::network::get_hostname()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
),
)?;
// HOSTTYPE
shell.env_mut().set_global(
"HOSTTYPE",
ShellVariable::new(std::env::consts::ARCH.to_string()),
)?;
// IFS
shell
.env_mut()
.set_global("IFS", ShellVariable::new(" \t\n"))?;
// LINENO
shell.env_mut().set_global(
"LINENO",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| get_lineno(shell).to_string().into(),
setter: |_| (),
}),
)?;
// MACHTYPE
shell
.env_mut()
.set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE))?;
// OLDPWD (initialization)
if !shell.env().is_set("OLDPWD") {
let mut oldpwd_var =
ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped));
oldpwd_var.export();
shell.env_mut().set_global("OLDPWD", oldpwd_var)?;
}
// OPTERR
shell
.env_mut()
.set_global("OPTERR", ShellVariable::new("1"))?;
// OPTIND
let mut optind_var = ShellVariable::new("1");
optind_var.treat_as_integer();
shell.env_mut().set_global("OPTIND", optind_var)?;
// OSTYPE
// Match bash's conventional OSTYPE on each platform so that shell scripts
// branching on `[[ $OSTYPE == darwin* ]]` / `linux-gnu*` etc. (Homebrew
// shellenv, nvm, asdf, ...) take the expected path. Real bash includes a
// kernel-version suffix on macOS/BSDs (e.g. `darwin24`); we omit the
// suffix for now since the common patterns all use prefix matching.
let os_type = match std::env::consts::OS {
"linux" => "linux-gnu",
"android" => "linux-android",
"macos" | "ios" | "tvos" | "watchos" | "visionos" => "darwin",
"freebsd" => "freebsd",
"netbsd" => "netbsd",
"openbsd" => "openbsd",
"dragonfly" => "dragonfly",
"solaris" | "illumos" => "solaris",
"windows" => "windows",
_ => "unknown",
};
shell
.env_mut()
.set_global("OSTYPE", ShellVariable::new(os_type))?;
// PATH (if not already set)
if !shell.env().is_set("PATH") {
let default_path_str = std::env::join_paths(sys::fs::get_default_executable_search_paths())
.unwrap_or_else(|_| PathBuf::from("").into());
shell
.env_mut()
.set_global("PATH", ShellVariable::new(default_path_str))?;
}
// PIPESTATUS
// TODO(well-known-vars): Investigate what happens if this gets unset.
// TODO(well-known-vars): Investigate if this needs to be saved/preserved across prompt display.
shell.env_mut().set_global(
"PIPESTATUS",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
ShellValue::indexed_array_from_strings(
shell.last_pipeline_statuses().iter().map(|s| s.to_string()),
)
},
setter: |_| (),
}),
)?;
// PPID
if let Some(ppid) = sys::terminal::get_parent_process_id() {
let mut ppid_var = ShellVariable::new(ppid.to_string());
ppid_var.treat_as_integer().set_readonly();
shell.env_mut().set_global("PPID", ppid_var)?;
}
// RANDOM
let mut random_var = ShellVariable::new(ShellValue::Dynamic {
getter: get_random_value,
setter: |_| (),
});
random_var.treat_as_integer();
shell.env_mut().set_global("RANDOM", random_var)?;
// SECONDS
shell.env_mut().set_global(
"SECONDS",
ShellVariable::new(ShellValue::Dynamic {
getter: |shell| {
let now = std::time::SystemTime::now();
let since_last = now
.duration_since(shell.last_stopwatch_time())
.unwrap_or_default();
let total_seconds = since_last.as_secs() + u64::from(shell.last_stopwatch_offset());
total_seconds.to_string().into()
},
// TODO(vars): implement updating SECONDS
setter: |_| (),
}),
)?;
// SHELL (if not already set)
if !shell.env().is_set("SHELL") {
// Per docs, this should be the user's default login shell -- not the current shell.
if let Some(default_shell) = sys::users::get_current_user_default_shell() {
shell.env_mut().set_global(
"SHELL",
ShellVariable::new(default_shell.to_string_lossy().to_string()),
)?;
}
}
// SHELLOPTS
let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic {
getter: |shell| shell.options().seto_optstr().into(),
setter: |_| (),
});
shellopts_var.set_readonly();
shell.env_mut().set_global("SHELLOPTS", shellopts_var)?;
// SHLVL
let input_shlvl = shell.env_str("SHLVL").unwrap_or_else(|| "0".into());
let updated_shlvl = input_shlvl.as_ref().parse::<u32>().unwrap_or(0) + 1;
let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string());
shlvl_var.export();
shell.env_mut().set_global("SHLVL", shlvl_var)?;
// SRANDOM
let mut random_var = ShellVariable::new(ShellValue::Dynamic {
getter: get_srandom_value,
setter: |_| (),
});
random_var.treat_as_integer();
shell.env_mut().set_global("SRANDOM", random_var)?;
// PS1 / PS2
if shell.options().interactive {
if !shell.env().is_set("PS1") {
shell
.env_mut()
.set_global("PS1", ShellVariable::new(r"\s-\v\$ "))?;
}
if !shell.env().is_set("PS2") {
shell
.env_mut()
.set_global("PS2", ShellVariable::new("> "))?;
}
}
// PS4
if !shell.env().is_set("PS4") {
shell
.env_mut()
.set_global("PS4", ShellVariable::new("+ "))?;
}
//
// PWD
//
// Reflect our actual working directory. There's a chance
// we inherited an out-of-sync version of the variable. Future updates
// will be handled by set_working_dir().
//
let pwd = shell.working_dir().to_string_lossy().to_string();
let mut pwd_var = ShellVariable::new(pwd);
pwd_var.export();
shell.env_mut().set_global("PWD", pwd_var)?;
// UID
if let Ok(uid) = sys::users::get_current_uid() {
let mut uid_var = ShellVariable::new(ShellValue::String(format!("{uid}")));
uid_var.treat_as_integer().set_readonly();
shell.env_mut().set_global("UID", uid_var)?;
}
Ok(())
}
/// Returns a list of the current user's group IDs, with the effective GID at the front.
fn get_current_user_gids() -> Vec<u32> {
let mut groups = sys::users::get_user_group_ids().unwrap_or_default();
// If the effective GID is present but not in the first position in the list, then move
// it there.
if let Ok(gid) = sys::users::get_effective_gid() {
if let Some(index) = groups.iter().position(|&g| g == gid) {
if index > 0 {
// Move it to the front.
groups.remove(index);
groups.insert(0, gid);
}
}
}
groups
}
fn get_random_value(_shell: &dyn ShellState) -> ShellValue {
let mut rng = rand::rng();
let num = rng.random_range(0..32768);
let str = num.to_string();
str.into()
}
fn get_srandom_value(_shell: &dyn ShellState) -> ShellValue {
let mut rng = rand::rng();
let num: u32 = rng.random();
let str = num.to_string();
str.into()
}
fn get_funcname_value(shell: &dyn ShellState) -> variables::ShellValue {
let stack = shell.call_stack();
if stack.iter_function_calls().next().is_none() {
ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray)
} else {
// When in a function, include both functions and sourced scripts in the stack
stack
.iter()
.filter_map(|frame| match &frame.frame_type {
crate::callstack::FrameType::Function(func) => Some(func.function_name.as_str()),
crate::callstack::FrameType::Script(script) => {
// Only include sourced scripts, not run scripts
if matches!(script.call_type, crate::callstack::ScriptCallType::Source) {
Some("source")
} else {
None
}
}
crate::callstack::FrameType::TrapHandler(_)
| crate::callstack::FrameType::Eval
| crate::callstack::FrameType::CommandString
| crate::callstack::FrameType::InteractiveSession => None,
})
.collect::<Vec<_>>()
.into()
}
}
fn get_bash_lineno_value(shell: &dyn ShellState) -> variables::ShellValue {
let stack = shell.call_stack();
// BASH_LINENO[$i] contains the line number where FUNCNAME[$i] was called
// This is extracted from the call_site of each frame
if stack.iter_function_calls().next().is_none() {
ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray)
} else {
stack
.iter()
.enumerate()
.filter_map(|(frame_idx, frame)| match &frame.frame_type {
crate::callstack::FrameType::Function(..)
| crate::callstack::FrameType::Script(..) => {
let caller_idx = frame_idx + 1;
if caller_idx < stack.depth() {
let caller_frame = &stack[caller_idx];
Some(
caller_frame
.current_line()
.unwrap_or(DEFAULT_LINENO)
.to_string(),
)
} else {
None
}
}
crate::callstack::FrameType::TrapHandler(_)
| crate::callstack::FrameType::Eval
| crate::callstack::FrameType::CommandString
| crate::callstack::FrameType::InteractiveSession => None,
})
.collect::<Vec<_>>()
.into()
}
}
fn get_bash_source_value(shell: &dyn ShellState) -> variables::ShellValue {
let stack = shell.call_stack();
if stack.iter_function_calls().next().is_none() {
let top_frame = stack.iter_script_calls().next();
top_frame
.map_or_else(Vec::new, |frame| vec![frame.source_info.source.clone()])
.into()
} else {
// When in a function, include both functions and sourced scripts in the stack
// This mirrors the FUNCNAME array structure
stack
.iter()
.filter_map(|frame| match &frame.frame_type {
crate::callstack::FrameType::Function(func) => {
Some(func.function.source().source.clone())
}
crate::callstack::FrameType::Script(script) => {
// Only include sourced scripts (matching the "source" in FUNCNAME)
if matches!(script.call_type, crate::callstack::ScriptCallType::Source) {
Some(script.source_info.source.clone())
} else {
None
}
}
crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => {
None
}
crate::callstack::FrameType::CommandString
| crate::callstack::FrameType::InteractiveSession => None,
})
.collect::<Vec<_>>()
.into()
}
}
fn get_bash_argc_value(shell: &dyn ShellState) -> variables::ShellValue {
if !shell.options().enable_debugger {
return ShellValue::indexed_array_from_strs(&[]);
}
let stack = shell.call_stack();
stack
.iter()
.filter_map(|frame| match &frame.frame_type {
crate::callstack::FrameType::Function(..)
| crate::callstack::FrameType::Script(..)
| crate::callstack::FrameType::CommandString
| crate::callstack::FrameType::InteractiveSession => Some(frame.args.len().to_string()),
crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => None,
})
.collect::<Vec<_>>()
.into()
}
fn get_bash_argv_value(shell: &dyn ShellState) -> variables::ShellValue {
if !shell.options().enable_debugger {
return ShellValue::indexed_array_from_strs(&[]);
}
let stack = shell.call_stack();
let mut argv = Vec::new();
for frame in stack.iter() {
let include = match &frame.frame_type {
crate::callstack::FrameType::Function(..)
| crate::callstack::FrameType::Script(..)
| crate::callstack::FrameType::CommandString
| crate::callstack::FrameType::InteractiveSession => true,
crate::callstack::FrameType::TrapHandler(_) | crate::callstack::FrameType::Eval => {
false
}
};
if include {
// Push args in reverse order per frame (last arg at lowest index = top of stack)
for arg in frame.args.iter().rev() {
argv.push(arg.clone());
}
}
}
argv.into()
}
fn get_lineno(shell: &dyn ShellState) -> usize {
shell
.call_stack()
.current_frame()
.and_then(|frame| frame.current_line())
.unwrap_or(DEFAULT_LINENO)
}