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