Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
[package]
|
||||
name = "brush-builtins"
|
||||
description = "Builtins for a POSIX/bash shell (used by brush-shell)"
|
||||
version = "0.2.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
|
||||
|
||||
[features]
|
||||
default = [
|
||||
'builtin.alias',
|
||||
'builtin.bg',
|
||||
'builtin.bind',
|
||||
'builtin.break',
|
||||
'builtin.builtin',
|
||||
'builtin.caller',
|
||||
'builtin.cd',
|
||||
'builtin.colon',
|
||||
'builtin.command',
|
||||
'builtin.compgen',
|
||||
'builtin.compopt',
|
||||
'builtin.complete',
|
||||
'builtin.continue',
|
||||
'builtin.declare',
|
||||
'builtin.dirs',
|
||||
'builtin.dot',
|
||||
'builtin.echo',
|
||||
'builtin.enable',
|
||||
'builtin.eval',
|
||||
'builtin.exec',
|
||||
'builtin.exit',
|
||||
'builtin.export',
|
||||
'builtin.false',
|
||||
'builtin.fc',
|
||||
'builtin.fg',
|
||||
'builtin.getopts',
|
||||
'builtin.hash',
|
||||
'builtin.help',
|
||||
'builtin.history',
|
||||
'builtin.jobs',
|
||||
'builtin.kill',
|
||||
'builtin.let',
|
||||
'builtin.mapfile',
|
||||
'builtin.popd',
|
||||
'builtin.printf',
|
||||
'builtin.pushd',
|
||||
'builtin.pwd',
|
||||
'builtin.read',
|
||||
'builtin.return',
|
||||
'builtin.set',
|
||||
'builtin.shift',
|
||||
'builtin.shopt',
|
||||
'builtin.suspend',
|
||||
'builtin.test',
|
||||
'builtin.times',
|
||||
'builtin.trap',
|
||||
'builtin.true',
|
||||
'builtin.type',
|
||||
'builtin.ulimit',
|
||||
'builtin.umask',
|
||||
'builtin.unalias',
|
||||
'builtin.unset',
|
||||
'builtin.wait',
|
||||
]
|
||||
|
||||
'builtin.alias' = []
|
||||
'builtin.bg' = []
|
||||
'builtin.bind' = []
|
||||
'builtin.break' = []
|
||||
'builtin.builtin' = []
|
||||
'builtin.caller' = []
|
||||
'builtin.cd' = []
|
||||
'builtin.colon' = []
|
||||
'builtin.command' = []
|
||||
'builtin.compgen' = []
|
||||
'builtin.complete' = []
|
||||
'builtin.compopt' = []
|
||||
'builtin.continue' = []
|
||||
'builtin.declare' = []
|
||||
'builtin.dirs' = []
|
||||
'builtin.dot' = []
|
||||
'builtin.echo' = []
|
||||
'builtin.enable' = []
|
||||
'builtin.eval' = []
|
||||
'builtin.exec' = []
|
||||
'builtin.exit' = []
|
||||
'builtin.export' = []
|
||||
'builtin.false' = []
|
||||
'builtin.fc' = []
|
||||
'builtin.fg' = []
|
||||
'builtin.getopts' = []
|
||||
'builtin.hash' = []
|
||||
'builtin.help' = []
|
||||
'builtin.history' = []
|
||||
'builtin.jobs' = []
|
||||
'builtin.kill' = []
|
||||
'builtin.let' = []
|
||||
'builtin.mapfile' = []
|
||||
'builtin.popd' = []
|
||||
'builtin.printf' = []
|
||||
'builtin.pushd' = []
|
||||
'builtin.pwd' = []
|
||||
'builtin.read' = []
|
||||
'builtin.return' = []
|
||||
'builtin.set' = []
|
||||
'builtin.shift' = []
|
||||
'builtin.shopt' = []
|
||||
'builtin.suspend' = []
|
||||
'builtin.test' = []
|
||||
'builtin.times' = []
|
||||
'builtin.trap' = []
|
||||
'builtin.true' = []
|
||||
'builtin.type' = []
|
||||
'builtin.ulimit' = []
|
||||
'builtin.umask' = []
|
||||
'builtin.unalias' = []
|
||||
'builtin.unset' = []
|
||||
'builtin.wait' = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
brush-core = { version = "^0.5.0", path = "../brush-core" }
|
||||
brush-parser = { version = "^0.4.0", path = "../brush-parser" }
|
||||
cfg-if = "1.0.4"
|
||||
chrono = "0.4.44"
|
||||
clap = { version = "4.6.0", features = ["derive", "wrap_help"] }
|
||||
fancy-regex = "0.17.0"
|
||||
futures = "0.3.32"
|
||||
itertools = "0.14.0"
|
||||
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"] }
|
||||
|
||||
[target.'cfg(any(unix, windows))'.dependencies]
|
||||
tokio = { version = "1.50.0", features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"process",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
"sync",
|
||||
] }
|
||||
uucore = { version = "0.8.0", default-features = false, features = ["format"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.31.2", features = [
|
||||
"fs",
|
||||
"process",
|
||||
"resource",
|
||||
"signal",
|
||||
"term",
|
||||
"user",
|
||||
] }
|
||||
rlimit = "0.11.0"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
procfs = "0.18.0"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.102"
|
||||
pretty_assertions = { version = "1.4.1", features = ["unstable"] }
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,55 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Manage aliases within the shell.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct AliasCommand {
|
||||
/// Print all defined aliases in a reusable format.
|
||||
#[arg(short = 'p')]
|
||||
print: bool,
|
||||
|
||||
/// List of aliases to display or update.
|
||||
#[arg(name = "name[=value]")]
|
||||
aliases: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for AliasCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut exit_code = ExecutionResult::success();
|
||||
|
||||
if self.print || self.aliases.is_empty() {
|
||||
for (name, value) in context.shell.aliases() {
|
||||
writeln!(context.stdout(), "alias {name}='{value}'")?;
|
||||
}
|
||||
} else {
|
||||
for alias in &self.aliases {
|
||||
if let Some((name, unexpanded_value)) = alias.split_once('=')
|
||||
&& !name.is_empty()
|
||||
{
|
||||
context
|
||||
.shell
|
||||
.aliases_mut()
|
||||
.insert(name.to_owned(), unexpanded_value.to_owned());
|
||||
} else if let Some(value) = context.shell.aliases().get(alias) {
|
||||
writeln!(context.stdout(), "alias {alias}='{value}'")?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {alias}: not found",
|
||||
context.command_name
|
||||
)?;
|
||||
exit_code = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Moves a job to run in the background.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct BgCommand {
|
||||
/// List of job specs to move to background.
|
||||
job_specs: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for BgCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut exit_code = ExecutionResult::success();
|
||||
|
||||
if !self.job_specs.is_empty() {
|
||||
for job_spec in &self.job_specs {
|
||||
if let Some(job) = context.shell.jobs_mut().resolve_job_spec(job_spec) {
|
||||
job.move_to_background()?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {}: no such job",
|
||||
context.command_name,
|
||||
job_spec
|
||||
)?;
|
||||
exit_code = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(job) = context.shell.jobs_mut().current_job_mut() {
|
||||
job.move_to_background()?;
|
||||
} else {
|
||||
writeln!(context.stderr(), "{}: no current job", context.command_name)?;
|
||||
exit_code = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
use clap::{Parser, ValueEnum};
|
||||
use itertools::Itertools as _;
|
||||
use std::{collections::HashMap, io::Write, str::FromStr as _, sync::Arc};
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use brush_core::{
|
||||
ExecutionExitCode, ExecutionResult, builtins,
|
||||
interfaces::{self, InputFunction, KeyAction, KeySequence},
|
||||
sys, trace_categories,
|
||||
};
|
||||
|
||||
/// Identifier for a keymap
|
||||
#[derive(Clone, ValueEnum)]
|
||||
enum BindKeyMap {
|
||||
#[clap(name = "emacs-standard", alias = "emacs")]
|
||||
EmacsStandard,
|
||||
#[clap(name = "emacs-meta")]
|
||||
EmacsMeta,
|
||||
#[clap(name = "emacs-ctlx")]
|
||||
EmacsCtlx,
|
||||
#[clap(name = "vi-command", aliases = &["vi", "vi-move"])]
|
||||
ViCommand,
|
||||
#[clap(name = "vi-insert")]
|
||||
ViInsert,
|
||||
}
|
||||
|
||||
impl BindKeyMap {
|
||||
const fn is_vi(&self) -> bool {
|
||||
matches!(self, Self::ViCommand | Self::ViInsert)
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
const fn is_emacs(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::EmacsStandard | Self::EmacsMeta | Self::EmacsCtlx
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect and modify key bindings and other input configuration.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct BindCommand {
|
||||
/// Name of key map to use.
|
||||
#[arg(short = 'm')]
|
||||
keymap: Option<BindKeyMap>,
|
||||
/// List functions.
|
||||
#[arg(short = 'l')]
|
||||
list_funcs: bool,
|
||||
/// List functions and bindings.
|
||||
#[arg(short = 'P')]
|
||||
list_funcs_and_bindings: bool,
|
||||
/// List functions and bindings in a format suitable for use as input.
|
||||
#[arg(short = 'p')]
|
||||
list_funcs_and_bindings_reusable: bool,
|
||||
/// List key sequences that invoke macros.
|
||||
#[arg(short = 'S')]
|
||||
list_key_seqs_that_invoke_macros: bool,
|
||||
/// List key sequences that invoke macros in a format suitable for use as input.
|
||||
#[arg(short = 's')]
|
||||
list_key_seqs_that_invoke_macros_reusable: bool,
|
||||
/// List variables.
|
||||
#[arg(short = 'V')]
|
||||
list_vars: bool,
|
||||
/// List variables in a format suitable for use as input.
|
||||
#[arg(short = 'v')]
|
||||
list_vars_reusable: bool,
|
||||
/// Find the keys bound to the given named function.
|
||||
#[arg(short = 'q', value_name = "FUNC_NAME")]
|
||||
query_func_bindings: Option<String>,
|
||||
/// Remove all bindings for the given named function.
|
||||
#[arg(short = 'u', value_name = "FUNC_NAME")]
|
||||
remove_func_bindings: Option<String>,
|
||||
/// Remove the binding for the given key sequence.
|
||||
#[arg(short = 'r', value_name = "KEY_SEQ")]
|
||||
remove_key_seq_binding: Option<String>,
|
||||
/// Import bindings from the given file.
|
||||
#[arg(short = 'f', value_name = "PATH")]
|
||||
bindings_file: Option<String>,
|
||||
/// Bind key sequence to command.
|
||||
#[arg(short = 'x', value_name = "BINDING")]
|
||||
key_seq_bindings: Vec<String>,
|
||||
/// List key sequence bindings.
|
||||
#[arg(short = 'X')]
|
||||
list_key_seq_bindings: bool,
|
||||
/// Key sequence binding to readline function or command.
|
||||
key_sequence: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum BindError {
|
||||
/// Unknown function specified.
|
||||
#[error("unknown function: {0}")]
|
||||
UnknownFunction(String),
|
||||
|
||||
/// Unknown key binding function.
|
||||
#[error("unknown key binding function: {0}")]
|
||||
UnknownKeyBindingFunction(String),
|
||||
|
||||
/// Unimplemented functionality.
|
||||
#[error("unimplemented: {0}")]
|
||||
Unimplemented(&'static str),
|
||||
|
||||
/// An I/O error occurred.
|
||||
#[error("I/O error occurred")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// A binding parse error occurred.
|
||||
#[error(transparent)]
|
||||
BindingParseError(#[from] brush_parser::BindingParseError),
|
||||
}
|
||||
|
||||
impl brush_core::BuiltinError for BindError {}
|
||||
|
||||
impl From<&BindError> for brush_core::ExecutionExitCode {
|
||||
fn from(_err: &BindError) -> Self {
|
||||
Self::GeneralError
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for BindCommand {
|
||||
type Error = BindError;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if let Some(key_bindings) = context.shell.key_bindings() {
|
||||
Ok(self.execute_impl(key_bindings, &context).await?)
|
||||
} else {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"bind: key bindings not supported in this config");
|
||||
|
||||
// Silently succeed when key bindings are unavailable (e.g., in
|
||||
// non-interactive mode or with an input backend that doesn't
|
||||
// yet support them).
|
||||
Ok(ExecutionExitCode::Success.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BindCommand {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn execute_impl(
|
||||
&self,
|
||||
bindings: &Arc<Mutex<dyn interfaces::KeyBindings>>,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<ExecutionResult, BindError> {
|
||||
let mut bindings = bindings.lock().await;
|
||||
|
||||
if self.list_funcs {
|
||||
for func in interfaces::InputFunction::iter() {
|
||||
writeln!(context.stdout(), "{func}")?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.list_funcs_and_bindings {
|
||||
display_funcs_and_bindings(&*bindings, context, false /* reusable? */)?;
|
||||
}
|
||||
|
||||
if self.list_funcs_and_bindings_reusable {
|
||||
display_funcs_and_bindings(&*bindings, context, true /* reusable? */)?;
|
||||
}
|
||||
|
||||
if self.list_key_seqs_that_invoke_macros {
|
||||
display_macros(&*bindings, context, false /* reusable? */)?;
|
||||
}
|
||||
|
||||
if self.list_key_seqs_that_invoke_macros_reusable {
|
||||
display_macros(&*bindings, context, true /* reusable? */)?;
|
||||
}
|
||||
|
||||
if self.list_vars {
|
||||
let options = &context.shell.completion_config().fallback_options;
|
||||
|
||||
// For now we'll just display a few items and show defaults.
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"mark-directories is set to `{}'",
|
||||
to_onoff(options.mark_directories)
|
||||
)?;
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"mark-symlinked-directories is set to `{}'",
|
||||
to_onoff(options.mark_symlinked_directories)
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.list_vars_reusable {
|
||||
let options = &context.shell.completion_config().fallback_options;
|
||||
|
||||
// For now we'll just display a few items and show defaults.
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"set mark-directories {}",
|
||||
to_onoff(options.mark_directories)
|
||||
)?;
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"set mark-symlinked-directories {}",
|
||||
to_onoff(options.mark_symlinked_directories)
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(func_str) = &self.query_func_bindings {
|
||||
let seqs = find_key_seqs_bound_to_function(&*bindings, func_str)?;
|
||||
|
||||
if !seqs.is_empty() {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{func_str} can be invoked via {}.",
|
||||
seqs.iter().map(|seq| std::format!("\"{seq}\"")).join(", ")
|
||||
)?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{func_str} is not bound to any keys.")?;
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(func_str) = &self.remove_func_bindings {
|
||||
let found_seqs = find_key_seqs_bound_to_function(&*bindings, func_str)?;
|
||||
|
||||
for seq in found_seqs {
|
||||
let _ = bindings.try_unbind(seq);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key_seq_str) = &self.remove_key_seq_binding {
|
||||
let key_seq = parse_key_sequence(key_seq_str)?;
|
||||
let _ = bindings.try_unbind(key_seq);
|
||||
}
|
||||
|
||||
if self.bindings_file.is_some() {
|
||||
return Err(BindError::Unimplemented("bind -f"));
|
||||
}
|
||||
|
||||
if self.list_key_seq_bindings {
|
||||
for (seq, action) in &bindings.get_current() {
|
||||
let KeyAction::ShellCommand(cmd) = action else {
|
||||
continue;
|
||||
};
|
||||
|
||||
writeln!(context.stdout(), "\"{seq}\" \"{cmd}\"")?;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.key_seq_bindings.is_empty() {
|
||||
if self.keymap.as_ref().is_some_and(|k| k.is_vi()) {
|
||||
// NOTE(vi): Quietly ignore since we don't support vi mode.
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
for key_seq_and_command in &self.key_seq_bindings {
|
||||
let (key_seq, command) = parse_key_sequence_and_shell_command(key_seq_and_command)?;
|
||||
bind_key_sequence_to_shell_cmd(&mut *bindings, key_seq, command)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key_sequence) = &self.key_sequence {
|
||||
if self.keymap.as_ref().is_some_and(|k| k.is_vi()) {
|
||||
// NOTE(vi): Quietly ignore since we don't support vi mode.
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
let (key_seq, target) = parse_key_sequence_and_readline_target(key_sequence.as_str())?;
|
||||
bind_key_sequence_to_readline_target(&mut *bindings, key_seq, target)?;
|
||||
}
|
||||
|
||||
drop(bindings);
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_key_sequence(input: &str) -> Result<interfaces::KeySequence, BindError> {
|
||||
// First trim any whitespace.
|
||||
let input = input.trim();
|
||||
|
||||
let parsed = brush_parser::readline_binding::parse_key_sequence(input)?;
|
||||
let abstract_seq = key_sequence_to_abstract_strokes(&parsed)?;
|
||||
|
||||
Ok(abstract_seq)
|
||||
}
|
||||
|
||||
fn parse_key_sequence_and_shell_command(
|
||||
input: &str,
|
||||
) -> Result<(interfaces::KeySequence, String), BindError> {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"parsing key binding entry: '{input}'"
|
||||
);
|
||||
|
||||
// First trim any whitespace.
|
||||
let input = input.trim();
|
||||
|
||||
// This should be something of the form:
|
||||
// "KEY-SEQUENCE": SHELL-COMMAND
|
||||
let binding = brush_parser::readline_binding::parse_key_sequence_shell_cmd_binding(input)?;
|
||||
let abstract_seq = key_sequence_to_abstract_strokes(&binding.seq)?;
|
||||
|
||||
Ok((abstract_seq, binding.shell_cmd))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code, reason = "not all variants implemented yet")]
|
||||
enum BindableReadlineTarget {
|
||||
Function(interfaces::InputFunction),
|
||||
Macro(interfaces::KeySequence),
|
||||
}
|
||||
|
||||
fn parse_key_sequence_and_readline_target(
|
||||
input: &str,
|
||||
) -> Result<(interfaces::KeySequence, BindableReadlineTarget), BindError> {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"parsing key binding entry: '{input}'"
|
||||
);
|
||||
|
||||
// First trim any whitespace.
|
||||
let input = input.trim();
|
||||
|
||||
// This should be of one of these forms:
|
||||
// "KEY-SEQUENCE":function-name
|
||||
// "KEY-SEQUENCE":readline-command
|
||||
let binding = brush_parser::readline_binding::parse_key_sequence_readline_binding(input)?;
|
||||
let abstract_seq = key_sequence_to_abstract_strokes(&binding.seq)?;
|
||||
|
||||
match binding.target {
|
||||
brush_parser::readline_binding::ReadlineTarget::Function(func_name) => {
|
||||
let func = parse_readline_function(func_name.as_str())?;
|
||||
Ok((abstract_seq, BindableReadlineTarget::Function(func)))
|
||||
}
|
||||
brush_parser::readline_binding::ReadlineTarget::Macro(target_seq_str) => {
|
||||
let parsed_target =
|
||||
brush_parser::readline_binding::parse_key_sequence(&target_seq_str)?;
|
||||
let abstract_target = key_sequence_to_abstract_strokes(&parsed_target)?;
|
||||
Ok((abstract_seq, BindableReadlineTarget::Macro(abstract_target)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_key_sequence_to_shell_cmd(
|
||||
bindings: &mut dyn interfaces::KeyBindings,
|
||||
key_sequence: interfaces::KeySequence,
|
||||
command: String,
|
||||
) -> Result<(), BindError> {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"binding key sequence: '{key_sequence}' => command '{command}'"
|
||||
);
|
||||
|
||||
bindings.bind(key_sequence, interfaces::KeyAction::ShellCommand(command))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bind_key_sequence_to_readline_target(
|
||||
bindings: &mut dyn interfaces::KeyBindings,
|
||||
key_sequence: interfaces::KeySequence,
|
||||
target: BindableReadlineTarget,
|
||||
) -> Result<(), BindError> {
|
||||
match target {
|
||||
BindableReadlineTarget::Function(func) => {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"binding key sequence: '{key_sequence}' => readline function '{func}'"
|
||||
);
|
||||
|
||||
if matches!(func, interfaces::InputFunction::ViEditingMode) {
|
||||
// NOTE(vi): We don't support vi mode; silently ignore.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bindings.bind(key_sequence, interfaces::KeyAction::DoInputFunction(func))?;
|
||||
Ok(())
|
||||
}
|
||||
BindableReadlineTarget::Macro(cmd_macro) => {
|
||||
tracing::debug!(target: trace_categories::INPUT,
|
||||
"binding key sequence: '{key_sequence}' => readline macro '{cmd_macro}'"
|
||||
);
|
||||
|
||||
bindings.define_macro(key_sequence, cmd_macro)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_sequence_to_abstract_strokes(
|
||||
seq: &brush_parser::readline_binding::KeySequence,
|
||||
) -> Result<interfaces::KeySequence, BindError> {
|
||||
let phys_strokes = brush_parser::readline_binding::key_sequence_to_strokes(seq)?;
|
||||
|
||||
// Lift from key codes to abstract keys.
|
||||
let mut abstract_strokes = vec![];
|
||||
let mut key_code_bytes = vec![];
|
||||
let mut uninterpretable = false;
|
||||
for mut phys_stroke in phys_strokes {
|
||||
let mut key = sys::input::try_get_key_from_key_code(phys_stroke.key_code.as_slice());
|
||||
|
||||
// If we couldn't interpret it directly but we see it starts with the escape character,
|
||||
// try to see if we can parse it as an Alt+<key> sequence.
|
||||
if key.is_none() && phys_stroke.key_code.len() > 1 && phys_stroke.key_code[0] == b'\x1b' {
|
||||
key = sys::input::try_get_key_from_key_code(&phys_stroke.key_code[1..]);
|
||||
if key.is_some() {
|
||||
phys_stroke.meta = true;
|
||||
}
|
||||
}
|
||||
|
||||
// When storing as bytes, apply control modifier to the key code.
|
||||
let mut raw_bytes = phys_stroke.key_code.clone();
|
||||
if phys_stroke.control {
|
||||
for byte in &mut raw_bytes {
|
||||
// Control characters are computed by ANDing with 0x1F
|
||||
*byte &= 0x1F;
|
||||
}
|
||||
}
|
||||
key_code_bytes.push(raw_bytes);
|
||||
|
||||
if let Some(key) = key {
|
||||
abstract_strokes.push(interfaces::KeyStroke {
|
||||
alt: phys_stroke.meta,
|
||||
control: phys_stroke.control,
|
||||
shift: false,
|
||||
key,
|
||||
});
|
||||
} else {
|
||||
uninterpretable = true;
|
||||
}
|
||||
}
|
||||
|
||||
if uninterpretable {
|
||||
Ok(interfaces::KeySequence::Bytes(key_code_bytes))
|
||||
} else {
|
||||
Ok(interfaces::KeySequence::Strokes(abstract_strokes))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_readline_function(func_name: &str) -> Result<interfaces::InputFunction, BindError> {
|
||||
interfaces::InputFunction::from_str(func_name)
|
||||
.map_err(|_err| BindError::UnknownKeyBindingFunction(func_name.to_owned()))
|
||||
}
|
||||
|
||||
const fn to_onoff(value: bool) -> &'static str {
|
||||
if value { "on" } else { "off" }
|
||||
}
|
||||
|
||||
fn display_funcs_and_bindings(
|
||||
bindings: &dyn interfaces::KeyBindings,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
reusable: bool,
|
||||
) -> Result<(), BindError> {
|
||||
let mut sequences_by_func: HashMap<InputFunction, Vec<KeySequence>> = HashMap::new();
|
||||
for (seq, action) in &bindings.get_current() {
|
||||
let KeyAction::DoInputFunction(func) = action else {
|
||||
continue;
|
||||
};
|
||||
|
||||
sequences_by_func
|
||||
.entry(func.clone())
|
||||
.or_default()
|
||||
.push(seq.clone());
|
||||
}
|
||||
|
||||
let sorted_funcs = interfaces::InputFunction::iter().sorted_by_key(|f| f.to_string());
|
||||
|
||||
for func in sorted_funcs {
|
||||
if let Some(seqs) = sequences_by_func.get(&func) {
|
||||
if reusable {
|
||||
for seq in seqs {
|
||||
writeln!(context.stdout(), "\"{seq}\": {func}")?;
|
||||
}
|
||||
} else {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{func} can be found on {}.",
|
||||
seqs.iter().map(|seq| std::format!("\"{seq}\"")).join(", ")
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
if reusable {
|
||||
writeln!(context.stdout(), "# {func} (not bound)")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{func} is not bound to any keys")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_macros(
|
||||
bindings: &dyn interfaces::KeyBindings,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
reusable: bool,
|
||||
) -> Result<(), BindError> {
|
||||
for (left, right) in bindings.get_macros() {
|
||||
if reusable {
|
||||
writeln!(context.stdout(), "\"{left}\": \"{right}\"")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{left} outputs {right}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_key_seqs_bound_to_function(
|
||||
bindings: &dyn interfaces::KeyBindings,
|
||||
func_str: &str,
|
||||
) -> Result<Vec<interfaces::KeySequence>, BindError> {
|
||||
let Ok(func_to_find) = InputFunction::from_str(func_str) else {
|
||||
return Err(BindError::UnknownFunction(func_str.to_owned()));
|
||||
};
|
||||
|
||||
let mut found_seqs = vec![];
|
||||
|
||||
for (seq, action) in &bindings.get_current() {
|
||||
if let KeyAction::DoInputFunction(func) = action
|
||||
&& *func == func_to_find
|
||||
{
|
||||
found_seqs.push(seq.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(found_seqs)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
|
||||
#[test]
|
||||
fn parse_example_key_sequence_and_readline_func() {
|
||||
let (key_seq, target) =
|
||||
parse_key_sequence_and_readline_target(r#""\C-a":beginning-of-line"#).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
key_seq,
|
||||
interfaces::KeySequence::Strokes(vec![interfaces::KeyStroke {
|
||||
alt: false,
|
||||
control: true,
|
||||
shift: false,
|
||||
key: interfaces::Key::Character('a'),
|
||||
}])
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
target,
|
||||
BindableReadlineTarget::Function(interfaces::InputFunction::BeginningOfLine)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_escape_char_key_binding() {
|
||||
let (key_seq, target) =
|
||||
parse_key_sequence_and_readline_target(r#""\er":transpose-chars"#).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
key_seq,
|
||||
interfaces::KeySequence::Strokes(vec![interfaces::KeyStroke {
|
||||
alt: true,
|
||||
control: false,
|
||||
shift: false,
|
||||
key: interfaces::Key::Character('r'),
|
||||
}])
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
target,
|
||||
BindableReadlineTarget::Function(interfaces::InputFunction::TransposeChars)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Breaks out of a control-flow loop.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct BreakCommand {
|
||||
/// If specified, indicates which nested loop to break out of.
|
||||
#[clap(default_value_t = 1)]
|
||||
which_loop: i8,
|
||||
}
|
||||
|
||||
impl builtins::Command for BreakCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
_context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// If specified, which_loop needs to be positive.
|
||||
if self.which_loop <= 0 {
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
result.next_control_flow = ExecutionControlFlow::BreakLoop {
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
levels: (self.which_loop - 1) as usize,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::BuiltinSet;
|
||||
|
||||
/// Extension trait that simplifies adding default builtins to a shell builder.
|
||||
pub trait ShellBuilderExt {
|
||||
/// Add default builtins to the shell being built.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `set` - The well-known set of built-ins to add.
|
||||
#[must_use]
|
||||
fn default_builtins(self, set: BuiltinSet) -> Self;
|
||||
}
|
||||
|
||||
impl<SE: brush_core::extensions::ShellExtensions, S: brush_core::ShellBuilderState> ShellBuilderExt
|
||||
for brush_core::ShellBuilder<SE, S>
|
||||
{
|
||||
fn default_builtins(self, set: BuiltinSet) -> Self {
|
||||
self.builtins(crate::default_builtins(set))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Directly invokes a built-in, without going through typical search order.
|
||||
#[derive(Default, Parser)]
|
||||
pub(crate) struct BuiltinCommand {
|
||||
#[clap(skip)]
|
||||
args: Vec<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for BuiltinCommand {
|
||||
fn set_declarations(&mut self, args: Vec<brush_core::CommandArg>) {
|
||||
self.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for BuiltinCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.args.is_empty() {
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
let args: Vec<_> = self.args.iter().skip(1).cloned().collect();
|
||||
if args.is_empty() {
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
let builtin_name = args[0].to_string();
|
||||
|
||||
if let Some(builtin) = context.shell.builtins().get(&builtin_name)
|
||||
&& !builtin.disabled
|
||||
{
|
||||
context.command_name = builtin_name;
|
||||
(builtin.execute_func)(context, args).await
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::BuiltinNotFound(builtin_name).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use brush_core::{ExecutionResult, builtins, callstack};
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
/// Return the context of the current subroutine call.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct CallerCommand {
|
||||
/// The number of call frames to go back.
|
||||
expr: Option<usize>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CallerCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
let stack = context.shell.call_stack();
|
||||
|
||||
// See how far back we need to look. Frame N represents the Nth caller
|
||||
// (e.g., 0 = immediate caller, 1 = caller's caller, etc.).
|
||||
let expr = self.expr.unwrap_or(0);
|
||||
|
||||
// Get all frames into a vector we can easily index into.
|
||||
let frames: Vec<_> = stack
|
||||
.iter()
|
||||
.filter(|frame| frame.frame_type.is_function() || frame.frame_type.is_script())
|
||||
.collect();
|
||||
|
||||
// Look for the last-known location in the parent of frame N.
|
||||
let Some(calling_frame) = frames.get(expr + 1) else {
|
||||
return Ok(ExecutionResult::general_error());
|
||||
};
|
||||
|
||||
let line = calling_frame.current_line().unwrap_or(1);
|
||||
let filename = &calling_frame.source_info.source;
|
||||
|
||||
// When the expr is provided, we display "LINE FUNCTION_NAME FILENAME"
|
||||
// When the expr is omitted, we only display "LINE FILENAME"
|
||||
if self.expr.is_some() {
|
||||
let function_name = match &calling_frame.frame_type {
|
||||
callstack::FrameType::Function(func_call) => func_call.name(),
|
||||
callstack::FrameType::Script(..) => "source".into(),
|
||||
_ => "".into(),
|
||||
};
|
||||
|
||||
writeln!(context.stdout(), "{line} {function_name} {filename}")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{line} {filename}")?;
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, error};
|
||||
|
||||
/// Change the current shell working directory.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct CdCommand {
|
||||
/// Force following symlinks.
|
||||
#[arg(short = 'L', overrides_with = "use_physical_dir")]
|
||||
force_follow_symlinks: bool,
|
||||
|
||||
/// Use physical dir structure without following symlinks.
|
||||
#[arg(short = 'P', overrides_with = "force_follow_symlinks")]
|
||||
use_physical_dir: bool,
|
||||
|
||||
/// Exit with non zero exit status if current working directory resolution fails.
|
||||
#[arg(short = 'e')]
|
||||
exit_on_failed_cwd_resolution: bool,
|
||||
|
||||
/// Show file with extended attributes as a dir with extended
|
||||
/// attributes.
|
||||
#[arg(short = '@')]
|
||||
file_with_xattr_as_dir: bool,
|
||||
|
||||
/// By default it is the value of the HOME shell variable. If `TARGET_DIR` is "-", it is
|
||||
/// converted to $OLDPWD.
|
||||
target_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CdCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
// TODO(cd): implement 'cd -@'
|
||||
if self.file_with_xattr_as_dir {
|
||||
return error::unimp("cd -@");
|
||||
}
|
||||
|
||||
let mut should_print = false;
|
||||
let mut target_dir = if let Some(target_dir) = &self.target_dir {
|
||||
// `cd -', equivalent to `cd $OLDPWD'
|
||||
if target_dir.as_os_str() == "-" {
|
||||
should_print = true;
|
||||
if let Some(oldpwd) = context.shell.env_str("OLDPWD") {
|
||||
PathBuf::from(oldpwd.to_string())
|
||||
} else {
|
||||
writeln!(context.stderr(), "OLDPWD not set")?;
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
} else {
|
||||
// TODO(cd): remove clone, and use temporary lifetime extension after rust 1.75
|
||||
target_dir.clone()
|
||||
}
|
||||
// `cd' without arguments is equivalent to `cd $HOME'
|
||||
} else {
|
||||
if let Some(home_var) = context.shell.env_str("HOME") {
|
||||
PathBuf::from(home_var.to_string())
|
||||
} else {
|
||||
writeln!(context.stderr(), "HOME not set")?;
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
};
|
||||
|
||||
if self.use_physical_dir
|
||||
|| context
|
||||
.shell
|
||||
.options()
|
||||
.do_not_resolve_symlinks_when_changing_dir
|
||||
{
|
||||
// -e is only relevant in physical mode.
|
||||
if self.exit_on_failed_cwd_resolution {
|
||||
return error::unimp("cd -e");
|
||||
}
|
||||
|
||||
target_dir = context.shell.absolute_path(target_dir).canonicalize()?;
|
||||
}
|
||||
|
||||
context.shell.set_working_dir(&target_dir)?;
|
||||
|
||||
// Bash compatibility
|
||||
// https://www.gnu.org/software/bash/manual/bash.html#index-cd
|
||||
// If a non-empty directory name from CDPATH is used, or if '-' is the first argument, and
|
||||
// the directory change is successful, the absolute pathname of the new working
|
||||
// directory is written to the standard output.
|
||||
if should_print {
|
||||
writeln!(context.stdout(), "{}", target_dir.display())?;
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use brush_core::{ExecutionResult, builtins, error};
|
||||
|
||||
/// No-op command.
|
||||
pub(crate) struct ColonCommand {}
|
||||
|
||||
impl builtins::SimpleCommand for ColonCommand {
|
||||
fn get_content(
|
||||
_name: &str,
|
||||
content_type: builtins::ContentType,
|
||||
_options: &builtins::ContentOptions,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
match content_type {
|
||||
builtins::ContentType::DetailedHelp => {
|
||||
Ok("Null command; always returns success.".into())
|
||||
}
|
||||
builtins::ContentType::ShortUsage => Ok(":: :".into()),
|
||||
builtins::ContentType::ShortDescription => Ok(": - Null command".into()),
|
||||
builtins::ContentType::ManPage => error::unimp("man page not yet implemented"),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute<SE: brush_core::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
|
||||
_context: brush_core::ExecutionContext<'_, SE>,
|
||||
_args: I,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use clap::Parser;
|
||||
use std::{fmt::Display, io::Write, path::Path};
|
||||
|
||||
use brush_core::{
|
||||
ExecutionResult, builtins, commands, pathsearch,
|
||||
sys::{self, fs::PathExt},
|
||||
};
|
||||
|
||||
/// Directly invokes an external command, without going through typical search order.
|
||||
#[derive(Default, Parser)]
|
||||
pub(crate) struct CommandCommand {
|
||||
/// Use default PATH value.
|
||||
#[arg(short = 'p')]
|
||||
pub use_default_path: bool,
|
||||
|
||||
/// Display a short description of the command.
|
||||
#[arg(short = 'v')]
|
||||
pub print_description: bool,
|
||||
|
||||
/// Display a more verbose description of the command.
|
||||
#[arg(short = 'V')]
|
||||
pub print_verbose_description: bool,
|
||||
|
||||
/// Command and arguments.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
pub command_and_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandCommand {
|
||||
fn command(&self) -> Option<&String> {
|
||||
self.command_and_args.first()
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for CommandCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
// Silently exit if no command was provided.
|
||||
if let Some(command_name) = self.command() {
|
||||
if self.print_description || self.print_verbose_description {
|
||||
if let Some(found_cmd) = Self::try_find_command(
|
||||
context.shell,
|
||||
command_name.as_str(),
|
||||
self.use_default_path,
|
||||
) {
|
||||
if self.print_description {
|
||||
writeln!(context.stdout(), "{found_cmd}")?;
|
||||
} else {
|
||||
match found_cmd {
|
||||
FoundCommand::Builtin(_name) => {
|
||||
writeln!(context.stdout(), "{command_name} is a shell builtin")?;
|
||||
}
|
||||
FoundCommand::External(path) => {
|
||||
writeln!(context.stdout(), "{command_name} is {path}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
if self.print_verbose_description {
|
||||
writeln!(context.stderr(), "command: {command_name}: not found")?;
|
||||
}
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
} else {
|
||||
self.execute_command(context, command_name, self.use_default_path)
|
||||
.await
|
||||
}
|
||||
} else {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum FoundCommand {
|
||||
Builtin(String),
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl Display for FoundCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Builtin(name) => write!(f, "{name}"),
|
||||
Self::External(path) => write!(f, "{path}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandCommand {
|
||||
fn try_find_command(
|
||||
shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
command_name: &str,
|
||||
use_default_path: bool,
|
||||
) -> Option<FoundCommand> {
|
||||
// Look in path.
|
||||
if sys::fs::contains_path_separator(command_name) {
|
||||
let candidate_path = shell.absolute_path(Path::new(command_name));
|
||||
if candidate_path.executable() {
|
||||
Some(FoundCommand::External(
|
||||
candidate_path.to_string_lossy().to_string(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
if let Some(builtin_cmd) = shell.builtins().get(command_name)
|
||||
&& !builtin_cmd.disabled
|
||||
{
|
||||
return Some(FoundCommand::Builtin(command_name.to_owned()));
|
||||
}
|
||||
|
||||
if use_default_path {
|
||||
let dirs = sys::fs::get_default_standard_utils_paths();
|
||||
|
||||
pathsearch::search_for_executable(dirs.iter(), command_name)
|
||||
.next()
|
||||
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()))
|
||||
} else {
|
||||
shell
|
||||
.find_first_executable_in_path_using_cache(command_name)
|
||||
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_command(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
command_name: &str,
|
||||
use_default_path: bool,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
command_name.clone_into(&mut context.command_name);
|
||||
let command_and_args = self.command_and_args.iter().map(|arg| arg.into()).collect();
|
||||
|
||||
let path_dirs = if use_default_path {
|
||||
Some(sys::fs::get_default_standard_utils_paths())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut cmd = commands::SimpleCommand::new(
|
||||
commands::ShellForCommand::ParentShell(context.shell),
|
||||
context.params,
|
||||
context.command_name,
|
||||
command_and_args,
|
||||
);
|
||||
cmd.use_functions = false;
|
||||
cmd.path_dirs = path_dirs;
|
||||
|
||||
let spawn_result = cmd.execute().await?;
|
||||
let wait_result = spawn_result.wait().await?;
|
||||
|
||||
Ok(wait_result.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
use clap::Parser;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write as _;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::completion::{self, CompleteAction, CompleteOption, Spec};
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins, error, escape};
|
||||
|
||||
#[derive(Parser)]
|
||||
struct CommonCompleteCommandArgs {
|
||||
/// Options governing the behavior of completions.
|
||||
#[arg(short = 'o')]
|
||||
options: Vec<CompleteOption>,
|
||||
|
||||
/// Actions to apply to generate completions.
|
||||
#[arg(short = 'A')]
|
||||
actions: Vec<CompleteAction>,
|
||||
|
||||
/// File glob pattern to be expanded to generate completions.
|
||||
#[arg(short = 'G', allow_hyphen_values = true, value_name = "GLOB")]
|
||||
glob_pattern: Option<String>,
|
||||
|
||||
/// List of words that will be considered as completions.
|
||||
#[arg(short = 'W', allow_hyphen_values = true)]
|
||||
word_list: Option<String>,
|
||||
|
||||
/// Name of a shell function to invoke to generate completions.
|
||||
#[arg(short = 'F', allow_hyphen_values = true, value_name = "FUNC_NAME")]
|
||||
function_name: Option<String>,
|
||||
|
||||
/// Command to execute to generate completions.
|
||||
#[arg(short = 'C', allow_hyphen_values = true)]
|
||||
command: Option<String>,
|
||||
|
||||
/// Pattern used as filter for completions.
|
||||
#[arg(short = 'X', allow_hyphen_values = true, value_name = "PATTERN")]
|
||||
filter_pattern: Option<String>,
|
||||
|
||||
/// Prefix pattern used as filter for completions.
|
||||
#[arg(short = 'P', allow_hyphen_values = true)]
|
||||
prefix: Option<String>,
|
||||
|
||||
/// Suffix pattern used as filter for completions.
|
||||
#[arg(short = 'S', allow_hyphen_values = true)]
|
||||
suffix: Option<String>,
|
||||
|
||||
/// Complete with valid aliases.
|
||||
#[arg(short = 'a')]
|
||||
action_alias: bool,
|
||||
|
||||
/// Complete with names of shell builtins.
|
||||
#[arg(short = 'b')]
|
||||
action_builtin: bool,
|
||||
|
||||
/// Complete with names of executable commands.
|
||||
#[arg(short = 'c')]
|
||||
action_command: bool,
|
||||
|
||||
/// Complete with directory names.
|
||||
#[arg(short = 'd')]
|
||||
action_directory: bool,
|
||||
|
||||
/// Complete with names of exported shell variables.
|
||||
#[arg(short = 'e')]
|
||||
action_exported: bool,
|
||||
|
||||
/// Complete with filenames.
|
||||
#[arg(short = 'f')]
|
||||
action_file: bool,
|
||||
|
||||
/// Complete with valid user groups.
|
||||
#[arg(short = 'g')]
|
||||
action_group: bool,
|
||||
|
||||
/// Complete with job specs.
|
||||
#[arg(short = 'j')]
|
||||
action_job: bool,
|
||||
|
||||
/// Complete with keywords.
|
||||
#[arg(short = 'k')]
|
||||
action_keyword: bool,
|
||||
|
||||
/// Complete with names of system services.
|
||||
#[arg(short = 's')]
|
||||
action_service: bool,
|
||||
|
||||
/// Complete with valid usernames.
|
||||
#[arg(short = 'u')]
|
||||
action_user: bool,
|
||||
|
||||
/// Complete with names of shell variables.
|
||||
#[arg(short = 'v')]
|
||||
action_variable: bool,
|
||||
}
|
||||
|
||||
impl CommonCompleteCommandArgs {
|
||||
fn create_spec(&self, extglob_enabled: bool) -> completion::Spec {
|
||||
let filter_pattern_excludes;
|
||||
let filter_pattern = if let Some(filter_pattern) = self.filter_pattern.as_ref() {
|
||||
// If the pattern starts with a '!' that's not the start of an extglob pattern,
|
||||
// then we invert.
|
||||
if let Some(remaining_pattern) = filter_pattern.strip_prefix('!') {
|
||||
if !extglob_enabled || !remaining_pattern.starts_with('(') {
|
||||
filter_pattern_excludes = false;
|
||||
Some(remaining_pattern.to_owned())
|
||||
} else {
|
||||
filter_pattern_excludes = true;
|
||||
Some(filter_pattern.to_owned())
|
||||
}
|
||||
} else {
|
||||
filter_pattern_excludes = true;
|
||||
Some(filter_pattern.clone())
|
||||
}
|
||||
} else {
|
||||
filter_pattern_excludes = false;
|
||||
None
|
||||
};
|
||||
|
||||
let mut spec = completion::Spec {
|
||||
options: completion::GenerationOptions::default(),
|
||||
actions: self.resolve_actions(),
|
||||
glob_pattern: self.glob_pattern.clone(),
|
||||
word_list: self.word_list.clone(),
|
||||
function_name: self.function_name.clone(),
|
||||
command: self.command.clone(),
|
||||
filter_pattern,
|
||||
filter_pattern_excludes,
|
||||
prefix: self.prefix.clone(),
|
||||
suffix: self.suffix.clone(),
|
||||
};
|
||||
|
||||
for option in &self.options {
|
||||
match option {
|
||||
CompleteOption::BashDefault => spec.options.bash_default = true,
|
||||
CompleteOption::Default => spec.options.default = true,
|
||||
CompleteOption::DirNames => spec.options.dir_names = true,
|
||||
CompleteOption::FileNames => spec.options.file_names = true,
|
||||
CompleteOption::NoQuote => spec.options.no_quote = true,
|
||||
CompleteOption::NoSort => spec.options.no_sort = true,
|
||||
CompleteOption::NoSpace => spec.options.no_space = true,
|
||||
CompleteOption::PlusDirs => spec.options.plus_dirs = true,
|
||||
}
|
||||
}
|
||||
|
||||
spec
|
||||
}
|
||||
|
||||
fn resolve_actions(&self) -> Vec<CompleteAction> {
|
||||
let mut actions = self.actions.clone();
|
||||
|
||||
if self.action_alias {
|
||||
actions.push(CompleteAction::Alias);
|
||||
}
|
||||
if self.action_builtin {
|
||||
actions.push(CompleteAction::Builtin);
|
||||
}
|
||||
if self.action_command {
|
||||
actions.push(CompleteAction::Command);
|
||||
}
|
||||
if self.action_directory {
|
||||
actions.push(CompleteAction::Directory);
|
||||
}
|
||||
if self.action_exported {
|
||||
actions.push(CompleteAction::Export);
|
||||
}
|
||||
if self.action_file {
|
||||
actions.push(CompleteAction::File);
|
||||
}
|
||||
if self.action_group {
|
||||
actions.push(CompleteAction::Group);
|
||||
}
|
||||
if self.action_job {
|
||||
actions.push(CompleteAction::Job);
|
||||
}
|
||||
if self.action_keyword {
|
||||
actions.push(CompleteAction::Keyword);
|
||||
}
|
||||
if self.action_service {
|
||||
actions.push(CompleteAction::Service);
|
||||
}
|
||||
if self.action_user {
|
||||
actions.push(CompleteAction::User);
|
||||
}
|
||||
if self.action_variable {
|
||||
actions.push(CompleteAction::Variable);
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure programmable command completion.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct CompleteCommand {
|
||||
/// Display registered completion settings.
|
||||
#[arg(short = 'p')]
|
||||
print: bool,
|
||||
|
||||
/// Remove the completion settings associated with the given command.
|
||||
#[arg(short = 'r')]
|
||||
remove: bool,
|
||||
|
||||
/// Apply these settings to the default completion scenario.
|
||||
#[arg(short = 'D')]
|
||||
use_as_default: bool,
|
||||
|
||||
/// Apply these settings to completion of empty lines.
|
||||
#[arg(short = 'E')]
|
||||
use_for_empty_line: bool,
|
||||
|
||||
/// Apply these settings to completion of the initial word of the input line.
|
||||
#[arg(short = 'I')]
|
||||
use_for_initial_word: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
common_args: CommonCompleteCommandArgs,
|
||||
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompleteCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
// If -D, -E, or -I are specified, then any names provided are ignored.
|
||||
if self.use_as_default
|
||||
|| self.use_for_empty_line
|
||||
|| self.use_for_initial_word
|
||||
|| self.names.is_empty()
|
||||
{
|
||||
self.process_global(&mut context)?;
|
||||
} else {
|
||||
for name in &self.names {
|
||||
if !self.try_process_for_command(&mut context, name.as_str())? {
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl CompleteCommand {
|
||||
fn process_global(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
// Read options before taking mutable borrow on completion_config
|
||||
let extended_globbing = context.shell.options().extended_globbing;
|
||||
|
||||
// These are processed in an intentional order.
|
||||
let special_option_name;
|
||||
let target_spec = if self.use_as_default {
|
||||
special_option_name = "-D";
|
||||
Some(&mut context.shell.completion_config_mut().default)
|
||||
} else if self.use_for_empty_line {
|
||||
special_option_name = "-E";
|
||||
Some(&mut context.shell.completion_config_mut().empty_line)
|
||||
} else if self.use_for_initial_word {
|
||||
special_option_name = "-I";
|
||||
Some(&mut context.shell.completion_config_mut().initial_word)
|
||||
} else {
|
||||
special_option_name = "";
|
||||
None
|
||||
};
|
||||
|
||||
// Treat 'complete' with no options the same as 'complete -p'.
|
||||
if self.print || (!self.remove && target_spec.is_none()) {
|
||||
if let Some(target_spec) = target_spec {
|
||||
if let Some(existing_spec) = target_spec {
|
||||
let existing_spec = existing_spec.clone();
|
||||
Self::display_spec(context, Some(special_option_name), None, &existing_spec)?;
|
||||
} else {
|
||||
return error::unimp("special spec not found");
|
||||
}
|
||||
} else {
|
||||
for (command_name, spec) in context.shell.completion_config().iter() {
|
||||
Self::display_spec(context, None, Some(command_name.as_str()), spec)?;
|
||||
}
|
||||
}
|
||||
} else if self.remove {
|
||||
if let Some(target_spec) = target_spec {
|
||||
let mut new_spec = None;
|
||||
std::mem::swap(&mut new_spec, target_spec);
|
||||
} else {
|
||||
context.shell.completion_config_mut().clear();
|
||||
}
|
||||
} else {
|
||||
if let Some(target_spec) = target_spec {
|
||||
let mut new_spec = Some(self.common_args.create_spec(extended_globbing));
|
||||
std::mem::swap(&mut new_spec, target_spec);
|
||||
} else {
|
||||
return error::unimp("set unspecified spec");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_display_spec_for_command(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
name: &str,
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
if let Some(spec) = context.shell.completion_config().get(name) {
|
||||
Self::display_spec(context, None, Some(name), spec)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
writeln!(context.stderr(), "no completion found for command")?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
fn display_spec(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
special_name: Option<&str>,
|
||||
command_name: Option<&str>,
|
||||
spec: &Spec,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let mut s = String::from("complete");
|
||||
|
||||
if let Some(special_name) = special_name {
|
||||
s.push(' ');
|
||||
s.push_str(special_name);
|
||||
}
|
||||
|
||||
for action in &spec.actions {
|
||||
s.push(' ');
|
||||
|
||||
let action_str = match action {
|
||||
CompleteAction::Alias => "-a",
|
||||
CompleteAction::ArrayVar => "-A arrayvar",
|
||||
CompleteAction::Binding => "-A binding",
|
||||
CompleteAction::Builtin => "-b",
|
||||
CompleteAction::Command => "-c",
|
||||
CompleteAction::Directory => "-d",
|
||||
CompleteAction::Disabled => "-A disabled",
|
||||
CompleteAction::Enabled => "-A enabled",
|
||||
CompleteAction::Export => "-e",
|
||||
CompleteAction::File => "-f",
|
||||
CompleteAction::Function => "-A function",
|
||||
CompleteAction::Group => "-g",
|
||||
CompleteAction::HelpTopic => "-A helptopic",
|
||||
CompleteAction::HostName => "-A hostname",
|
||||
CompleteAction::Job => "-j",
|
||||
CompleteAction::Keyword => "-k",
|
||||
CompleteAction::Running => "-A running",
|
||||
CompleteAction::Service => "-s",
|
||||
CompleteAction::SetOpt => "-A setopt",
|
||||
CompleteAction::ShOpt => "-A shopt",
|
||||
CompleteAction::Signal => "-A signal",
|
||||
CompleteAction::Stopped => "-A stopped",
|
||||
CompleteAction::User => "-u",
|
||||
CompleteAction::Variable => "-v",
|
||||
};
|
||||
|
||||
s.push_str(action_str);
|
||||
}
|
||||
|
||||
if spec.options.bash_default {
|
||||
s.push_str(" -o bashdefault");
|
||||
}
|
||||
if spec.options.default {
|
||||
s.push_str(" -o default");
|
||||
}
|
||||
if spec.options.dir_names {
|
||||
s.push_str(" -o dirnames");
|
||||
}
|
||||
if spec.options.file_names {
|
||||
s.push_str(" -o filenames");
|
||||
}
|
||||
if spec.options.no_quote {
|
||||
s.push_str(" -o noquote");
|
||||
}
|
||||
if spec.options.no_sort {
|
||||
s.push_str(" -o nosort");
|
||||
}
|
||||
if spec.options.no_space {
|
||||
s.push_str(" -o nospace");
|
||||
}
|
||||
if spec.options.plus_dirs {
|
||||
s.push_str(" -o plusdirs");
|
||||
}
|
||||
|
||||
if let Some(glob_pattern) = &spec.glob_pattern {
|
||||
write!(
|
||||
s,
|
||||
" -G {}",
|
||||
escape::force_quote(glob_pattern, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
if let Some(word_list) = &spec.word_list {
|
||||
write!(
|
||||
s,
|
||||
" -W {}",
|
||||
escape::force_quote(word_list, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
if let Some(function_name) = &spec.function_name {
|
||||
write!(s, " -F {function_name}")?;
|
||||
}
|
||||
if let Some(command) = &spec.command {
|
||||
write!(
|
||||
s,
|
||||
" -C {}",
|
||||
escape::force_quote(command, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
if let Some(filter_pattern) = &spec.filter_pattern {
|
||||
write!(
|
||||
s,
|
||||
" -X {}",
|
||||
escape::force_quote(filter_pattern, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
if let Some(prefix) = &spec.prefix {
|
||||
write!(
|
||||
s,
|
||||
" -P {}",
|
||||
escape::force_quote(prefix, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
if let Some(suffix) = &spec.suffix {
|
||||
write!(
|
||||
s,
|
||||
" -S {}",
|
||||
escape::force_quote(suffix, escape::QuoteMode::SingleQuote)
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(command_name) = command_name {
|
||||
s.push(' ');
|
||||
s.push_str(command_name);
|
||||
}
|
||||
|
||||
writeln!(context.stdout(), "{s}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_process_for_command(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
name: &str,
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
if self.print {
|
||||
return Self::try_display_spec_for_command(context, name);
|
||||
} else if self.remove {
|
||||
let mut result = context.shell.completion_config_mut().remove(name);
|
||||
|
||||
if !result {
|
||||
if context.shell.options().interactive {
|
||||
writeln!(context.stderr(), "complete: {name}: not found")?;
|
||||
} else {
|
||||
// For some reason, this is not supposed to be treated as a failure
|
||||
// in non-interactive execution.
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
let config = self
|
||||
.common_args
|
||||
.create_spec(context.shell.options().extended_globbing);
|
||||
|
||||
context.shell.completion_config_mut().set(name, config);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate command completions.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct CompGenCommand {
|
||||
#[clap(flatten)]
|
||||
common_args: CommonCompleteCommandArgs,
|
||||
|
||||
// N.B. The word can only start with a hyphen if it's after a --.
|
||||
word: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompGenCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut spec = self
|
||||
.common_args
|
||||
.create_spec(context.shell.options().extended_globbing);
|
||||
spec.options.no_sort = true;
|
||||
|
||||
let token_to_complete = self.word.as_deref().unwrap_or_default();
|
||||
|
||||
// We unquote the token-to-be-completed before passing it to the completion system.
|
||||
let unquoted_token = brush_parser::unquote_str(token_to_complete);
|
||||
|
||||
let completion_context = completion::Context {
|
||||
token_to_complete: unquoted_token.as_str(),
|
||||
preceding_token: None,
|
||||
command_name: None,
|
||||
token_index: 0,
|
||||
tokens: &[&completion::CompletionToken {
|
||||
text: token_to_complete,
|
||||
start: 0,
|
||||
}],
|
||||
input_line: token_to_complete,
|
||||
cursor_index: token_to_complete.len(),
|
||||
trigger: completion::CompletionTrigger::Programmatic,
|
||||
};
|
||||
|
||||
let result = spec
|
||||
.get_completions(context.shell, &completion_context)
|
||||
.await?;
|
||||
|
||||
match result {
|
||||
completion::Answer::Candidates(candidates, _options) => {
|
||||
// We are expected to return 1 if there are no candidates, even if no errors
|
||||
// occurred along the way.
|
||||
if candidates.is_empty() {
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
|
||||
for candidate in candidates {
|
||||
writeln!(context.stdout(), "{candidate}")?;
|
||||
}
|
||||
}
|
||||
completion::Answer::RestartCompletionProcess => {
|
||||
return error::unimp("restart completion");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
/// Set programmable command completion options.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct CompOptCommand {
|
||||
/// Update the default completion settings.
|
||||
#[arg(short = 'D')]
|
||||
update_default: bool,
|
||||
|
||||
/// Update the completion settings for empty lines.
|
||||
#[arg(short = 'E')]
|
||||
update_empty: bool,
|
||||
|
||||
/// Update the completion settings for the initial word of the input line.
|
||||
#[arg(short = 'I')]
|
||||
update_initial_word: bool,
|
||||
|
||||
/// Enable the specified option for selected completion scenarios.
|
||||
#[arg(short = 'o', value_name = "OPT")]
|
||||
enabled_options: Vec<CompleteOption>,
|
||||
#[arg(long = concat!("+o"), hide = true)]
|
||||
disabled_options: Vec<CompleteOption>,
|
||||
|
||||
/// If specified, scopes updates to completions of the named commands.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for CompOptCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut options = HashMap::new();
|
||||
for option in &self.disabled_options {
|
||||
options.insert(option.clone(), false);
|
||||
}
|
||||
for option in &self.enabled_options {
|
||||
options.insert(option.clone(), true);
|
||||
}
|
||||
|
||||
if !self.names.is_empty() {
|
||||
if self.update_default || self.update_empty || self.update_initial_word {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"compopt: cannot specify names with -D, -E, or -I"
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
for name in &self.names {
|
||||
let spec = context.shell.completion_config_mut().get_or_add_mut(name);
|
||||
Self::set_options_for_spec(spec, &options);
|
||||
}
|
||||
} else if self.update_default {
|
||||
if let Some(spec) = &mut context.shell.completion_config_mut().default {
|
||||
Self::set_options_for_spec(spec, &options);
|
||||
} else {
|
||||
let mut spec = Spec::default();
|
||||
Self::set_options_for_spec(&mut spec, &options);
|
||||
context.shell.completion_config_mut().default = Some(spec);
|
||||
}
|
||||
} else if self.update_empty {
|
||||
if let Some(spec) = &mut context.shell.completion_config_mut().empty_line {
|
||||
Self::set_options_for_spec(spec, &options);
|
||||
} else {
|
||||
let mut spec = Spec::default();
|
||||
Self::set_options_for_spec(&mut spec, &options);
|
||||
context.shell.completion_config_mut().empty_line = Some(spec);
|
||||
}
|
||||
} else if self.update_initial_word {
|
||||
if let Some(spec) = &mut context.shell.completion_config_mut().initial_word {
|
||||
Self::set_options_for_spec(spec, &options);
|
||||
} else {
|
||||
let mut spec = Spec::default();
|
||||
Self::set_options_for_spec(&mut spec, &options);
|
||||
context.shell.completion_config_mut().initial_word = Some(spec);
|
||||
}
|
||||
} else {
|
||||
// If we got here, then we need to apply to any completion actively in-flight.
|
||||
if let Some(in_flight_options) = context
|
||||
.shell
|
||||
.completion_config_mut()
|
||||
.current_completion_options
|
||||
.as_mut()
|
||||
{
|
||||
Self::set_options(in_flight_options, &options);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
impl CompOptCommand {
|
||||
fn set_options_for_spec(spec: &mut Spec, options: &HashMap<CompleteOption, bool>) {
|
||||
Self::set_options(&mut spec.options, options);
|
||||
}
|
||||
|
||||
fn set_options(
|
||||
target_options: &mut completion::GenerationOptions,
|
||||
options: &HashMap<CompleteOption, bool>,
|
||||
) {
|
||||
for (option, value) in options {
|
||||
match option {
|
||||
CompleteOption::BashDefault => target_options.bash_default = *value,
|
||||
CompleteOption::Default => target_options.default = *value,
|
||||
CompleteOption::DirNames => target_options.dir_names = *value,
|
||||
CompleteOption::FileNames => target_options.file_names = *value,
|
||||
CompleteOption::NoQuote => target_options.no_quote = *value,
|
||||
CompleteOption::NoSort => target_options.no_sort = *value,
|
||||
CompleteOption::NoSpace => target_options.no_space = *value,
|
||||
CompleteOption::PlusDirs => target_options.plus_dirs = *value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Continue to the next iteration of a control-flow loop.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ContinueCommand {
|
||||
/// If specified, indicates which nested loop to continue to the next iteration of.
|
||||
#[clap(default_value_t = 1)]
|
||||
which_loop: i8,
|
||||
}
|
||||
|
||||
impl builtins::Command for ContinueCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
_context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// If specified, which_loop needs to be positive.
|
||||
if self.which_loop <= 0 {
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
result.next_control_flow = ExecutionControlFlow::ContinueLoop {
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
levels: (self.which_loop - 1) as usize,
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::{io::Write, sync::LazyLock};
|
||||
|
||||
use brush_core::{
|
||||
ErrorKind, ExecutionResult, builtins,
|
||||
env::{self, EnvironmentLookup, EnvironmentScope},
|
||||
error,
|
||||
parser::ast,
|
||||
variables::{
|
||||
self, ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable,
|
||||
ShellVariableUpdateTransform,
|
||||
},
|
||||
};
|
||||
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
MakeIndexedArrayFlag,
|
||||
'a',
|
||||
"Make the variable an indexed array."
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
MakeAssociativeArrayFlag,
|
||||
'A',
|
||||
"Make the variable an associative array."
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
CapitalizeValueOnAssignmentFlag,
|
||||
'c',
|
||||
"Enable capitalize-on-assignment for the variable."
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(MakeIntegerFlag, 'i', "Mark the variable as integer-typed");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
LowercaseValueOnAssignmentFlag,
|
||||
'l',
|
||||
"Enable lowercase-on-assignment for the variable."
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
MakeNameRefFlag,
|
||||
'n',
|
||||
"Mark the variable as a name reference"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(MakeReadonlyFlag, 'r', "Mark the variable as read-only.");
|
||||
crate::minus_or_plus_flag_arg!(MakeTracedFlag, 't', "Enable tracing for the variable.");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
UppercaseValueOnAssignmentFlag,
|
||||
'u',
|
||||
"Enable uppercase-on-assignment for the variable."
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(MakeExportedFlag, 'x', "Mark the variable for export.");
|
||||
|
||||
/// Display or update variables and their attributes.
|
||||
#[derive(Parser)]
|
||||
#[clap(override_usage = "declare [OPTIONS] [DECLARATIONS]...")]
|
||||
pub(crate) struct DeclareCommand {
|
||||
/// Constrain to function names or definitions.
|
||||
#[arg(short = 'f')]
|
||||
function_names_or_defs_only: bool,
|
||||
|
||||
/// Constrain to function names only.
|
||||
#[arg(short = 'F')]
|
||||
function_names_only: bool,
|
||||
|
||||
/// Create global variable, if applicable.
|
||||
#[arg(short = 'g')]
|
||||
create_global: bool,
|
||||
|
||||
/// When creating a local variable that shadows another variable of the same name,
|
||||
/// then initialize it with the contents and attributes of the variable being shadowed.
|
||||
#[arg(short = 'I')]
|
||||
locals_inherit_from_prev_scope: bool,
|
||||
|
||||
/// Display each item's attributes and values.
|
||||
#[arg(short = 'p')]
|
||||
print: bool,
|
||||
|
||||
//
|
||||
// Attribute options
|
||||
#[clap(flatten)] // -a
|
||||
make_indexed_array: MakeIndexedArrayFlag,
|
||||
#[clap(flatten)] // -A
|
||||
make_associative_array: MakeAssociativeArrayFlag,
|
||||
#[clap(flatten)] // -c
|
||||
capitalize_value_on_assignment: CapitalizeValueOnAssignmentFlag,
|
||||
#[clap(flatten)] // -i
|
||||
make_integer: MakeIntegerFlag,
|
||||
#[clap(flatten)] // -l
|
||||
lowercase_value_on_assignment: LowercaseValueOnAssignmentFlag,
|
||||
#[clap(flatten)] // -n
|
||||
make_nameref: MakeNameRefFlag,
|
||||
#[clap(flatten)] // -r
|
||||
make_readonly: MakeReadonlyFlag,
|
||||
#[clap(flatten)] // -t
|
||||
make_traced: MakeTracedFlag,
|
||||
#[clap(flatten)] // -u
|
||||
uppercase_value_on_assignment: UppercaseValueOnAssignmentFlag,
|
||||
#[clap(flatten)] // -x
|
||||
make_exported: MakeExportedFlag,
|
||||
|
||||
//
|
||||
// Declarations
|
||||
//
|
||||
// N.B. These are skipped by clap, but filled in by the BuiltinDeclarationCommand trait.
|
||||
#[clap(skip)]
|
||||
declarations: Vec<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum DeclareVerb {
|
||||
Declare,
|
||||
Local,
|
||||
Readonly,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for DeclareCommand {
|
||||
fn set_declarations(&mut self, declarations: Vec<brush_core::CommandArg>) {
|
||||
self.declarations = declarations;
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for DeclareCommand {
|
||||
fn takes_plus_options() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let verb = match context.command_name.as_str() {
|
||||
"local" => DeclareVerb::Local,
|
||||
"readonly" => DeclareVerb::Readonly,
|
||||
_ => DeclareVerb::Declare,
|
||||
};
|
||||
|
||||
if matches!(verb, DeclareVerb::Local) && !context.shell.in_function() {
|
||||
writeln!(context.stderr(), "can only be used in a function")?;
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
|
||||
if self.locals_inherit_from_prev_scope {
|
||||
return error::unimp("declare -I");
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::success();
|
||||
if !self.declarations.is_empty() {
|
||||
for declaration in &self.declarations {
|
||||
if self.print && !matches!(verb, DeclareVerb::Readonly) {
|
||||
if !self.try_display_declaration(&context, declaration, verb)? {
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
} else {
|
||||
if !self.process_declaration(&mut context, declaration, verb)? {
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Display matching declarations from the variable environment.
|
||||
if !self.function_names_only && !self.function_names_or_defs_only {
|
||||
self.display_matching_env_declarations(&context, verb)?;
|
||||
}
|
||||
|
||||
// Do the same for functions.
|
||||
if !matches!(verb, DeclareVerb::Local | DeclareVerb::Readonly)
|
||||
&& (!self.print || self.function_names_only || self.function_names_or_defs_only)
|
||||
{
|
||||
self.display_matching_functions(&context)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeclareCommand {
|
||||
fn try_display_declaration(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
declaration: &brush_core::CommandArg,
|
||||
verb: DeclareVerb,
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
let name = match declaration {
|
||||
brush_core::CommandArg::String(s) => s,
|
||||
brush_core::CommandArg::Assignment(_) => {
|
||||
writeln!(context.stderr(), "declare: {declaration}: not found")?;
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let lookup = if matches!(verb, DeclareVerb::Local) {
|
||||
EnvironmentLookup::OnlyInCurrentLocal
|
||||
} else {
|
||||
EnvironmentLookup::Anywhere
|
||||
};
|
||||
|
||||
if self.function_names_only || self.function_names_or_defs_only {
|
||||
if let Some(func_registration) = context.shell.funcs().get(name) {
|
||||
if self.function_names_only {
|
||||
if self.print {
|
||||
writeln!(context.stdout(), "declare -f {name}")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{name}")?;
|
||||
}
|
||||
} else {
|
||||
writeln!(context.stdout(), "{}", func_registration.definition())?;
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
// For some reason, bash does not print an error message in this case.
|
||||
Ok(false)
|
||||
}
|
||||
} else if let Some(variable) = context.shell.env().get_using_policy(name, lookup) {
|
||||
let mut cs = variable.attribute_flags(context.shell);
|
||||
if cs.is_empty() {
|
||||
cs.push('-');
|
||||
}
|
||||
|
||||
let resolved_value = variable.resolve_value(context.shell);
|
||||
let separator_str = if matches!(resolved_value, ShellValue::Unset(_)) {
|
||||
""
|
||||
} else {
|
||||
"="
|
||||
};
|
||||
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"declare -{cs} {name}{separator_str}{}",
|
||||
resolved_value.format(variables::FormatStyle::DeclarePrint, context.shell)?
|
||||
)?;
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
writeln!(context.stderr(), "declare: {name}: not found")?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_declaration(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
declaration: &brush_core::CommandArg,
|
||||
verb: DeclareVerb,
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
let create_var_local = matches!(verb, DeclareVerb::Local)
|
||||
|| (matches!(verb, DeclareVerb::Declare)
|
||||
&& context.shell.in_function()
|
||||
&& !self.create_global);
|
||||
|
||||
if self.function_names_or_defs_only || self.function_names_only {
|
||||
return self.try_display_declaration(context, declaration, verb);
|
||||
}
|
||||
|
||||
// Extract the variable name and the initial value being assigned (if any).
|
||||
let (name, assigned_index, initial_value, name_is_array) =
|
||||
Self::declaration_to_name_and_value(declaration)?;
|
||||
|
||||
// Special-case: `local -`
|
||||
if name == "-" && matches!(verb, DeclareVerb::Local) {
|
||||
// TODO(local): `local -` allows shadowing the current `set` options (i.e., $-), with
|
||||
// subsequent updates getting discarded when the current local scope is popped.
|
||||
tracing::warn!("not yet implemented: local -");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Make sure it's a valid name.
|
||||
if !env::valid_variable_name(name.as_str()) {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {name}: not a valid variable name",
|
||||
context.command_name
|
||||
)?;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Figure out where we should look.
|
||||
let lookup = if create_var_local {
|
||||
EnvironmentLookup::OnlyInCurrentLocal
|
||||
} else {
|
||||
EnvironmentLookup::Anywhere
|
||||
};
|
||||
|
||||
// Look up the variable.
|
||||
if let Some(var) = context
|
||||
.shell
|
||||
.env_mut()
|
||||
.get_mut_using_policy(name.as_str(), lookup)
|
||||
{
|
||||
if self.make_associative_array.is_some() {
|
||||
var.convert_to_associative_array()?;
|
||||
}
|
||||
if self.make_indexed_array.is_some() {
|
||||
var.convert_to_indexed_array()?;
|
||||
}
|
||||
|
||||
self.apply_attributes_before_update(var)?;
|
||||
|
||||
if let Some(initial_value) = initial_value {
|
||||
// We append if the declaration included an explicit index.
|
||||
var.assign(initial_value, assigned_index.is_some())?;
|
||||
}
|
||||
|
||||
self.apply_attributes_after_update(var, verb)?;
|
||||
} else {
|
||||
let unset_type = if self.make_indexed_array.is_some() {
|
||||
ShellValueUnsetType::IndexedArray
|
||||
} else if self.make_associative_array.is_some() {
|
||||
ShellValueUnsetType::AssociativeArray
|
||||
} else if name_is_array {
|
||||
ShellValueUnsetType::IndexedArray
|
||||
} else {
|
||||
ShellValueUnsetType::Untyped
|
||||
};
|
||||
|
||||
let mut var = ShellVariable::new(ShellValue::Unset(unset_type));
|
||||
|
||||
self.apply_attributes_before_update(&mut var)?;
|
||||
|
||||
if let Some(initial_value) = initial_value {
|
||||
var.assign(initial_value, false)?;
|
||||
}
|
||||
|
||||
if context.shell.options().export_variables_on_modification && !var.value().is_array() {
|
||||
var.export();
|
||||
}
|
||||
|
||||
self.apply_attributes_after_update(&mut var, verb)?;
|
||||
|
||||
let scope = if create_var_local {
|
||||
EnvironmentScope::Local
|
||||
} else {
|
||||
EnvironmentScope::Global
|
||||
};
|
||||
|
||||
context.shell.env_mut().add(name, var, scope)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn declaration_to_name_and_value(
|
||||
declaration: &brush_core::CommandArg,
|
||||
) -> Result<(String, Option<String>, Option<ShellValueLiteral>, bool), brush_core::Error> {
|
||||
let name;
|
||||
let assigned_index;
|
||||
let initial_value;
|
||||
let name_is_array;
|
||||
|
||||
match declaration {
|
||||
brush_core::CommandArg::String(s) => {
|
||||
// We need to handle the case of someone invoking `declare array[index]`.
|
||||
// In such case, we ignore the index and treat it as a declaration of
|
||||
// the array.
|
||||
#[allow(
|
||||
clippy::unwrap_in_result,
|
||||
clippy::unwrap_used,
|
||||
reason = "regex is valid and should not fail"
|
||||
)]
|
||||
static ARRAY_AND_INDEX_RE: LazyLock<fancy_regex::Regex> =
|
||||
LazyLock::new(|| fancy_regex::Regex::new(r"^(.*?)\[(.*?)\]$").unwrap());
|
||||
|
||||
if let Some(captures) = ARRAY_AND_INDEX_RE.captures(s)? {
|
||||
name = captures
|
||||
.get(1)
|
||||
.ok_or_else(|| {
|
||||
brush_core::ErrorKind::InternalError("declaration parse error".into())
|
||||
})?
|
||||
.as_str()
|
||||
.to_owned();
|
||||
|
||||
assigned_index = captures.get(2).map(|m| m.as_str().to_owned());
|
||||
name_is_array = true;
|
||||
} else {
|
||||
name = s.clone();
|
||||
assigned_index = None;
|
||||
name_is_array = false;
|
||||
}
|
||||
initial_value = None;
|
||||
}
|
||||
brush_core::CommandArg::Assignment(assignment) => {
|
||||
match &assignment.name {
|
||||
ast::AssignmentName::VariableName(var_name) => {
|
||||
name = var_name.to_owned();
|
||||
assigned_index = None;
|
||||
}
|
||||
ast::AssignmentName::ArrayElementName(var_name, index) => {
|
||||
if matches!(assignment.value, ast::AssignmentValue::Array(_)) {
|
||||
return Err(ErrorKind::AssigningListToArrayMember.into());
|
||||
}
|
||||
|
||||
name = var_name.to_owned();
|
||||
assigned_index = Some(index.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
match &assignment.value {
|
||||
ast::AssignmentValue::Scalar(s) => {
|
||||
if let Some(index) = &assigned_index {
|
||||
initial_value = Some(ShellValueLiteral::Array(ArrayLiteral(vec![(
|
||||
Some(index.to_owned()),
|
||||
s.value.clone(),
|
||||
)])));
|
||||
name_is_array = true;
|
||||
} else {
|
||||
initial_value = Some(ShellValueLiteral::Scalar(s.value.clone()));
|
||||
name_is_array = false;
|
||||
}
|
||||
}
|
||||
ast::AssignmentValue::Array(a) => {
|
||||
initial_value = Some(ShellValueLiteral::Array(ArrayLiteral(
|
||||
a.iter()
|
||||
.map(|(i, v)| {
|
||||
(i.as_ref().map(|w| w.value.clone()), v.value.clone())
|
||||
})
|
||||
.collect(),
|
||||
)));
|
||||
name_is_array = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((name, assigned_index, initial_value, name_is_array))
|
||||
}
|
||||
|
||||
fn display_matching_env_declarations(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
verb: DeclareVerb,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
//
|
||||
// Dump all declarations. Use attribute flags to filter which variables are dumped.
|
||||
//
|
||||
|
||||
// We start by excluding all variables that are not enumerable.
|
||||
#[expect(clippy::type_complexity)]
|
||||
let mut filters: Vec<Box<dyn Fn((&String, &ShellVariable)) -> bool>> =
|
||||
vec![Box::new(|(_, v)| v.is_enumerable())];
|
||||
|
||||
// Add filters depending on verb.
|
||||
if matches!(verb, DeclareVerb::Readonly) {
|
||||
filters.push(Box::new(|(_, v)| v.is_readonly()));
|
||||
}
|
||||
|
||||
// Add filters depending on attribute flags.
|
||||
if let Some(value) = self.make_indexed_array.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| {
|
||||
matches!(v.value(), ShellValue::IndexedArray(_)) == value
|
||||
}));
|
||||
}
|
||||
if let Some(value) = self.make_associative_array.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| {
|
||||
matches!(v.value(), ShellValue::AssociativeArray(_)) == value
|
||||
}));
|
||||
}
|
||||
if let Some(value) = self.make_integer.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| v.is_treated_as_integer() == value));
|
||||
}
|
||||
if let Some(value) = self.capitalize_value_on_assignment.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| {
|
||||
matches!(
|
||||
v.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Capitalize
|
||||
) == value
|
||||
}));
|
||||
}
|
||||
if let Some(value) = self.lowercase_value_on_assignment.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| {
|
||||
matches!(
|
||||
v.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Lowercase
|
||||
) == value
|
||||
}));
|
||||
}
|
||||
if let Some(value) = self.make_nameref.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| v.is_treated_as_nameref() == value));
|
||||
}
|
||||
if let Some(value) = self.make_readonly.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| v.is_readonly() == value));
|
||||
}
|
||||
if let Some(value) = self.make_readonly.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| v.is_trace_enabled() == value));
|
||||
}
|
||||
if let Some(value) = self.uppercase_value_on_assignment.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| {
|
||||
matches!(
|
||||
v.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Uppercase
|
||||
) == value
|
||||
}));
|
||||
}
|
||||
if let Some(value) = self.make_exported.to_bool() {
|
||||
filters.push(Box::new(move |(_, v)| v.is_exported() == value));
|
||||
}
|
||||
|
||||
let iter_policy = if matches!(verb, DeclareVerb::Local) {
|
||||
EnvironmentLookup::OnlyInCurrentLocal
|
||||
} else {
|
||||
EnvironmentLookup::Anywhere
|
||||
};
|
||||
|
||||
// Iterate through an ordered list of all matching declarations tracked in the
|
||||
// environment.
|
||||
for (name, variable) in context
|
||||
.shell
|
||||
.env()
|
||||
.iter_using_policy(iter_policy)
|
||||
.filter(|pair| filters.iter().all(|f| f(*pair)))
|
||||
.sorted_by_key(|v| v.0)
|
||||
{
|
||||
if self.print {
|
||||
let mut cs = variable.attribute_flags(context.shell);
|
||||
if cs.is_empty() {
|
||||
cs.push('-');
|
||||
}
|
||||
|
||||
let separator_str = if matches!(variable.value(), ShellValue::Unset(_)) {
|
||||
""
|
||||
} else {
|
||||
"="
|
||||
};
|
||||
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"declare -{cs} {name}{separator_str}{}",
|
||||
variable
|
||||
.value()
|
||||
.format(variables::FormatStyle::DeclarePrint, context.shell)?
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{name}={}",
|
||||
variable
|
||||
.value()
|
||||
.format(variables::FormatStyle::Basic, context.shell)?
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_matching_functions(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
for (name, registration) in context.shell.funcs().iter().sorted_by_key(|v| v.0) {
|
||||
if self.function_names_only {
|
||||
writeln!(context.stdout(), "declare -f {name}")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{}", registration.definition())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
const fn apply_attributes_before_update(
|
||||
&self,
|
||||
var: &mut ShellVariable,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
if let Some(value) = self.make_integer.to_bool() {
|
||||
if value {
|
||||
var.treat_as_integer();
|
||||
} else {
|
||||
var.unset_treat_as_integer();
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.capitalize_value_on_assignment.to_bool() {
|
||||
if value {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::Capitalize);
|
||||
} else if matches!(
|
||||
var.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Capitalize
|
||||
) {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::None);
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.lowercase_value_on_assignment.to_bool() {
|
||||
if value {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::Lowercase);
|
||||
} else if matches!(
|
||||
var.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Lowercase
|
||||
) {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::None);
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.make_nameref.to_bool() {
|
||||
if value {
|
||||
var.treat_as_nameref();
|
||||
} else {
|
||||
var.unset_treat_as_nameref();
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.make_traced.to_bool() {
|
||||
if value {
|
||||
var.enable_trace();
|
||||
} else {
|
||||
var.disable_trace();
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.uppercase_value_on_assignment.to_bool() {
|
||||
if value {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::Uppercase);
|
||||
} else if matches!(
|
||||
var.get_update_transform(),
|
||||
ShellVariableUpdateTransform::Uppercase
|
||||
) {
|
||||
var.set_update_transform(ShellVariableUpdateTransform::None);
|
||||
}
|
||||
}
|
||||
if let Some(value) = self.make_exported.to_bool() {
|
||||
if value {
|
||||
var.export();
|
||||
} else {
|
||||
var.unexport();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_attributes_after_update(
|
||||
&self,
|
||||
var: &mut ShellVariable,
|
||||
verb: DeclareVerb,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
if matches!(verb, DeclareVerb::Readonly) {
|
||||
var.set_readonly();
|
||||
} else if let Some(value) = self.make_readonly.to_bool() {
|
||||
if value {
|
||||
var.set_readonly();
|
||||
} else {
|
||||
var.unset_readonly()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum DirError {
|
||||
/// Directory stack is empty.
|
||||
#[error("directory stack is empty")]
|
||||
DirStackEmpty,
|
||||
|
||||
/// A shell error occurred.
|
||||
#[error(transparent)]
|
||||
ShellError(#[from] brush_core::Error),
|
||||
}
|
||||
|
||||
impl From<&DirError> for brush_core::ExecutionExitCode {
|
||||
fn from(value: &DirError) -> Self {
|
||||
match value {
|
||||
DirError::DirStackEmpty => Self::GeneralError,
|
||||
DirError::ShellError(e) => e.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl brush_core::BuiltinError for DirError {}
|
||||
|
||||
/// Manage the current directory stack.
|
||||
#[derive(Default, Parser)]
|
||||
pub(crate) struct DirsCommand {
|
||||
/// Clear the directory stack.
|
||||
#[arg(short = 'c')]
|
||||
clear: bool,
|
||||
|
||||
/// Don't tilde-shorten paths.
|
||||
#[arg(short = 'l')]
|
||||
tilde_long: bool,
|
||||
|
||||
/// Print one directory per line instead of all on one line.
|
||||
#[arg(short = 'p')]
|
||||
print_one_per_line: bool,
|
||||
|
||||
/// Print one directory per line with its index.
|
||||
#[arg(short = 'v')]
|
||||
print_one_per_line_with_index: bool,
|
||||
//
|
||||
// TODO(dirs): implement +N and -N
|
||||
}
|
||||
|
||||
impl builtins::Command for DirsCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.clear {
|
||||
context.shell.directory_stack_mut().clear();
|
||||
} else {
|
||||
let dirs = vec![context.shell.working_dir()]
|
||||
.into_iter()
|
||||
.chain(
|
||||
context
|
||||
.shell
|
||||
.directory_stack()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|p| p.as_path()),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let one_per_line = self.print_one_per_line || self.print_one_per_line_with_index;
|
||||
|
||||
for (i, dir) in dirs.iter().enumerate() {
|
||||
if !one_per_line && i > 0 {
|
||||
write!(context.stdout(), " ")?;
|
||||
}
|
||||
|
||||
if self.print_one_per_line_with_index {
|
||||
write!(context.stdout(), "{i:2} ")?;
|
||||
}
|
||||
|
||||
let mut dir_str = dir.to_string_lossy().to_string();
|
||||
|
||||
if !self.tilde_long {
|
||||
dir_str = context.shell.tilde_shorten(dir_str);
|
||||
}
|
||||
|
||||
write!(context.stdout(), "{dir_str}")?;
|
||||
|
||||
if one_per_line || i == dirs.len() - 1 {
|
||||
writeln!(context.stdout())?;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brush_core::builtins;
|
||||
use clap::Parser;
|
||||
|
||||
/// Evaluate the provided script in the current shell environment.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct DotCommand {
|
||||
/// Path to the script to evaluate.
|
||||
script_path: String,
|
||||
|
||||
/// Any arguments to be passed as positional parameters to the script.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
script_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for DotCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// TODO(dot): Handle trap inheritance.
|
||||
context
|
||||
.shell
|
||||
.source_script(
|
||||
Path::new(&self.script_path),
|
||||
self.script_args.iter(),
|
||||
&context.params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, escape};
|
||||
|
||||
/// Echo text to standard output.
|
||||
#[derive(Parser)]
|
||||
#[clap(disable_help_flag = true, disable_version_flag = true)]
|
||||
pub(crate) struct EchoCommand {
|
||||
/// Suppress the trailing newline from the output.
|
||||
#[arg(short = 'n')]
|
||||
no_trailing_newline: bool,
|
||||
|
||||
/// Interpret backslash escapes in the provided text.
|
||||
#[arg(short = 'e')]
|
||||
interpret_backslash_escapes: bool,
|
||||
|
||||
/// Do not interpret backslash escapes in the provided text.
|
||||
#[arg(short = 'E')]
|
||||
no_interpret_backslash_escapes: bool,
|
||||
|
||||
/// Tokens to echo to standard output.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for EchoCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
/// Override the default [`builtins::Command::new`] function to handle clap's limitation related
|
||||
/// to `--`. See [`builtins::parse_known`] for more information
|
||||
/// TODO(echo): we can safely remove this after the issue is resolved
|
||||
fn new<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
if let Some(args) = rest_args {
|
||||
this.args.extend(args);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut trailing_newline = !self.no_trailing_newline;
|
||||
let mut s;
|
||||
if self.interpret_backslash_escapes {
|
||||
s = String::new();
|
||||
for (i, arg) in self.args.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push(' ');
|
||||
}
|
||||
|
||||
let (expanded_arg, keep_going) = escape::expand_backslash_escapes(
|
||||
arg.as_str(),
|
||||
escape::EscapeExpansionMode::EchoBuiltin,
|
||||
)?;
|
||||
s.push_str(&String::from_utf8_lossy(expanded_arg.as_slice()));
|
||||
|
||||
if !keep_going {
|
||||
trailing_newline = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s = self.args.join(" ");
|
||||
}
|
||||
|
||||
if trailing_newline {
|
||||
s.push('\n');
|
||||
}
|
||||
|
||||
write!(context.stdout(), "{s}")?;
|
||||
context.stdout().flush()?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use brush_core::ExecutionResult;
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::builtins;
|
||||
use brush_core::error;
|
||||
|
||||
/// Enable, disable, or display built-in commands.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct EnableCommand {
|
||||
/// Print a list of built-in commands.
|
||||
#[arg(short = 'a')]
|
||||
print_list: bool,
|
||||
|
||||
/// Disables the specified built-in commands.
|
||||
#[arg(short = 'n')]
|
||||
disable: bool,
|
||||
|
||||
/// Print a list of built-in commands with reusable output.
|
||||
#[arg(short = 'p')]
|
||||
print_reusably: bool,
|
||||
|
||||
/// Only operate on special built-in commands.
|
||||
#[arg(short = 's')]
|
||||
special_only: bool,
|
||||
|
||||
/// Path to a shared object from which built-in commands will be loaded.
|
||||
#[arg(short = 'f', value_name = "PATH")]
|
||||
shared_object_path: Option<String>,
|
||||
|
||||
/// Remove the built-in commands loaded from the indicated object path.
|
||||
#[arg(short = 'd')]
|
||||
remove_loaded_builtin: bool,
|
||||
|
||||
/// Names of built-in commands to operate on.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for EnableCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
if self.shared_object_path.is_some() {
|
||||
return error::unimp("enable -f");
|
||||
}
|
||||
if self.remove_loaded_builtin {
|
||||
return error::unimp("enable -d");
|
||||
}
|
||||
|
||||
if !self.names.is_empty() {
|
||||
for name in &self.names {
|
||||
if let Some(builtin) = context.shell.builtin_mut(name) {
|
||||
builtin.disabled = self.disable;
|
||||
} else {
|
||||
writeln!(context.stderr(), "{name}: not a shell builtin")?;
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let builtins: Vec<_> = context
|
||||
.shell
|
||||
.builtins()
|
||||
.iter()
|
||||
.sorted_by_key(|(name, _reg)| *name)
|
||||
.collect();
|
||||
|
||||
for (builtin_name, builtin) in builtins {
|
||||
if self.disable {
|
||||
if !builtin.disabled {
|
||||
continue;
|
||||
}
|
||||
} else if self.print_list {
|
||||
if builtin.disabled {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if self.special_only && !builtin.special_builtin {
|
||||
continue;
|
||||
}
|
||||
|
||||
let prefix = if builtin.disabled { "-n " } else { "" };
|
||||
|
||||
writeln!(context.stdout(), "enable {prefix}{builtin_name}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
use clap::Parser;
|
||||
|
||||
/// Evaluate the given string as script.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct EvalCommand {
|
||||
/// The script to evaluate.
|
||||
#[clap(allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for EvalCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if !self.args.is_empty() {
|
||||
let args_concatenated = self.args.join(" ");
|
||||
|
||||
tracing::debug!("Applying eval to: {:?}", args_concatenated);
|
||||
|
||||
// Our new source context is relative to the current position because we are only
|
||||
// providing the raw string being eval'd.
|
||||
// TODO(source-info): Provide the location of the specific tokens that make up
|
||||
// `self.args`.
|
||||
let source_info = context.shell.call_stack().current_pos_as_source_info();
|
||||
|
||||
// Return the direct result of running the string; we intentionally
|
||||
// pass through the result and honor its requested control flow. eval
|
||||
// executes in the current environment, so all control flow (return,
|
||||
// exit, break, continue) should propagate.
|
||||
context
|
||||
.shell
|
||||
.run_string(args_concatenated, &source_info, &context.params)
|
||||
.await
|
||||
} else {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use clap::Parser;
|
||||
use std::{borrow::Cow, os::unix::process::CommandExt};
|
||||
|
||||
use brush_core::{ErrorKind, ExecutionExitCode, ExecutionResult, builtins, commands};
|
||||
|
||||
/// Exec the provided command.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ExecCommand {
|
||||
/// Pass given name as zeroth argument to command.
|
||||
#[arg(short = 'a', value_name = "NAME")]
|
||||
name_for_argv0: Option<String>,
|
||||
|
||||
/// Exec command with an empty environment.
|
||||
#[arg(short = 'c')]
|
||||
empty_environment: bool,
|
||||
|
||||
/// Exec command as a login shell.
|
||||
#[arg(short = 'l')]
|
||||
exec_as_login: bool,
|
||||
|
||||
/// Command and args.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ExecCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if self.args.is_empty() {
|
||||
// When no arguments are present, then there's nothing for us to execute -- but we need
|
||||
// to ensure that any redirections setup for this builtin get applied to the calling
|
||||
// shell instance.
|
||||
#[allow(clippy::needless_collect)]
|
||||
let fds: Vec<_> = context.iter_fds().collect();
|
||||
|
||||
context.shell.replace_open_files(fds.into_iter());
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
// If we know we're already running in a subshell, then `exec`ing is actually
|
||||
// unsafe, since it would also replace the *parent* shell instance. We instead
|
||||
// delegate to the `command` builtin to perform the execution, with an expectation
|
||||
// of returning.
|
||||
if context.shell.is_subshell() {
|
||||
if self.empty_environment || self.exec_as_login || self.name_for_argv0.is_some() {
|
||||
return brush_core::error::unimp("exec with options in subshell not yet supported");
|
||||
}
|
||||
|
||||
let cmd_cmd = crate::command::CommandCommand {
|
||||
command_and_args: self.args.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
return cmd_cmd.execute(context).await;
|
||||
}
|
||||
|
||||
let mut argv0 = Cow::Borrowed(self.name_for_argv0.as_ref().unwrap_or(&self.args[0]));
|
||||
|
||||
if self.exec_as_login {
|
||||
argv0 = Cow::Owned(std::format!("-{argv0}"));
|
||||
}
|
||||
|
||||
let mut cmd = commands::compose_std_command(
|
||||
&context,
|
||||
&self.args[0],
|
||||
argv0.as_str(),
|
||||
&self.args[1..],
|
||||
self.empty_environment,
|
||||
)?;
|
||||
|
||||
let exec_error = cmd.exec();
|
||||
|
||||
if exec_error.kind() == std::io::ErrorKind::NotFound {
|
||||
Ok(ExecutionExitCode::NotFound.into())
|
||||
} else {
|
||||
Err(ErrorKind::from(exec_error).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionControlFlow, ExecutionResult, builtins};
|
||||
|
||||
/// Exit the shell.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ExitCommand {
|
||||
/// The exit code to return.
|
||||
#[arg(allow_hyphen_values = true)]
|
||||
code: Option<i64>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ExitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
let code_8bit = if let Some(code_32bit) = &self.code {
|
||||
(code_32bit & 0xFF) as u8
|
||||
} else {
|
||||
context.shell.last_exit_status()
|
||||
};
|
||||
|
||||
let mut result = ExecutionResult::new(code_8bit);
|
||||
result.next_control_flow = ExecutionControlFlow::ExitShell;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{
|
||||
ExecutionExitCode, ExecutionResult, builtins,
|
||||
env::{EnvironmentLookup, EnvironmentScope},
|
||||
parser::ast,
|
||||
variables,
|
||||
};
|
||||
|
||||
/// Add or update exported shell variables.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ExportCommand {
|
||||
/// Names are treated as function names.
|
||||
#[arg(short = 'f')]
|
||||
names_are_functions: bool,
|
||||
|
||||
/// Un-export the names.
|
||||
#[arg(short = 'n')]
|
||||
unexport: bool,
|
||||
|
||||
/// Display all exported names.
|
||||
#[arg(short = 'p')]
|
||||
display_exported_names: bool,
|
||||
|
||||
//
|
||||
// Declarations
|
||||
//
|
||||
// N.B. These are skipped by clap, but filled in by the BuiltinDeclarationCommand trait.
|
||||
#[clap(skip)]
|
||||
declarations: Vec<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for ExportCommand {
|
||||
fn set_declarations(&mut self, declarations: Vec<brush_core::CommandArg>) {
|
||||
self.declarations = declarations;
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for ExportCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.declarations.is_empty() {
|
||||
display_all_exported_vars(&context)?;
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::success();
|
||||
for decl in &self.declarations {
|
||||
let current_result = self.process_decl(&mut context, decl)?;
|
||||
if !current_result.is_success() {
|
||||
result = current_result;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExportCommand {
|
||||
fn process_decl(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
decl: &brush_core::CommandArg,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
match decl {
|
||||
brush_core::CommandArg::String(s) => {
|
||||
// See if this is supposed to be a function name.
|
||||
if self.names_are_functions {
|
||||
// Try to find the function already present; if we find it, then mark it
|
||||
// exported.
|
||||
if let Some(func) = context.shell.func_mut(s) {
|
||||
if self.unexport {
|
||||
func.unexport();
|
||||
} else {
|
||||
func.export();
|
||||
}
|
||||
} else {
|
||||
writeln!(context.stderr(), "{s}: not a function")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
}
|
||||
// Try to find the variable already present; if we find it, then mark it
|
||||
// exported.
|
||||
else if let Some((_, variable)) = context.shell.env_mut().get_mut(s) {
|
||||
if self.unexport {
|
||||
variable.unexport();
|
||||
} else {
|
||||
variable.export();
|
||||
}
|
||||
}
|
||||
}
|
||||
brush_core::CommandArg::Assignment(assignment) => {
|
||||
let name = match &assignment.name {
|
||||
ast::AssignmentName::VariableName(name) => name,
|
||||
ast::AssignmentName::ArrayElementName(_, _) => {
|
||||
writeln!(context.stderr(), "not a valid variable name")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
};
|
||||
|
||||
let value = match &assignment.value {
|
||||
ast::AssignmentValue::Scalar(s) => {
|
||||
variables::ShellValueLiteral::Scalar(s.flatten())
|
||||
}
|
||||
ast::AssignmentValue::Array(a) => {
|
||||
variables::ShellValueLiteral::Array(variables::ArrayLiteral(
|
||||
a.iter()
|
||||
.map(|(k, v)| (k.as_ref().map(|k| k.flatten()), v.flatten()))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Update the variable with the provided value and then mark it exported.
|
||||
context.shell.env_mut().update_or_add(
|
||||
name,
|
||||
value,
|
||||
|var| {
|
||||
if self.unexport {
|
||||
var.unexport();
|
||||
} else {
|
||||
var.export();
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
EnvironmentLookup::Anywhere,
|
||||
EnvironmentScope::Global,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn display_all_exported_vars(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
// Enumerate variables, sorted by key.
|
||||
for (name, variable) in context.shell.env().iter().sorted_by_key(|v| v.0) {
|
||||
if variable.is_exported() {
|
||||
let value = variable.value().try_get_cow_str(context.shell);
|
||||
if let Some(value) = value {
|
||||
writeln!(context.stdout(), "declare -x {name}=\"{value}\"")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "declare -x {name}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
#[allow(unused_imports, reason = "not all builtins are used in all configs")]
|
||||
use brush_core::builtins::{self, builtin, decl_builtin, raw_arg_builtin, simple_builtin};
|
||||
|
||||
/// Identifies well-known sets of builtins.
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
pub enum BuiltinSet {
|
||||
/// Identifies builtins appropriate for POSIX `sh` compatibility.
|
||||
ShMode,
|
||||
/// Identifies builtins appropriate for a more full-featured `bash`-compatible shell.
|
||||
BashMode,
|
||||
}
|
||||
|
||||
/// Returns the default set of built-in commands.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `set` - The set of built-ins to return.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn default_builtins<SE: brush_core::ShellExtensions>(
|
||||
set: BuiltinSet,
|
||||
) -> HashMap<String, builtins::Registration<SE>> {
|
||||
let mut m = HashMap::<String, builtins::Registration<SE>>::new();
|
||||
|
||||
//
|
||||
// POSIX special builtins
|
||||
//
|
||||
// N.B. There seems to be some inconsistency as to whether 'times'
|
||||
// should be a special built-in.
|
||||
//
|
||||
|
||||
#[cfg(feature = "builtin.break")]
|
||||
m.insert(
|
||||
"break".into(),
|
||||
builtin::<break_::BreakCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.colon")]
|
||||
m.insert(
|
||||
":".into(),
|
||||
simple_builtin::<colon::ColonCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.continue")]
|
||||
m.insert(
|
||||
"continue".into(),
|
||||
builtin::<continue_::ContinueCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.dot")]
|
||||
m.insert(".".into(), builtin::<dot::DotCommand, SE>().special());
|
||||
#[cfg(feature = "builtin.eval")]
|
||||
m.insert("eval".into(), builtin::<eval::EvalCommand, SE>().special());
|
||||
#[cfg(all(feature = "builtin.exec", unix))]
|
||||
m.insert("exec".into(), builtin::<exec::ExecCommand, SE>().special());
|
||||
#[cfg(feature = "builtin.exit")]
|
||||
m.insert("exit".into(), builtin::<exit::ExitCommand, SE>().special());
|
||||
#[cfg(feature = "builtin.export")]
|
||||
m.insert(
|
||||
"export".into(),
|
||||
decl_builtin::<export::ExportCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.return")]
|
||||
m.insert(
|
||||
"return".into(),
|
||||
builtin::<return_::ReturnCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.set")]
|
||||
m.insert("set".into(), builtin::<set::SetCommand, SE>().special());
|
||||
#[cfg(feature = "builtin.shift")]
|
||||
m.insert(
|
||||
"shift".into(),
|
||||
builtin::<shift::ShiftCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.trap")]
|
||||
m.insert("trap".into(), builtin::<trap::TrapCommand, SE>().special());
|
||||
#[cfg(feature = "builtin.unset")]
|
||||
m.insert(
|
||||
"unset".into(),
|
||||
builtin::<unset::UnsetCommand, SE>().special(),
|
||||
);
|
||||
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert(
|
||||
"readonly".into(),
|
||||
decl_builtin::<declare::DeclareCommand, SE>().special(),
|
||||
);
|
||||
#[cfg(feature = "builtin.times")]
|
||||
m.insert(
|
||||
"times".into(),
|
||||
builtin::<times::TimesCommand, SE>().special(),
|
||||
);
|
||||
|
||||
//
|
||||
// Non-special builtins
|
||||
//
|
||||
|
||||
#[cfg(feature = "builtin.alias")]
|
||||
m.insert("alias".into(), builtin::<alias::AliasCommand, SE>()); // TODO(alias): should be exec_declaration_builtin
|
||||
#[cfg(feature = "builtin.bg")]
|
||||
m.insert("bg".into(), builtin::<bg::BgCommand, SE>());
|
||||
#[cfg(feature = "builtin.cd")]
|
||||
m.insert("cd".into(), builtin::<cd::CdCommand, SE>());
|
||||
#[cfg(feature = "builtin.command")]
|
||||
m.insert("command".into(), builtin::<command::CommandCommand, SE>());
|
||||
#[cfg(feature = "builtin.false")]
|
||||
m.insert("false".into(), simple_builtin::<false_::FalseCommand, SE>());
|
||||
#[cfg(feature = "builtin.fg")]
|
||||
m.insert("fg".into(), builtin::<fg::FgCommand, SE>());
|
||||
#[cfg(feature = "builtin.getopts")]
|
||||
m.insert("getopts".into(), builtin::<getopts::GetOptsCommand, SE>());
|
||||
#[cfg(feature = "builtin.hash")]
|
||||
m.insert("hash".into(), builtin::<hash::HashCommand, SE>());
|
||||
#[cfg(feature = "builtin.help")]
|
||||
m.insert("help".into(), builtin::<help::HelpCommand, SE>());
|
||||
#[cfg(feature = "builtin.jobs")]
|
||||
m.insert("jobs".into(), builtin::<jobs::JobsCommand, SE>());
|
||||
#[cfg(all(feature = "builtin.kill", unix))]
|
||||
m.insert("kill".into(), builtin::<kill::KillCommand, SE>());
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert(
|
||||
"local".into(),
|
||||
decl_builtin::<declare::DeclareCommand, SE>(),
|
||||
);
|
||||
#[cfg(feature = "builtin.pwd")]
|
||||
m.insert("pwd".into(), builtin::<pwd::PwdCommand, SE>());
|
||||
#[cfg(feature = "builtin.read")]
|
||||
m.insert("read".into(), builtin::<read::ReadCommand, SE>());
|
||||
#[cfg(feature = "builtin.true")]
|
||||
m.insert("true".into(), simple_builtin::<true_::TrueCommand, SE>());
|
||||
#[cfg(feature = "builtin.type")]
|
||||
m.insert("type".into(), builtin::<type_::TypeCommand, SE>());
|
||||
#[cfg(all(feature = "builtin.ulimit", unix))]
|
||||
m.insert("ulimit".into(), builtin::<ulimit::ULimitCommand, SE>());
|
||||
#[cfg(all(feature = "builtin.umask", unix))]
|
||||
m.insert("umask".into(), builtin::<umask::UmaskCommand, SE>());
|
||||
#[cfg(feature = "builtin.unalias")]
|
||||
m.insert("unalias".into(), builtin::<unalias::UnaliasCommand, SE>());
|
||||
#[cfg(feature = "builtin.wait")]
|
||||
m.insert("wait".into(), builtin::<wait::WaitCommand, SE>());
|
||||
|
||||
#[cfg(feature = "builtin.fc")]
|
||||
m.insert("fc".into(), builtin::<fc::FcCommand, SE>());
|
||||
|
||||
if matches!(set, BuiltinSet::BashMode) {
|
||||
#[cfg(feature = "builtin.builtin")]
|
||||
m.insert(
|
||||
"builtin".into(),
|
||||
raw_arg_builtin::<builtin_::BuiltinCommand, SE>(),
|
||||
);
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert(
|
||||
"declare".into(),
|
||||
decl_builtin::<declare::DeclareCommand, SE>(),
|
||||
);
|
||||
#[cfg(feature = "builtin.echo")]
|
||||
m.insert("echo".into(), builtin::<echo::EchoCommand, SE>());
|
||||
#[cfg(feature = "builtin.enable")]
|
||||
m.insert("enable".into(), builtin::<enable::EnableCommand, SE>());
|
||||
#[cfg(feature = "builtin.let")]
|
||||
m.insert("let".into(), builtin::<let_::LetCommand, SE>());
|
||||
#[cfg(feature = "builtin.mapfile")]
|
||||
m.insert("mapfile".into(), builtin::<mapfile::MapFileCommand, SE>());
|
||||
#[cfg(feature = "builtin.mapfile")]
|
||||
m.insert("readarray".into(), builtin::<mapfile::MapFileCommand, SE>());
|
||||
#[cfg(all(feature = "builtin.printf", any(unix, windows)))]
|
||||
m.insert("printf".into(), builtin::<printf::PrintfCommand, SE>());
|
||||
#[cfg(feature = "builtin.shopt")]
|
||||
m.insert("shopt".into(), builtin::<shopt::ShoptCommand, SE>());
|
||||
#[cfg(feature = "builtin.dot")]
|
||||
m.insert("source".into(), builtin::<dot::DotCommand, SE>().special());
|
||||
#[cfg(all(feature = "builtin.suspend", unix))]
|
||||
m.insert("suspend".into(), builtin::<suspend::SuspendCommand, SE>());
|
||||
#[cfg(feature = "builtin.test")]
|
||||
m.insert("test".into(), builtin::<test::TestCommand, SE>());
|
||||
#[cfg(feature = "builtin.test")]
|
||||
m.insert("[".into(), builtin::<test::TestCommand, SE>());
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
m.insert(
|
||||
"typeset".into(),
|
||||
decl_builtin::<declare::DeclareCommand, SE>(),
|
||||
);
|
||||
|
||||
// Completion builtins
|
||||
#[cfg(feature = "builtin.complete")]
|
||||
m.insert(
|
||||
"complete".into(),
|
||||
builtin::<complete::CompleteCommand, SE>(),
|
||||
);
|
||||
#[cfg(feature = "builtin.compgen")]
|
||||
m.insert("compgen".into(), builtin::<complete::CompGenCommand, SE>());
|
||||
#[cfg(feature = "builtin.compopt")]
|
||||
m.insert("compopt".into(), builtin::<complete::CompOptCommand, SE>());
|
||||
|
||||
// Dir stack builtins
|
||||
#[cfg(feature = "builtin.dirs")]
|
||||
m.insert("dirs".into(), builtin::<dirs::DirsCommand, SE>());
|
||||
#[cfg(feature = "builtin.popd")]
|
||||
m.insert("popd".into(), builtin::<popd::PopdCommand, SE>());
|
||||
#[cfg(feature = "builtin.pushd")]
|
||||
m.insert("pushd".into(), builtin::<pushd::PushdCommand, SE>());
|
||||
|
||||
// Input configuration builtins
|
||||
#[cfg(feature = "builtin.bind")]
|
||||
m.insert("bind".into(), builtin::<bind::BindCommand, SE>());
|
||||
|
||||
// History
|
||||
#[cfg(feature = "builtin.history")]
|
||||
m.insert("history".into(), builtin::<history::HistoryCommand, SE>());
|
||||
|
||||
#[cfg(feature = "builtin.caller")]
|
||||
m.insert("caller".into(), builtin::<caller::CallerCommand, SE>());
|
||||
|
||||
// TODO(disown): implement disown builtin
|
||||
m.insert(
|
||||
"disown".into(),
|
||||
builtin::<unimp::UnimplementedCommand, SE>(),
|
||||
);
|
||||
|
||||
// TODO(logout): implement logout builtin
|
||||
m.insert(
|
||||
"logout".into(),
|
||||
builtin::<unimp::UnimplementedCommand, SE>(),
|
||||
);
|
||||
}
|
||||
|
||||
m
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use brush_core::{ExecutionResult, builtins, error};
|
||||
|
||||
/// Return exit code 1.
|
||||
pub(crate) struct FalseCommand {}
|
||||
|
||||
impl builtins::SimpleCommand for FalseCommand {
|
||||
fn get_content(
|
||||
_name: &str,
|
||||
content_type: builtins::ContentType,
|
||||
_options: &builtins::ContentOptions,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
match content_type {
|
||||
builtins::ContentType::DetailedHelp => Ok("Returns a failure exit status.".into()),
|
||||
builtins::ContentType::ShortUsage => Ok("false".into()),
|
||||
builtins::ContentType::ShortDescription => Ok("false - fail".into()),
|
||||
builtins::ContentType::ManPage => error::unimp("man page not yet implemented"),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute<SE: brush_core::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
|
||||
_context: brush_core::ExecutionContext<'_, SE>,
|
||||
_args: I,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
use brush_core::{ExecutionResult, builtins, error, history};
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
/// Process command history list.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct FcCommand {
|
||||
/// List commands instead of editing them.
|
||||
#[arg(short = 'l')]
|
||||
list: bool,
|
||||
|
||||
/// Suppress line numbers when listing.
|
||||
#[arg(short = 'n', requires = "list")]
|
||||
no_line_numbers: bool,
|
||||
|
||||
/// Reverse the order of commands.
|
||||
#[arg(short = 'r')]
|
||||
reverse: bool,
|
||||
|
||||
/// Re-execute command after substitution (old=new format).
|
||||
#[arg(short = 's')]
|
||||
substitute: bool,
|
||||
|
||||
/// Editor to use (only relevant when not listing or substituting).
|
||||
#[arg(short = 'e', value_name = "ENAME")]
|
||||
editor: Option<String>,
|
||||
|
||||
/// First command in range (number or string prefix).
|
||||
#[arg(value_name = "FIRST", allow_hyphen_values = true)]
|
||||
first: Option<String>,
|
||||
|
||||
/// Last command in range (number or string prefix).
|
||||
#[arg(value_name = "LAST", allow_hyphen_values = true)]
|
||||
last: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for FcCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if self.substitute {
|
||||
return self.do_execute(context).await;
|
||||
}
|
||||
|
||||
if self.list {
|
||||
return self.do_list(&context);
|
||||
}
|
||||
|
||||
error::unimp("fc editor mode is not yet implemented")
|
||||
}
|
||||
}
|
||||
|
||||
impl FcCommand {
|
||||
fn do_list(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
let history = context
|
||||
.shell
|
||||
.history()
|
||||
.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;
|
||||
|
||||
let (first_idx, last_idx, reverse) = self.resolve_range(history)?;
|
||||
|
||||
// Determine the order of iteration
|
||||
let indices: Vec<usize> = if reverse {
|
||||
(first_idx..=last_idx).rev().collect()
|
||||
} else {
|
||||
(first_idx..=last_idx).collect()
|
||||
};
|
||||
|
||||
for idx in indices {
|
||||
if let Some(item) = history.get(idx) {
|
||||
if self.no_line_numbers {
|
||||
// With -n, bash still outputs a tab before the command
|
||||
writeln!(context.stdout(), "\t {}", item.command_line)?;
|
||||
} else {
|
||||
// Match bash's fc format: number, tab, command
|
||||
writeln!(context.stdout(), "{}\t {}", idx + 1, item.command_line)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
|
||||
async fn do_execute(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
let history = context
|
||||
.shell
|
||||
.history()
|
||||
.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;
|
||||
|
||||
// Parse the first argument for pattern=replacement
|
||||
let (pattern, replacement) = self
|
||||
.first
|
||||
.as_ref()
|
||||
.and_then(|s| s.split_once('='))
|
||||
.map_or((None, None), |(p, r)| (Some(p), Some(r)));
|
||||
|
||||
// Determine which command to re-execute
|
||||
let cmd_spec = if pattern.is_some() {
|
||||
// If we have a pattern, the command spec is in 'last' if present
|
||||
self.last.as_deref()
|
||||
} else {
|
||||
// Otherwise, it's in 'first'
|
||||
self.first.as_deref()
|
||||
};
|
||||
|
||||
// Find the command
|
||||
let cmd_line = if let Some(spec) = cmd_spec {
|
||||
Self::find_command_by_specifier(history, spec)?
|
||||
} else {
|
||||
// No spec means use the previous command (excluding the fc command itself)
|
||||
let effective_count = effective_history_count(history);
|
||||
history
|
||||
.get(effective_count.saturating_sub(1))
|
||||
.map(|item| item.command_line.clone())
|
||||
.ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))?
|
||||
};
|
||||
|
||||
// Apply substitution if present
|
||||
let final_cmd = if let (Some(pat), Some(rep)) = (pattern, replacement) {
|
||||
cmd_line.replace(pat, rep)
|
||||
} else {
|
||||
cmd_line
|
||||
};
|
||||
|
||||
// Echo the command to stderr.
|
||||
writeln!(context.stderr(), "{final_cmd}")?;
|
||||
|
||||
// Remove the fc command from history before executing the substituted command
|
||||
// This matches bash behavior where the fc command is replaced by the executed command
|
||||
let history_mut = context
|
||||
.shell
|
||||
.history_mut()
|
||||
.ok_or_else(|| brush_core::Error::from(brush_core::ErrorKind::HistoryNotEnabled))?;
|
||||
history_mut.remove_nth_item(history_mut.count().saturating_sub(1));
|
||||
|
||||
let source_info = brush_core::SourceInfo::from("(history)");
|
||||
|
||||
// Execute the command
|
||||
let result = context
|
||||
.shell
|
||||
.run_string(final_cmd.clone(), &source_info, &context.params)
|
||||
.await?;
|
||||
|
||||
// Add the executed command to history.
|
||||
context.shell.add_to_history(&final_cmd)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn resolve_range(
|
||||
&self,
|
||||
history: &history::History,
|
||||
) -> Result<(usize, usize, bool), brush_core::Error> {
|
||||
let effective_count = effective_history_count(history);
|
||||
let max_idx = effective_count.saturating_sub(1);
|
||||
|
||||
// Resolve first index
|
||||
let first_idx = self
|
||||
.first
|
||||
.as_ref()
|
||||
.map(|s| Self::resolve_position(history, s))
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| {
|
||||
if self.list {
|
||||
effective_count.saturating_sub(16) // Default for listing: -16
|
||||
} else {
|
||||
max_idx // Default for editing: previous command
|
||||
}
|
||||
});
|
||||
|
||||
// Resolve last index (default depends on mode and first_idx)
|
||||
let default_last = if self.list { max_idx } else { first_idx };
|
||||
let last_idx = self
|
||||
.last
|
||||
.as_ref()
|
||||
.map(|s| Self::resolve_position(history, s))
|
||||
.transpose()?
|
||||
.unwrap_or(default_last);
|
||||
|
||||
// If first > last, swap them and indicate reversal
|
||||
let (first_idx, last_idx, force_reverse) = if first_idx > last_idx {
|
||||
(last_idx, first_idx, true)
|
||||
} else {
|
||||
(first_idx, last_idx, false)
|
||||
};
|
||||
|
||||
// Clamp both indices to valid range
|
||||
Ok((
|
||||
first_idx.min(max_idx),
|
||||
last_idx.min(max_idx),
|
||||
force_reverse || self.reverse,
|
||||
))
|
||||
}
|
||||
|
||||
/// Resolves a position specifier (number or string prefix) to a history index.
|
||||
/// NOTE: The returned index may still be out of range if the history is empty.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history` - The history to resolve against.
|
||||
/// * `spec` - The position specifier (number or string prefix).
|
||||
fn resolve_position(
|
||||
history: &history::History,
|
||||
spec: &str,
|
||||
) -> Result<usize, brush_core::Error> {
|
||||
// Try to parse it as a number. If it's not parseable, then we need to assume
|
||||
// it's a string prefix we need to search for.
|
||||
let Ok(num) = spec.parse::<i64>() else {
|
||||
// Not a number, treat as string prefix
|
||||
return Self::find_command_by_prefix(history, spec);
|
||||
};
|
||||
|
||||
let effective_count = effective_history_count(history);
|
||||
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
let result = match num.cmp(&0) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
// 0 means -1 for listing (relative to effective count)
|
||||
effective_count.saturating_sub(1)
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// Positive: 1-based index
|
||||
let idx = (num - 1) as usize;
|
||||
if idx < effective_count {
|
||||
idx
|
||||
} else {
|
||||
// Out of range - use 0 (first item)
|
||||
0
|
||||
}
|
||||
}
|
||||
std::cmp::Ordering::Less => {
|
||||
// Negative: offset from end (relative to effective count)
|
||||
let offset = (-num) as usize;
|
||||
effective_count.saturating_sub(offset)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Finds the command matching the given specifier (number or string prefix). Returns
|
||||
/// the command line. Returns an error if no such command can be found in the history.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history` - The history to search.
|
||||
/// * `spec` - The position spec
|
||||
fn find_command_by_specifier(
|
||||
history: &history::History,
|
||||
spec: &str,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
let idx = Self::resolve_position(history, spec)?;
|
||||
history
|
||||
.get(idx)
|
||||
.map(|item| item.command_line.clone())
|
||||
.ok_or_else(|| brush_core::Error::from(error::ErrorKind::HistoryItemNotFound))
|
||||
}
|
||||
|
||||
/// Finds the most recent command starting with the given prefix. Returns
|
||||
/// the index of the command in the history. Returns an error if no such
|
||||
/// command can be found in the history.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `history` - The history to search.
|
||||
/// * `prefix` - The command prefix to search for.
|
||||
fn find_command_by_prefix(
|
||||
history: &history::History,
|
||||
prefix: &str,
|
||||
) -> Result<usize, brush_core::Error> {
|
||||
// Search backwards for a command starting with the prefix (excluding fc command itself)
|
||||
let effective_count = effective_history_count(history);
|
||||
|
||||
for idx in (0..effective_count).rev() {
|
||||
if let Some(item) = history.get(idx) {
|
||||
if item.command_line.starts_with(prefix) {
|
||||
return Ok(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(brush_core::Error::from(
|
||||
error::ErrorKind::HistoryItemNotFound,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the effective history count (excluding the fc command itself).
|
||||
fn effective_history_count(history: &history::History) -> usize {
|
||||
history.count().saturating_sub(1)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, jobs, sys};
|
||||
|
||||
/// Move a specified job to the foreground.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct FgCommand {
|
||||
/// Job spec for the job to move to the foreground; if not specified, the current job is moved.
|
||||
job_spec: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for FgCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut stderr = context.stdout();
|
||||
|
||||
// Read interactive option before taking mutable borrow on jobs
|
||||
let is_interactive = context.shell.options().interactive;
|
||||
|
||||
if let Some(job_spec) = &self.job_spec {
|
||||
if let Some(job) = context.shell.jobs_mut().resolve_job_spec(job_spec) {
|
||||
job.move_to_foreground()?;
|
||||
writeln!(stderr, "{}", job.command_line)?;
|
||||
|
||||
let result = job.wait().await?;
|
||||
if is_interactive {
|
||||
sys::terminal::move_self_to_foreground()?;
|
||||
}
|
||||
|
||||
if matches!(job.state, jobs::JobState::Stopped) {
|
||||
// N.B. We use the '\r' to overwrite any ^Z output.
|
||||
let formatted = job.to_string();
|
||||
writeln!(context.stderr(), "\r{formatted}")?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
writeln!(
|
||||
stderr,
|
||||
"{}: {}: no such job",
|
||||
job_spec, context.command_name
|
||||
)?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
} else {
|
||||
if let Some(job) = context.shell.jobs_mut().current_job_mut() {
|
||||
job.move_to_foreground()?;
|
||||
writeln!(stderr, "{}", job.command_line)?;
|
||||
|
||||
let result = job.wait().await?;
|
||||
if is_interactive {
|
||||
sys::terminal::move_self_to_foreground()?;
|
||||
}
|
||||
|
||||
if matches!(job.state, jobs::JobState::Stopped) {
|
||||
// N.B. We use the '\r' to overwrite any ^Z output.
|
||||
let formatted = job.to_string();
|
||||
writeln!(context.stderr(), "\r{formatted}")?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
writeln!(stderr, "{}: no current job", context.command_name)?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
use std::{collections::HashMap, io::Write};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, env, variables};
|
||||
|
||||
/// Parse command options.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct GetOptsCommand {
|
||||
/// Specification for options
|
||||
options_string: String,
|
||||
|
||||
/// Name of variable to receive next option
|
||||
variable_name: String,
|
||||
|
||||
/// Arguments to parse
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
// We track cross-call state in special variables. They are hidden from enumeration
|
||||
// (e.g. `set`, `declare`) so they don't leak into scripts' environments.
|
||||
const VAR_GETOPTS_NEXT_CHAR_INDEX: &str = "__GETOPTS_NEXT_CHAR";
|
||||
const VAR_GETOPTS_LAST_OPTIND: &str = "__GETOPTS_LAST_OPTIND";
|
||||
const DEFAULT_NEXT_CHAR_INDEX: usize = 1;
|
||||
|
||||
/// The result of processing one option from the argument list.
|
||||
struct GetOptsResult {
|
||||
/// The value to assign to the target variable (the option char, `?`, or `:`).
|
||||
variable_value: String,
|
||||
/// The value for OPTARG, if any (option's argument or, on error, the offending char).
|
||||
optarg: Option<String>,
|
||||
/// The new value for OPTIND after this call.
|
||||
optind: usize,
|
||||
/// Exit code: success (0) if an option was found, general error (1) when done.
|
||||
exit_code: ExecutionResult,
|
||||
}
|
||||
|
||||
/// Parsed representation of the optstring (e.g., `":a:bc"`).
|
||||
struct OptionSpec {
|
||||
/// Maps each option character to whether it requires an argument.
|
||||
defs: HashMap<char, bool>,
|
||||
/// True when the optstring has a leading `:`, suppressing error messages
|
||||
/// and reporting errors via `?`/`:` in the variable and OPTARG instead.
|
||||
silent_errors: bool,
|
||||
}
|
||||
|
||||
/// Parses the optstring into an [`OptionSpec`]. A leading `:` enables silent error
|
||||
/// mode. Each letter defines an option; a `:` immediately after a letter means it
|
||||
/// takes an argument. Duplicate letters are ignored (first definition wins).
|
||||
fn parse_option_spec(spec: &str) -> OptionSpec {
|
||||
let mut defs = HashMap::<char, bool>::new();
|
||||
let mut silent_errors = false;
|
||||
|
||||
let mut last_char = None;
|
||||
for c in spec.chars() {
|
||||
if c == ':' {
|
||||
if let Some(last_char) = last_char {
|
||||
defs.insert(last_char, true);
|
||||
} else if defs.is_empty() {
|
||||
// First character is ':' — request silent error reporting.
|
||||
silent_errors = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = defs.entry(c) {
|
||||
e.insert(false);
|
||||
last_char = Some(c);
|
||||
} else {
|
||||
// Duplicate option char; first definition wins.
|
||||
// Clear last_char so a trailing colon doesn't modify the first definition.
|
||||
last_char = None;
|
||||
}
|
||||
}
|
||||
|
||||
OptionSpec {
|
||||
defs,
|
||||
silent_errors,
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for GetOptsCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
/// Override the default [`builtins::Command::new`] function to handle clap's limitation related
|
||||
/// to `--`. See [`builtins::parse_known`] for more information
|
||||
/// TODO(command): we can safely remove this after the issue is resolved
|
||||
fn new<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
if let Some(args) = rest_args {
|
||||
this.args.extend(args);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// Validate the target variable name.
|
||||
if !env::valid_variable_name(&self.variable_name) {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: `{}': not a valid identifier",
|
||||
context.command_name,
|
||||
self.variable_name
|
||||
)?;
|
||||
return Ok(ExecutionResult::new(1));
|
||||
}
|
||||
|
||||
let spec = parse_option_spec(&self.options_string);
|
||||
|
||||
// If unset or non-numeric, assume OPTIND is 1.
|
||||
let next_index_signed = context
|
||||
.shell
|
||||
.env_str("OPTIND")
|
||||
.and_then(|s| brush_core::int_utils::parse::<i32>(s.as_ref(), 10).ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
// Detect external OPTIND modifications (e.g., `OPTIND=1` to restart
|
||||
// parsing). If the current OPTIND differs from what we last set, clear
|
||||
// the internal char index so we don't resume mid-arg.
|
||||
let last_optind = context
|
||||
.shell
|
||||
.env_str(VAR_GETOPTS_LAST_OPTIND)
|
||||
.and_then(|s| s.parse::<i32>().ok());
|
||||
if last_optind != Some(next_index_signed) {
|
||||
context.shell.env_mut().unset(VAR_GETOPTS_NEXT_CHAR_INDEX)?;
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_sign_loss)] // .max(1) guarantees positive
|
||||
let next_index = next_index_signed.max(1) as usize;
|
||||
|
||||
// Select the arguments to parse. If none were explicitly provided, we
|
||||
// default to using the shell's current positional parameters.
|
||||
// Clone positional params to avoid borrowing context immutably while we
|
||||
// also need it mutably in parse_next_option.
|
||||
let owned_args;
|
||||
let args_to_parse = if !self.args.is_empty() {
|
||||
&self.args
|
||||
} else {
|
||||
owned_args = context.shell.current_shell_args().to_vec();
|
||||
&owned_args
|
||||
};
|
||||
|
||||
let result = parse_next_option(&mut context, &spec, args_to_parse, next_index)?;
|
||||
|
||||
update_variables(&mut context, &self.variable_name, result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the next option from `args_to_parse` starting at 1-based position
|
||||
/// `next_index`. Handles combined flags (e.g., `-abc`), option arguments (both
|
||||
/// `-pVALUE` and `-p VALUE` forms), and error reporting for unknown options or
|
||||
/// missing arguments. Tracks position within combined flags via the
|
||||
/// `__GETOPTS_NEXT_CHAR` shell variable.
|
||||
fn parse_next_option<SE: brush_core::ShellExtensions>(
|
||||
context: &mut brush_core::ExecutionContext<'_, SE>,
|
||||
spec: &OptionSpec,
|
||||
args_to_parse: &[String],
|
||||
mut next_index: usize,
|
||||
) -> Result<GetOptsResult, brush_core::Error> {
|
||||
// See if there are any args left to parse.
|
||||
if next_index > args_to_parse.len() {
|
||||
return Ok(GetOptsResult {
|
||||
variable_value: String::from("?"),
|
||||
optarg: None,
|
||||
// Normalize OPTIND to one past the last argument.
|
||||
optind: args_to_parse.len() + 1,
|
||||
exit_code: ExecutionResult::general_error(),
|
||||
});
|
||||
}
|
||||
|
||||
let arg = args_to_parse[next_index - 1].as_str();
|
||||
|
||||
// See if this is an option.
|
||||
if !arg.starts_with('-') || arg == "--" || arg == "-" {
|
||||
// Not an option. If it was "--", skip past it.
|
||||
return Ok(GetOptsResult {
|
||||
variable_value: String::from("?"),
|
||||
optarg: None,
|
||||
optind: if arg == "--" {
|
||||
next_index + 1
|
||||
} else {
|
||||
next_index
|
||||
},
|
||||
exit_code: ExecutionResult::general_error(),
|
||||
});
|
||||
}
|
||||
|
||||
// Figure out how far into this option we are.
|
||||
let mut next_char_index = context
|
||||
.shell
|
||||
.env_str(VAR_GETOPTS_NEXT_CHAR_INDEX)
|
||||
.map_or(DEFAULT_NEXT_CHAR_INDEX, |s| {
|
||||
s.parse().unwrap_or(DEFAULT_NEXT_CHAR_INDEX)
|
||||
});
|
||||
|
||||
// Find the char. If the index is stale (exceeds this arg's length),
|
||||
// reset to the default index, mirroring bash behavior.
|
||||
let mut c = arg.chars().nth(next_char_index);
|
||||
if c.is_none() {
|
||||
next_char_index = DEFAULT_NEXT_CHAR_INDEX;
|
||||
c = arg.chars().nth(next_char_index);
|
||||
}
|
||||
let Some(c) = c else {
|
||||
// Arg is too short even at default index.
|
||||
return Ok(GetOptsResult {
|
||||
variable_value: String::from("?"),
|
||||
optarg: None,
|
||||
optind: next_index,
|
||||
exit_code: ExecutionResult::general_error(),
|
||||
});
|
||||
};
|
||||
|
||||
let arg_char_count = arg.chars().count();
|
||||
let mut is_last_char_in_option = next_char_index == arg_char_count - 1;
|
||||
|
||||
let mut variable_value;
|
||||
let optarg;
|
||||
|
||||
// Look up the char in the option spec.
|
||||
if let Some(takes_arg) = spec.defs.get(&c) {
|
||||
variable_value = String::from(c);
|
||||
|
||||
if *takes_arg {
|
||||
(variable_value, optarg, is_last_char_in_option, next_index) = resolve_option_argument(
|
||||
context,
|
||||
spec,
|
||||
c,
|
||||
arg,
|
||||
args_to_parse,
|
||||
next_char_index,
|
||||
is_last_char_in_option,
|
||||
next_index,
|
||||
)?;
|
||||
} else {
|
||||
optarg = None;
|
||||
}
|
||||
} else {
|
||||
(variable_value, optarg) = report_unknown_option(context, spec, c)?;
|
||||
}
|
||||
|
||||
let optind = if is_last_char_in_option {
|
||||
// We're done with this argument, so unset the internal char index variable
|
||||
// and request an update to OPTIND.
|
||||
context.shell.env_mut().unset(VAR_GETOPTS_NEXT_CHAR_INDEX)?;
|
||||
next_index + 1
|
||||
} else {
|
||||
// We have more to go in this argument, so update the internal char index
|
||||
// and request that OPTIND not be updated.
|
||||
context.shell.env_mut().update_or_add(
|
||||
VAR_GETOPTS_NEXT_CHAR_INDEX,
|
||||
variables::ShellValueLiteral::Scalar((next_char_index + 1).to_string()),
|
||||
|v| {
|
||||
v.hide_from_enumeration();
|
||||
Ok(())
|
||||
},
|
||||
brush_core::env::EnvironmentLookup::Anywhere,
|
||||
brush_core::env::EnvironmentScope::Global,
|
||||
)?;
|
||||
next_index
|
||||
};
|
||||
|
||||
Ok(GetOptsResult {
|
||||
variable_value,
|
||||
optarg,
|
||||
optind,
|
||||
exit_code: ExecutionResult::success(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Resolves the argument for an option that takes a value. Returns the updated
|
||||
/// `(variable_value, optarg, is_last_char, next_index)` tuple.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn resolve_option_argument<SE: brush_core::ShellExtensions>(
|
||||
context: &brush_core::ExecutionContext<'_, SE>,
|
||||
spec: &OptionSpec,
|
||||
c: char,
|
||||
arg: &str,
|
||||
args_to_parse: &[String],
|
||||
next_char_index: usize,
|
||||
is_last_char_in_option: bool,
|
||||
mut next_index: usize,
|
||||
) -> Result<(String, Option<String>, bool, usize), brush_core::Error> {
|
||||
// If this is the last character in the token, the argument value comes from
|
||||
// the next token. Otherwise, the remainder of the current token is the value.
|
||||
if is_last_char_in_option {
|
||||
if next_index >= args_to_parse.len() {
|
||||
// Missing required argument.
|
||||
let (variable_value, optarg) = if spec.silent_errors {
|
||||
(String::from(":"), Some(String::from(c)))
|
||||
} else {
|
||||
if is_opterr_enabled(context) {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"getopts: option requires an argument -- {c}"
|
||||
)?;
|
||||
}
|
||||
(String::from("?"), None)
|
||||
};
|
||||
Ok((variable_value, optarg, true, next_index))
|
||||
} else {
|
||||
next_index += 1;
|
||||
Ok((
|
||||
String::from(c),
|
||||
Some(args_to_parse[next_index - 1].clone()),
|
||||
true,
|
||||
next_index,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
let optarg: String = arg.chars().skip(next_char_index + 1).collect();
|
||||
Ok((String::from(c), Some(optarg), true, next_index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles an unknown option character, reporting an error if appropriate.
|
||||
fn report_unknown_option<SE: brush_core::ShellExtensions>(
|
||||
context: &brush_core::ExecutionContext<'_, SE>,
|
||||
spec: &OptionSpec,
|
||||
c: char,
|
||||
) -> Result<(String, Option<String>), brush_core::Error> {
|
||||
if !spec.silent_errors && is_opterr_enabled(context) {
|
||||
writeln!(context.stderr(), "getopts: illegal option -- {c}")?;
|
||||
}
|
||||
|
||||
let optarg = if spec.silent_errors {
|
||||
Some(String::from(c))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((String::from("?"), optarg))
|
||||
}
|
||||
|
||||
/// Writes the parsing result back into shell variables: the target variable,
|
||||
/// OPTARG, OPTIND, and the internal `__GETOPTS_LAST_OPTIND` tracker.
|
||||
fn update_variables<SE: brush_core::ShellExtensions>(
|
||||
context: &mut brush_core::ExecutionContext<'_, SE>,
|
||||
variable_name: &str,
|
||||
result: GetOptsResult,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
// Update variable value.
|
||||
context.shell.env_mut().update_or_add(
|
||||
variable_name,
|
||||
variables::ShellValueLiteral::Scalar(result.variable_value),
|
||||
|_| Ok(()),
|
||||
brush_core::env::EnvironmentLookup::Anywhere,
|
||||
brush_core::env::EnvironmentScope::Global,
|
||||
)?;
|
||||
|
||||
// Update OPTARG
|
||||
if let Some(optarg) = result.optarg {
|
||||
context.shell.env_mut().update_or_add(
|
||||
"OPTARG",
|
||||
variables::ShellValueLiteral::Scalar(optarg),
|
||||
|_| Ok(()),
|
||||
brush_core::env::EnvironmentLookup::Anywhere,
|
||||
brush_core::env::EnvironmentScope::Global,
|
||||
)?;
|
||||
} else {
|
||||
context.shell.env_mut().unset("OPTARG")?;
|
||||
}
|
||||
|
||||
// Update OPTIND and record it so we can detect external modifications.
|
||||
let optind_str = result.optind.to_string();
|
||||
context.shell.env_mut().update_or_add(
|
||||
"OPTIND",
|
||||
variables::ShellValueLiteral::Scalar(optind_str.clone()),
|
||||
|_| Ok(()),
|
||||
brush_core::env::EnvironmentLookup::Anywhere,
|
||||
brush_core::env::EnvironmentScope::Global,
|
||||
)?;
|
||||
context.shell.env_mut().update_or_add(
|
||||
VAR_GETOPTS_LAST_OPTIND,
|
||||
variables::ShellValueLiteral::Scalar(optind_str),
|
||||
|v| {
|
||||
v.hide_from_enumeration();
|
||||
Ok(())
|
||||
},
|
||||
brush_core::env::EnvironmentLookup::Anywhere,
|
||||
brush_core::env::EnvironmentScope::Global,
|
||||
)?;
|
||||
|
||||
Ok(result.exit_code)
|
||||
}
|
||||
|
||||
/// Returns whether OPTERR is enabled (i.e., getopts should print error messages).
|
||||
/// OPTERR defaults to 1; any nonzero value means errors are enabled.
|
||||
fn is_opterr_enabled<SE: brush_core::ShellExtensions>(
|
||||
context: &brush_core::ExecutionContext<'_, SE>,
|
||||
) -> bool {
|
||||
context
|
||||
.shell
|
||||
.env_str("OPTERR")
|
||||
.is_none_or(|s| s.parse::<i64>().unwrap_or(1) != 0)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
use clap::Parser;
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct HashCommand {
|
||||
/// Remove entries associated with the given names.
|
||||
#[arg(short = 'd')]
|
||||
remove: bool,
|
||||
|
||||
/// Display paths in a format usable for input.
|
||||
#[arg(short = 'l')]
|
||||
display_as_usable_input: bool,
|
||||
|
||||
/// The path to associate with the names.
|
||||
#[arg(short = 'p', value_name = "PATH")]
|
||||
path_to_use: Option<PathBuf>,
|
||||
|
||||
/// Remove all entries.
|
||||
#[arg(short = 'r')]
|
||||
remove_all: bool,
|
||||
|
||||
/// Display the paths associated with the names.
|
||||
#[arg(short = 't')]
|
||||
display_paths: bool,
|
||||
|
||||
/// Names to process.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HashCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
if self.remove_all {
|
||||
context.shell.program_location_cache_mut().reset();
|
||||
} else if self.remove {
|
||||
for name in &self.names {
|
||||
if !context.shell.program_location_cache_mut().unset(name) {
|
||||
writeln!(context.stderr(), "{name}: not found")?;
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
} else if self.display_paths {
|
||||
for name in &self.names {
|
||||
if let Some(path) = context.shell.program_location_cache().get(name) {
|
||||
if self.display_as_usable_input {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"builtin hash -p {} {name}",
|
||||
path.to_string_lossy()
|
||||
)?;
|
||||
} else {
|
||||
let mut prefix = String::new();
|
||||
|
||||
if self.names.len() > 1 {
|
||||
prefix.push_str(name.as_str());
|
||||
prefix.push('\t');
|
||||
}
|
||||
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{prefix}{}",
|
||||
path.to_string_lossy().as_ref()
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
writeln!(context.stderr(), "{name}: not found")?;
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
} else if let Some(path) = &self.path_to_use {
|
||||
for name in &self.names {
|
||||
context
|
||||
.shell
|
||||
.program_location_cache_mut()
|
||||
.set(name, path.clone());
|
||||
}
|
||||
} else {
|
||||
for name in &self.names {
|
||||
// Remove from the cache if already hashed.
|
||||
let _ = context.shell.program_location_cache_mut().unset(name);
|
||||
|
||||
// Names with slashes are accepted silently
|
||||
if name.contains('/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hash the path
|
||||
if context
|
||||
.shell
|
||||
.find_first_executable_in_path_using_cache(name)
|
||||
.is_none()
|
||||
{
|
||||
writeln!(context.stderr(), "{name}: not found")?;
|
||||
result = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
|
||||
/// Display command help.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct HelpCommand {
|
||||
/// Display a short description for the commands.
|
||||
#[arg(short = 'd')]
|
||||
short_description: bool,
|
||||
|
||||
/// Display a man-style page of documentation for the commands.
|
||||
#[arg(short = 'm')]
|
||||
man_page_style: bool,
|
||||
|
||||
/// Display a short usage summary for the commands.
|
||||
#[arg(short = 's')]
|
||||
short_usage: bool,
|
||||
|
||||
/// Patterns of topics to display help for.
|
||||
topic_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HelpCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.topic_patterns.is_empty() {
|
||||
Self::display_general_help(&context)?;
|
||||
} else {
|
||||
for topic_pattern in &self.topic_patterns {
|
||||
self.display_help_for_topic_pattern(&context, topic_pattern)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
impl HelpCommand {
|
||||
fn display_general_help(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
const COLUMN_COUNT: usize = 3;
|
||||
|
||||
if let Some(display_str) = context.shell.product_display_str() {
|
||||
writeln!(context.stdout(), "{display_str}\n")?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"The following commands are implemented as shell built-ins:"
|
||||
)?;
|
||||
|
||||
let builtins = get_builtins_sorted_by_name(context);
|
||||
let items_per_column = builtins.len().div_ceil(COLUMN_COUNT);
|
||||
|
||||
for i in 0..items_per_column {
|
||||
for j in 0..COLUMN_COUNT {
|
||||
if let Some((name, builtin)) = builtins.get(i + j * items_per_column) {
|
||||
let prefix = if builtin.disabled { "*" } else { " " };
|
||||
write!(context.stdout(), " {prefix}{name:<20}")?; // adjust 20 to the desired
|
||||
// column width
|
||||
}
|
||||
}
|
||||
writeln!(context.stdout())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_help_for_topic_pattern(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
topic_pattern: &str,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let pattern = brush_core::patterns::Pattern::from(topic_pattern)
|
||||
.set_extended_globbing(context.shell.options().extended_globbing)
|
||||
.set_case_insensitive(context.shell.options().case_insensitive_pathname_expansion);
|
||||
|
||||
let mut found_count = 0;
|
||||
for (builtin_name, builtin_registration) in get_builtins_sorted_by_name(context) {
|
||||
if pattern.exactly_matches(builtin_name.as_str())? {
|
||||
self.display_help_for_builtin(
|
||||
context,
|
||||
builtin_name.as_str(),
|
||||
builtin_registration,
|
||||
)?;
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if found_count == 0 {
|
||||
writeln!(context.stderr(), "No help topics match '{topic_pattern}'")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_help_for_builtin<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, SE>,
|
||||
name: &str,
|
||||
registration: &builtins::Registration<SE>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let content_type = if self.short_description {
|
||||
builtins::ContentType::ShortDescription
|
||||
} else if self.man_page_style {
|
||||
builtins::ContentType::ManPage
|
||||
} else if self.short_usage {
|
||||
builtins::ContentType::ShortUsage
|
||||
} else {
|
||||
builtins::ContentType::DetailedHelp
|
||||
};
|
||||
|
||||
let Some(mut stdout) = context.try_fd(brush_core::openfiles::OpenFiles::STDOUT_FD) else {
|
||||
// If there's no stdout, nothing to do.
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// For now, we assume colorized output if stdout is a terminal.
|
||||
let options = builtins::ContentOptions {
|
||||
colorized: stdout.is_terminal(),
|
||||
};
|
||||
|
||||
let content = (registration.content_func)(name, content_type, &options)?;
|
||||
|
||||
write!(stdout, "{content}")?;
|
||||
stdout.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_builtins_sorted_by_name<'a, SE: brush_core::ShellExtensions>(
|
||||
context: &'a brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Vec<(&'a String, &'a builtins::Registration<SE>)> {
|
||||
context
|
||||
.shell
|
||||
.builtins()
|
||||
.iter()
|
||||
.sorted_by_key(|(name, _)| *name)
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins, error, history};
|
||||
use clap::Parser;
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
/// Query or manipulate the shell's command history.
|
||||
// TODO(history): Evaluate which of the options conflict with each other.
|
||||
#[derive(Parser)]
|
||||
#[expect(clippy::option_option)]
|
||||
pub(crate) struct HistoryCommand {
|
||||
/// Clears all history.
|
||||
#[arg(short = 'c')]
|
||||
clear_history: bool,
|
||||
|
||||
/// Deletes the history entry at the given offset. Positive offsets are relative to the
|
||||
/// beginning of the history, while negative offsets are relative to the end of the history.
|
||||
#[arg(short = 'd', value_name = "OFFSET")]
|
||||
delete_offset: Option<i64>,
|
||||
|
||||
/// Appends the history from the current session to the history file.
|
||||
#[arg(short = 'a', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")]
|
||||
append_session_to_file: Option<Option<String>>,
|
||||
|
||||
/// Appends any remaining history from the history file to the current session.
|
||||
#[arg(short = 'n', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")]
|
||||
append_rest_of_file_to_session: Option<Option<String>>,
|
||||
|
||||
/// Appends the history from the history file to the current session.
|
||||
#[arg(short = 'r', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")]
|
||||
append_file_to_session: Option<Option<String>>,
|
||||
|
||||
/// Replaces the history file with the current session history.
|
||||
#[arg(short = 'w', group = "anrw", num_args = 0..=1, value_name = "HIST_FILE")]
|
||||
write_session_to_file: Option<Option<String>>,
|
||||
|
||||
/// History-expands positional arguments and displays them.
|
||||
#[arg(short = 'p', num_args = 0.., value_name = "ARG")]
|
||||
expand_args: Option<Vec<String>>,
|
||||
|
||||
/// Appends positional arguments as an entry in the current session.
|
||||
#[arg(short = 's', num_args = 0.., value_name = "ARG")]
|
||||
append_args_to_session: Option<Vec<String>>,
|
||||
|
||||
/// Arguments.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
struct HistoryConfig {
|
||||
default_history_file_path: Option<PathBuf>,
|
||||
time_format: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for HistoryCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
// Retrieve the shell's history config while we still can.
|
||||
let config = HistoryConfig {
|
||||
default_history_file_path: context.shell.history_file_path(),
|
||||
time_format: context.shell.history_time_format(),
|
||||
};
|
||||
|
||||
let stdout = context.stdout();
|
||||
let stderr = context.stderr();
|
||||
|
||||
if let Some(history) = context.shell.history_mut() {
|
||||
self.execute_with_history(history, config, stdout, stderr)
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::HistoryNotEnabled.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCommand {
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
fn execute_with_history(
|
||||
&self,
|
||||
history: &mut history::History,
|
||||
config: HistoryConfig,
|
||||
stdout: impl Write,
|
||||
mut stderr: impl Write,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
if self.clear_history {
|
||||
history.clear()?;
|
||||
}
|
||||
|
||||
if let Some(offset) = self.delete_offset {
|
||||
if offset == 0 {
|
||||
writeln!(stderr, "cannot delete history item at offset 0")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
// Convert to 0-based index.
|
||||
let index = (offset - 1) as usize;
|
||||
if !history.remove_nth_item(index) {
|
||||
writeln!(stderr, "index past end of history")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
} else {
|
||||
let count = history.count() as i64;
|
||||
let index = count + offset;
|
||||
if index < 0 {
|
||||
writeln!(stderr, "index before beginning of history")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
let _ = history.remove_nth_item(index as usize);
|
||||
}
|
||||
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
if let Some(append_option) = &self.append_session_to_file {
|
||||
if let Some(file_path) = get_effective_history_file_path(
|
||||
config.default_history_file_path,
|
||||
append_option.as_ref(),
|
||||
) {
|
||||
history.flush(
|
||||
file_path,
|
||||
true, /* append? */
|
||||
true, /* unsaved items only */
|
||||
config.time_format.is_some(), /* write timestamps? */
|
||||
)?;
|
||||
}
|
||||
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
if self.append_rest_of_file_to_session.is_some() {
|
||||
return error::unimp("history -n is not yet implemented");
|
||||
}
|
||||
|
||||
if self.append_file_to_session.is_some() {
|
||||
return error::unimp("history -r is not yet implemented");
|
||||
}
|
||||
|
||||
if let Some(write_option) = &self.write_session_to_file {
|
||||
if let Some(file_path) = get_effective_history_file_path(
|
||||
config.default_history_file_path,
|
||||
write_option.as_ref(),
|
||||
) {
|
||||
history.flush(
|
||||
file_path,
|
||||
false, /* append? */
|
||||
false, /* unsaved items only? */
|
||||
config.time_format.is_some(), /* write timestamps? */
|
||||
)?;
|
||||
}
|
||||
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
if self.expand_args.is_some() {
|
||||
return error::unimp("history -p is not yet implemented");
|
||||
}
|
||||
|
||||
if let Some(args) = &self.append_args_to_session {
|
||||
history.add(history::Item::new(args.join(" ")))?;
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
let max_entries: Option<usize> = if let Some(arg) = self.args.first() {
|
||||
Some(brush_core::int_utils::parse(arg.as_str(), 10)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
display_history(history, &config, max_entries, stdout, stderr)?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn display_history(
|
||||
history: &history::History,
|
||||
config: &HistoryConfig,
|
||||
max_entries: Option<usize>,
|
||||
mut stdout: impl Write,
|
||||
_stderr: impl Write,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let item_count = history.count();
|
||||
let skip_count = item_count - max_entries.unwrap_or(item_count);
|
||||
|
||||
for (i, item) in history.iter().skip(skip_count).enumerate() {
|
||||
let mut formatted_timestamp = String::new();
|
||||
|
||||
if let Some(timestamp) = item.timestamp {
|
||||
let local_timestamp = timestamp.with_timezone(&chrono::Local);
|
||||
if let Some(time_format) = &config.time_format {
|
||||
let fmt_items = chrono::format::StrftimeItems::new(time_format);
|
||||
formatted_timestamp = local_timestamp.format_with_items(fmt_items).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Output format is something like:
|
||||
// 1 echo hello world
|
||||
std::writeln!(
|
||||
stdout,
|
||||
"{:>5} {formatted_timestamp}{}",
|
||||
skip_count + i + 1,
|
||||
item.command_line
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_effective_history_file_path(
|
||||
default_history_file_path: Option<PathBuf>,
|
||||
option: Option<&String>,
|
||||
) -> Option<PathBuf> {
|
||||
option.map_or_else(
|
||||
|| default_history_file_path,
|
||||
|file_path| Some(PathBuf::from(file_path)),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
|
||||
#[test]
|
||||
fn test_parse_dash_a() -> Result<()> {
|
||||
let cmd = HistoryCommand::try_parse_from(["history", "5"])?;
|
||||
assert_matches!(cmd.append_session_to_file, None);
|
||||
|
||||
let cmd = HistoryCommand::try_parse_from(["history", "-a"])?;
|
||||
assert_matches!(cmd.append_session_to_file, Some(None));
|
||||
|
||||
let cmd = HistoryCommand::try_parse_from(["history", "-a", "token"])?;
|
||||
assert_eq!(
|
||||
cmd.append_session_to_file,
|
||||
Some(Some(String::from("token")))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, error, jobs};
|
||||
|
||||
/// Manage jobs.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct JobsCommand {
|
||||
/// Also show process IDs.
|
||||
#[arg(short = 'l')]
|
||||
also_show_pids: bool,
|
||||
|
||||
/// List only jobs that have changed status since the last notification.
|
||||
#[arg(short = 'n')]
|
||||
list_changed_only: bool,
|
||||
|
||||
/// Show only process IDs.
|
||||
#[arg(short = 'p')]
|
||||
show_pids_only: bool,
|
||||
|
||||
/// Show only running jobs.
|
||||
#[arg(short = 'r')]
|
||||
running_jobs_only: bool,
|
||||
|
||||
/// Show only stopped jobs.
|
||||
#[arg(short = 's')]
|
||||
stopped_jobs_only: bool,
|
||||
|
||||
/// Job specs to list.
|
||||
// TODO(jobs): Add -x option
|
||||
job_specs: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for JobsCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.also_show_pids {
|
||||
return error::unimp("jobs -l");
|
||||
}
|
||||
if self.list_changed_only {
|
||||
return error::unimp("jobs -n");
|
||||
}
|
||||
|
||||
if self.job_specs.is_empty() {
|
||||
for job in &context.shell.jobs().jobs {
|
||||
self.display_job(&context, job)?;
|
||||
}
|
||||
} else {
|
||||
return error::unimp("jobs with job specs");
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
impl JobsCommand {
|
||||
fn display_job(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
job: &jobs::Job,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
if self.running_jobs_only && !matches!(job.state, jobs::JobState::Running) {
|
||||
return Ok(());
|
||||
}
|
||||
if self.stopped_jobs_only && !matches!(job.state, jobs::JobState::Stopped) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.show_pids_only {
|
||||
if let Some(pid) = job.representative_pid() {
|
||||
writeln!(context.stdout(), "{pid}")?;
|
||||
}
|
||||
} else {
|
||||
writeln!(context.stdout(), "{job}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::traps::TrapSignal;
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins, sys};
|
||||
|
||||
/// Signal a job or process.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct KillCommand {
|
||||
/// Name of the signal to send.
|
||||
#[arg(short = 's', value_name = "SIG_NAME")]
|
||||
signal_name: Option<String>,
|
||||
|
||||
/// Number of the signal to send.
|
||||
#[arg(short = 'n', value_name = "SIG_NUM")]
|
||||
signal_number: Option<usize>,
|
||||
|
||||
//
|
||||
// TODO(kill): implement -sigspec syntax
|
||||
/// List known signal names.
|
||||
#[arg(short = 'l', short_alias = 'L')]
|
||||
list_signals: bool,
|
||||
|
||||
// Interpretation of these depends on whether -l is present.
|
||||
#[arg(allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for KillCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
// Default signal is SIGKILL.
|
||||
let mut trap_signal = TrapSignal::Signal(nix::sys::signal::Signal::SIGKILL);
|
||||
|
||||
// Try parsing the signal name (if specified).
|
||||
if let Some(signal_name) = &self.signal_name {
|
||||
if let Ok(parsed_trap_signal) = TrapSignal::try_from(signal_name.as_str()) {
|
||||
trap_signal = parsed_trap_signal;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: invalid signal name: {}",
|
||||
context.command_name,
|
||||
signal_name
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing the signal number (if specified).
|
||||
if let Some(signal_number) = &self.signal_number {
|
||||
#[expect(clippy::cast_possible_truncation)]
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
if let Ok(parsed_trap_signal) = TrapSignal::try_from(*signal_number as i32) {
|
||||
trap_signal = parsed_trap_signal;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: invalid signal number: {}",
|
||||
context.command_name,
|
||||
signal_number
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Look through the remaining args for a pid/job spec or a -sigspec style option.
|
||||
let mut pid_or_job_spec = None;
|
||||
for arg in &self.args {
|
||||
// See if this is -sigspec syntax.
|
||||
if let Some(possible_sigspec) = arg.strip_prefix("-") {
|
||||
// See if this is -sigspec syntax.
|
||||
if let Ok(parsed_trap_signal) = TrapSignal::try_from(possible_sigspec) {
|
||||
trap_signal = parsed_trap_signal;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: invalid signal name",
|
||||
context.command_name
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
} else if pid_or_job_spec.is_none() {
|
||||
pid_or_job_spec = Some(arg);
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: too many jobs or processes specified",
|
||||
context.command_name
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
}
|
||||
|
||||
if self.list_signals {
|
||||
return print_signals(&context, self.args.as_ref());
|
||||
} else {
|
||||
let Some(pid_or_job_spec) = pid_or_job_spec else {
|
||||
writeln!(context.stderr(), "{}: invalid usage", context.command_name)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
};
|
||||
|
||||
if pid_or_job_spec.starts_with('%') {
|
||||
// It's a job spec.
|
||||
if let Some(job) = context.shell.jobs_mut().resolve_job_spec(pid_or_job_spec) {
|
||||
job.kill(trap_signal)?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {}: no such job",
|
||||
context.command_name,
|
||||
pid_or_job_spec
|
||||
)?;
|
||||
return Ok(ExecutionResult::general_error());
|
||||
}
|
||||
} else {
|
||||
let pid = brush_core::int_utils::parse(pid_or_job_spec.as_str(), 10)?;
|
||||
|
||||
// It's a pid.
|
||||
sys::signal::kill_process(pid, trap_signal)?;
|
||||
}
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_signals(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
signals: &[String],
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
let mut exit_code = ExecutionResult::success();
|
||||
if !signals.is_empty() {
|
||||
for s in signals {
|
||||
// If the user gives us a code, we print the name; if they give a name, we print its
|
||||
// code.
|
||||
enum PrintSignal {
|
||||
Name(&'static str),
|
||||
Num(i32),
|
||||
}
|
||||
|
||||
let signal = if let Ok(n) = s.parse::<i32>() {
|
||||
// bash compatibility. `SIGHUP` -> `HUP`
|
||||
TrapSignal::try_from(n).map(|s| {
|
||||
PrintSignal::Name(s.as_str().strip_prefix("SIG").unwrap_or(s.as_str()))
|
||||
})
|
||||
} else {
|
||||
TrapSignal::try_from(s.as_str()).map(|sig| {
|
||||
i32::try_from(sig).map_or(PrintSignal::Name(sig.as_str()), PrintSignal::Num)
|
||||
})
|
||||
};
|
||||
|
||||
match signal {
|
||||
Ok(PrintSignal::Num(n)) => {
|
||||
writeln!(context.stdout(), "{n}")?;
|
||||
}
|
||||
Ok(PrintSignal::Name(s)) => {
|
||||
writeln!(context.stdout(), "{s}")?;
|
||||
}
|
||||
Err(e) => {
|
||||
writeln!(context.stderr(), "{e}")?;
|
||||
exit_code = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return brush_core::traps::format_signals(
|
||||
context.stdout(),
|
||||
TrapSignal::iterator().filter(|s| !matches!(s, TrapSignal::Exit)),
|
||||
)
|
||||
.map(|()| ExecutionResult::success());
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, arithmetic::Evaluatable, builtins};
|
||||
|
||||
/// Evaluate arithmetic expressions.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct LetCommand {
|
||||
/// Arithmetic expressions to evaluate.
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
exprs: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for LetCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionExitCode::InvalidUsage.into();
|
||||
|
||||
if self.exprs.is_empty() {
|
||||
writeln!(context.stderr(), "missing expression")?;
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
for expr in &self.exprs {
|
||||
let parsed = brush_parser::arithmetic::parse(expr.as_str())?;
|
||||
let evaluated = parsed.eval(context.shell)?;
|
||||
|
||||
if evaluated == 0 {
|
||||
result = ExecutionResult::general_error();
|
||||
} else {
|
||||
result = ExecutionResult::success();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Standard builtins.
|
||||
|
||||
#[cfg(feature = "builtin.alias")]
|
||||
mod alias;
|
||||
#[cfg(feature = "builtin.bg")]
|
||||
mod bg;
|
||||
#[cfg(feature = "builtin.bind")]
|
||||
mod bind;
|
||||
#[cfg(feature = "builtin.break")]
|
||||
mod break_;
|
||||
#[cfg(feature = "builtin.builtin")]
|
||||
mod builtin_;
|
||||
#[cfg(feature = "builtin.caller")]
|
||||
mod caller;
|
||||
#[cfg(feature = "builtin.cd")]
|
||||
mod cd;
|
||||
#[cfg(feature = "builtin.colon")]
|
||||
mod colon;
|
||||
#[cfg(feature = "builtin.command")]
|
||||
mod command;
|
||||
#[cfg(any(
|
||||
feature = "builtin.complete",
|
||||
feature = "builtin.compgen",
|
||||
feature = "builtin.compopt"
|
||||
))]
|
||||
mod complete;
|
||||
#[cfg(feature = "builtin.continue")]
|
||||
mod continue_;
|
||||
#[cfg(feature = "builtin.declare")]
|
||||
mod declare;
|
||||
#[cfg(feature = "builtin.dirs")]
|
||||
mod dirs;
|
||||
#[cfg(feature = "builtin.dot")]
|
||||
mod dot;
|
||||
#[cfg(feature = "builtin.echo")]
|
||||
mod echo;
|
||||
#[cfg(feature = "builtin.enable")]
|
||||
mod enable;
|
||||
#[cfg(feature = "builtin.eval")]
|
||||
mod eval;
|
||||
#[cfg(all(feature = "builtin.exec", unix))]
|
||||
mod exec;
|
||||
#[cfg(feature = "builtin.exit")]
|
||||
mod exit;
|
||||
#[cfg(feature = "builtin.export")]
|
||||
mod export;
|
||||
#[cfg(feature = "builtin.false")]
|
||||
mod false_;
|
||||
#[cfg(feature = "builtin.fc")]
|
||||
mod fc;
|
||||
#[cfg(feature = "builtin.fg")]
|
||||
mod fg;
|
||||
#[cfg(feature = "builtin.getopts")]
|
||||
mod getopts;
|
||||
#[cfg(feature = "builtin.hash")]
|
||||
mod hash;
|
||||
#[cfg(feature = "builtin.help")]
|
||||
mod help;
|
||||
#[cfg(feature = "builtin.history")]
|
||||
mod history;
|
||||
#[cfg(feature = "builtin.jobs")]
|
||||
mod jobs;
|
||||
#[cfg(all(feature = "builtin.kill", unix))]
|
||||
mod kill;
|
||||
#[cfg(feature = "builtin.let")]
|
||||
mod let_;
|
||||
#[cfg(feature = "builtin.mapfile")]
|
||||
mod mapfile;
|
||||
#[cfg(feature = "builtin.popd")]
|
||||
mod popd;
|
||||
#[cfg(all(feature = "builtin.printf", any(unix, windows)))]
|
||||
mod printf;
|
||||
#[cfg(feature = "builtin.pushd")]
|
||||
mod pushd;
|
||||
#[cfg(feature = "builtin.pwd")]
|
||||
mod pwd;
|
||||
#[cfg(feature = "builtin.read")]
|
||||
mod read;
|
||||
#[cfg(feature = "builtin.return")]
|
||||
mod return_;
|
||||
#[cfg(feature = "builtin.set")]
|
||||
mod set;
|
||||
#[cfg(feature = "builtin.shift")]
|
||||
mod shift;
|
||||
#[cfg(feature = "builtin.shopt")]
|
||||
mod shopt;
|
||||
#[cfg(all(feature = "builtin.suspend", unix))]
|
||||
mod suspend;
|
||||
#[cfg(feature = "builtin.test")]
|
||||
mod test;
|
||||
#[cfg(feature = "builtin.times")]
|
||||
mod times;
|
||||
#[cfg(feature = "builtin.trap")]
|
||||
mod trap;
|
||||
#[cfg(feature = "builtin.true")]
|
||||
mod true_;
|
||||
#[cfg(feature = "builtin.type")]
|
||||
mod type_;
|
||||
#[cfg(all(feature = "builtin.ulimit", unix))]
|
||||
mod ulimit;
|
||||
#[cfg(all(feature = "builtin.umask", unix))]
|
||||
mod umask;
|
||||
#[cfg(feature = "builtin.unalias")]
|
||||
mod unalias;
|
||||
#[cfg(feature = "builtin.unset")]
|
||||
mod unset;
|
||||
#[cfg(feature = "builtin.wait")]
|
||||
mod wait;
|
||||
|
||||
mod builder;
|
||||
mod factory;
|
||||
mod unimp;
|
||||
|
||||
pub use builder::ShellBuilderExt;
|
||||
pub use factory::{BuiltinSet, default_builtins};
|
||||
|
||||
/// Macro to define a struct that represents a shell built-in flag argument that can be
|
||||
/// enabled or disabled by specifying an option with a leading '+' or '-' character.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `$struct_name` - The identifier to be used for the struct to define.
|
||||
/// - `$flag_char` - The character to use as the flag.
|
||||
/// - `$desc` - The string description of the flag.
|
||||
#[macro_export]
|
||||
macro_rules! minus_or_plus_flag_arg {
|
||||
($struct_name:ident, $flag_char:literal, $desc:literal) => {
|
||||
#[derive(clap::Parser)]
|
||||
pub(crate) struct $struct_name {
|
||||
#[arg(short = $flag_char, name = concat!(stringify!($struct_name), "_enable"), action = clap::ArgAction::SetTrue, help = $desc)]
|
||||
_enable: bool,
|
||||
#[arg(long = concat!("+", $flag_char), name = concat!(stringify!($struct_name), "_disable"), action = clap::ArgAction::SetTrue, hide = true)]
|
||||
_disable: bool,
|
||||
}
|
||||
|
||||
impl From<$struct_name> for Option<bool> {
|
||||
fn from(value: $struct_name) -> Self {
|
||||
value.to_bool()
|
||||
}
|
||||
}
|
||||
|
||||
impl $struct_name {
|
||||
#[allow(dead_code, reason = "may not be used in all macro instantiations")]
|
||||
pub const fn is_some(&self) -> bool {
|
||||
self._enable || self._disable
|
||||
}
|
||||
|
||||
pub const fn to_bool(&self) -> Option<bool> {
|
||||
match (self._enable, self._disable) {
|
||||
(true, false) => Some(true),
|
||||
(false, true) => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ErrorKind, ExecutionExitCode, ExecutionResult, builtins, env, error, variables};
|
||||
|
||||
/// Read lines from standard input into an indexed array variable.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct MapFileCommand {
|
||||
/// Delimiter to use (defaults to newline).
|
||||
#[arg(short = 'd')]
|
||||
delimiter: Option<String>,
|
||||
|
||||
/// Maximum number of entries to read (0 means no limit).
|
||||
#[arg(short = 'n', default_value_t = 0)]
|
||||
max_count: i64,
|
||||
|
||||
/// Index into array at which to start assignment.
|
||||
#[arg(short = 'O', allow_hyphen_values = true)]
|
||||
origin: Option<i64>,
|
||||
|
||||
/// Number of initial entries to skip.
|
||||
#[arg(short = 's', default_value_t = 0, value_parser = clap::value_parser!(i64).range(0..))]
|
||||
skip_count: i64,
|
||||
|
||||
/// Whether or not to remove the delimiter from each read line.
|
||||
#[arg(short = 't')]
|
||||
remove_delimiter: bool,
|
||||
|
||||
/// File descriptor to read from (defaults to stdin).
|
||||
#[arg(short = 'u', default_value_t = 0)]
|
||||
fd: brush_core::ShellFd,
|
||||
|
||||
/// Name of function to call for each group of lines.
|
||||
#[arg(short = 'C')]
|
||||
callback: Option<String>,
|
||||
|
||||
/// Number of lines to pass the callback for each group.
|
||||
#[arg(short = 'c', default_value_t = 5000, value_parser = clap::value_parser!(i64).range(1..))]
|
||||
callback_group_size: i64,
|
||||
|
||||
/// Name of array to read into.
|
||||
#[arg(default_value = "MAPFILE")]
|
||||
array_var_name: String,
|
||||
}
|
||||
|
||||
impl builtins::Command for MapFileCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.callback_group_size != 5000 || self.callback.is_some() {
|
||||
return error::unimp("mapfile -C/-c is not yet implemented");
|
||||
}
|
||||
|
||||
if let Some(origin) = self.origin {
|
||||
if origin < 0 {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {origin}: invalid array origin",
|
||||
context.command_name
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::GeneralError.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((_, var)) = context.shell.env().get(&self.array_var_name) {
|
||||
if matches!(
|
||||
var.value(),
|
||||
variables::ShellValue::AssociativeArray(_)
|
||||
| variables::ShellValue::Unset(
|
||||
variables::ShellValueUnsetType::AssociativeArray
|
||||
)
|
||||
) {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {}: not an indexed array",
|
||||
context.command_name,
|
||||
self.array_var_name
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::GeneralError.into());
|
||||
}
|
||||
}
|
||||
|
||||
let input_file = context
|
||||
.try_fd(self.fd)
|
||||
.ok_or_else(|| ErrorKind::BadFileDescriptor(self.fd))?;
|
||||
|
||||
// Read!
|
||||
let results = self.read_entries(input_file)?;
|
||||
|
||||
if let Some(origin) = self.origin {
|
||||
// -O: preserve existing array, assign at offset.
|
||||
for (elem_idx, (_key, value)) in results.0.into_iter().enumerate() {
|
||||
// If the user is getting to wraparounds in *bash*, they got bigger problems.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
let elem_idx = elem_idx as i64;
|
||||
context.shell.env_mut().update_or_add_array_element(
|
||||
&self.array_var_name,
|
||||
(elem_idx + origin).to_string(),
|
||||
value,
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// No -O: replace the entire variable (clears existing).
|
||||
context.shell.env_mut().update_or_add(
|
||||
&self.array_var_name,
|
||||
variables::ShellValueLiteral::Array(results),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
impl MapFileCommand {
|
||||
fn read_entries(
|
||||
&self,
|
||||
mut input_file: brush_core::openfiles::OpenFile,
|
||||
) -> Result<variables::ArrayLiteral, brush_core::Error> {
|
||||
let _term_mode = setup_terminal_settings(&input_file)?;
|
||||
|
||||
let mut entries = vec![];
|
||||
let mut read_count = 0;
|
||||
let max_count = self.max_count.try_into()?;
|
||||
let delimiter = match &self.delimiter {
|
||||
Some(d) if d.is_empty() => b'\0',
|
||||
Some(d) => d.as_bytes().first().copied().unwrap_or(b'\n'),
|
||||
None => b'\n',
|
||||
};
|
||||
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
while max_count == 0 || entries.len() < max_count {
|
||||
let mut line = vec![];
|
||||
let mut saw_delimiter = false;
|
||||
|
||||
loop {
|
||||
match input_file.read(&mut buf) {
|
||||
Ok(0) => break, // End of input
|
||||
Ok(1) if buf[0] == b'\x03' => break, // Ctrl+C
|
||||
Ok(1) if buf[0] == b'\x04' && line.is_empty() => break, // Ctrl+D
|
||||
Ok(1) => {
|
||||
let byte = buf[0];
|
||||
line.push(byte);
|
||||
if byte == delimiter {
|
||||
saw_delimiter = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => unreachable!("input can only be 0, 1, or error"),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
if line.is_empty() && !saw_delimiter {
|
||||
break;
|
||||
}
|
||||
|
||||
if read_count < self.skip_count {
|
||||
read_count += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.remove_delimiter && line.ends_with(&[delimiter]) {
|
||||
line.pop();
|
||||
}
|
||||
|
||||
let line_str = String::from_utf8_lossy(&line).to_string();
|
||||
|
||||
entries.push((None, line_str));
|
||||
}
|
||||
|
||||
Ok(variables::ArrayLiteral(entries))
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_terminal_settings(
|
||||
file: &brush_core::openfiles::OpenFile,
|
||||
) -> Result<Option<brush_core::terminal::AutoModeGuard>, brush_core::Error> {
|
||||
let mode = brush_core::terminal::AutoModeGuard::new(file.to_owned()).ok();
|
||||
if let Some(mode) = &mode {
|
||||
let config = brush_core::terminal::Settings::builder()
|
||||
.line_input(false)
|
||||
.interrupt_signals(false)
|
||||
.build();
|
||||
|
||||
mode.apply_settings(&config)?;
|
||||
}
|
||||
|
||||
Ok(mode)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Pop a path from the current directory stack.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct PopdCommand {
|
||||
/// Pop the path without changing the current working directory.
|
||||
#[clap(short = 'n')]
|
||||
no_directory_change: bool,
|
||||
//
|
||||
// TODO(popd): implement +N and -N
|
||||
}
|
||||
|
||||
impl builtins::Command for PopdCommand {
|
||||
type Error = crate::dirs::DirError;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if let Some(popped) = context.shell.directory_stack_mut().pop() {
|
||||
if !self.no_directory_change {
|
||||
context.shell.set_working_dir(&popped)?;
|
||||
}
|
||||
|
||||
// Display dirs.
|
||||
let dirs_cmd = crate::dirs::DirsCommand::default();
|
||||
dirs_cmd.execute(context).await?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
Err(crate::dirs::DirError::DirStackEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
use clap::Parser;
|
||||
use std::{ffi::OsString, io::Write, ops::ControlFlow};
|
||||
use uucore::format;
|
||||
|
||||
use brush_core::{Error, ErrorKind, ExecutionResult, builtins, escape, expansion};
|
||||
|
||||
/// Format a string.
|
||||
#[derive(Parser)]
|
||||
#[clap(disable_help_flag = true, disable_version_flag = true)]
|
||||
pub(crate) struct PrintfCommand {
|
||||
/// If specified, the output of the command is assigned to this variable.
|
||||
#[arg(short = 'v')]
|
||||
output_variable: Option<String>,
|
||||
|
||||
/// Format string + arguments to the format string.
|
||||
#[arg(trailing_var_arg = true, required = true, allow_hyphen_values = true)]
|
||||
format_and_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for PrintfCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if let Some(variable_name) = &self.output_variable {
|
||||
// Format to a u8 vector.
|
||||
let mut result: Vec<u8> = vec![];
|
||||
format(self.format_and_args.as_slice(), &mut result)?;
|
||||
|
||||
// Convert to a string.
|
||||
let result_str = String::from_utf8(result).map_err(|_| {
|
||||
brush_core::ErrorKind::PrintfInvalidUsage("invalid UTF-8 output".into())
|
||||
})?;
|
||||
|
||||
// Assign to the selected variable.
|
||||
expansion::assign_to_named_parameter(
|
||||
context.shell,
|
||||
&context.params,
|
||||
variable_name,
|
||||
result_str,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
format(self.format_and_args.as_slice(), context.stdout())?;
|
||||
context.stdout().flush()?;
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn format(format_and_args: &[String], writer: impl Write) -> Result<(), brush_core::Error> {
|
||||
match format_and_args {
|
||||
// Special-case invocation of printf with %q-based format string from bash-completion.
|
||||
// It has hard-coded expectation of backslash-style escaping instead of quoting.
|
||||
[fmt, arg] if fmt == "%q" => format_special_case_for_percent_q(None, arg, writer),
|
||||
[fmt, arg] if fmt == "~%q" => format_special_case_for_percent_q(Some("~"), arg, writer),
|
||||
// Handle format string with arguments using uucore
|
||||
[fmt, args @ ..] => format_via_uucore(fmt, args.iter(), writer),
|
||||
// Handle case with no format string (we shouldn't be able to get here since clap will
|
||||
// fail parsing when the format string is missing)
|
||||
[] => Err(ErrorKind::PrintfInvalidUsage("missing operand".into()).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_special_case_for_percent_q(
|
||||
prefix: Option<&str>,
|
||||
arg: &str,
|
||||
mut writer: impl Write,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let mut result = escape::quote_if_needed(arg, escape::QuoteMode::BackslashEscape).to_string();
|
||||
|
||||
if let Some(prefix) = prefix {
|
||||
result.insert_str(0, prefix);
|
||||
}
|
||||
|
||||
write!(writer, "{result}")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_via_uucore(
|
||||
format_string: &str,
|
||||
args: impl Iterator<Item = impl Into<OsString>>,
|
||||
mut writer: impl Write,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
// Convert string arguments to FormatArgument::Unparsed
|
||||
let format_args: Vec<_> = args
|
||||
.map(|s| format::FormatArgument::Unparsed(s.into()))
|
||||
.collect();
|
||||
|
||||
// Parse format string once.
|
||||
let format_items = parse_format_string(format_string)?;
|
||||
|
||||
// Wrap the format arguments.
|
||||
let mut format_args_wrapper = format::FormatArguments::new(&format_args);
|
||||
|
||||
// Keep going until we've exhausted all format arguments. Also make sure to run at least once
|
||||
// even if there's no format arguments.
|
||||
while format_args.is_empty() || !format_args_wrapper.is_exhausted() {
|
||||
// Process all format items, in order. We'll bail when we're told to stop.
|
||||
for item in &format_items {
|
||||
let control_flow = item
|
||||
.write(&mut writer, &mut format_args_wrapper)
|
||||
.map_err(|e| match e {
|
||||
// Propagate I/O errors directly so they can be handled appropriately
|
||||
format::FormatError::IoError(io_err) => Error::from(io_err),
|
||||
// Wrap other format errors
|
||||
other => Error::from(ErrorKind::PrintfInvalidUsage(std::format!(
|
||||
"printf formatting error: {other}"
|
||||
))),
|
||||
})?;
|
||||
|
||||
if control_flow == ControlFlow::Break(()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start next batch if not exhausted
|
||||
if !format_args_wrapper.is_exhausted() {
|
||||
format_args_wrapper.start_next_batch();
|
||||
}
|
||||
|
||||
if format_args.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_format_string(
|
||||
format_string: &str,
|
||||
) -> Result<Vec<format::FormatItem<format::EscapedChar>>, brush_core::Error> {
|
||||
let format_items: Result<Vec<_>, _> =
|
||||
format::parse_spec_and_escape(format_string.as_bytes()).collect();
|
||||
|
||||
// Observe any errors we encountered along the way.
|
||||
let format_items = format_items
|
||||
.map_err(|e| ErrorKind::PrintfInvalidUsage(format!("printf parsing error: {e}")))?;
|
||||
|
||||
Ok(format_items)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[expect(clippy::panic_in_result_fn)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
|
||||
fn sprintf_via_uucore(
|
||||
format_string: &str,
|
||||
args: impl Iterator<Item = impl Into<OsString>>,
|
||||
) -> Result<String> {
|
||||
let mut result = vec![];
|
||||
format_via_uucore(format_string, args, &mut result)?;
|
||||
|
||||
Ok(String::from_utf8(result)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_sprintf() -> Result<()> {
|
||||
assert_eq!(sprintf_via_uucore("%s", std::iter::once(&"xyz"))?, "xyz");
|
||||
assert_eq!(sprintf_via_uucore(r"%d\n", std::iter::once(&"1"))?, "1\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sprintf_without_args() -> Result<()> {
|
||||
let empty: [&str; 0] = [];
|
||||
|
||||
assert_eq!(sprintf_via_uucore("xyz", empty.iter())?, "xyz");
|
||||
assert_eq!(sprintf_via_uucore("%s|", empty.iter())?, "|");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sprintf_with_cycles() -> Result<()> {
|
||||
assert_eq!(sprintf_via_uucore("%s|", ["x", "y"].iter())?, "x|y|");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Push a path onto the current directory stack.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct PushdCommand {
|
||||
/// Push the path without changing the current working directory.
|
||||
#[clap(short = 'n')]
|
||||
no_directory_change: bool,
|
||||
|
||||
/// Directory to push on the directory stack.
|
||||
dir: String,
|
||||
//
|
||||
// TODO(pushd): implement +N and -N
|
||||
}
|
||||
|
||||
impl builtins::Command for PushdCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.no_directory_change {
|
||||
context
|
||||
.shell
|
||||
.directory_stack_mut()
|
||||
.push(std::path::PathBuf::from(&self.dir));
|
||||
} else {
|
||||
let prev_working_dir = context.shell.working_dir().to_path_buf();
|
||||
|
||||
let dir = std::path::Path::new(&self.dir);
|
||||
context.shell.set_working_dir(dir)?;
|
||||
|
||||
context.shell.directory_stack_mut().push(prev_working_dir);
|
||||
}
|
||||
|
||||
// Display dirs.
|
||||
let dirs_cmd = crate::dirs::DirsCommand::default();
|
||||
dirs_cmd.execute(context).await?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
use clap::Parser;
|
||||
use std::{borrow::Cow, io::Write, path::Path};
|
||||
|
||||
/// Display the current working directory.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct PwdCommand {
|
||||
/// Print the physical directory without any symlinks.
|
||||
#[arg(short = 'P', overrides_with = "allow_symlinks")]
|
||||
physical: bool,
|
||||
|
||||
/// Print $PWD if it names the current working directory.
|
||||
#[arg(short = 'L', overrides_with = "physical")]
|
||||
allow_symlinks: bool,
|
||||
}
|
||||
|
||||
impl builtins::Command for PwdCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut cwd: Cow<'_, Path> = context.shell.working_dir().into();
|
||||
|
||||
let should_canonicalize = self.physical
|
||||
|| context
|
||||
.shell
|
||||
.options()
|
||||
.do_not_resolve_symlinks_when_changing_dir;
|
||||
|
||||
if should_canonicalize {
|
||||
cwd = cwd.canonicalize()?.into();
|
||||
}
|
||||
|
||||
writeln!(context.stdout(), "{}", cwd.to_string_lossy())?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,840 @@
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use brush_core::{ErrorKind, builtins, env, error, variables};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
/// Exit code returned when `read` times out.
|
||||
/// This is 128 + SIGALRM (14) = 142, matching bash behavior.
|
||||
const TIMEOUT_EXIT_CODE: u8 = 142;
|
||||
|
||||
/// ASCII control character for Ctrl+C (ETX - End of Text).
|
||||
const CTRL_C: char = '\x03';
|
||||
/// ASCII control character for Ctrl+D (EOT - End of Transmission).
|
||||
const CTRL_D: char = '\x04';
|
||||
/// Backslash character used for escape processing.
|
||||
const BACKSLASH: char = '\\';
|
||||
/// Default line delimiter (newline).
|
||||
const DEFAULT_DELIMITER: char = '\n';
|
||||
/// NUL character used as delimiter when `-d ''` is specified.
|
||||
const NUL_DELIMITER: char = '\0';
|
||||
|
||||
/// Parse standard input.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ReadCommand {
|
||||
/// Optionally, name of an array variable to receive read words
|
||||
/// of input.
|
||||
#[clap(short = 'a', value_name = "VAR_NAME")]
|
||||
array_variable: Option<String>,
|
||||
|
||||
/// Optionally, a delimiter to use other than a newline character.
|
||||
#[clap(short = 'd')]
|
||||
delimiter: Option<String>,
|
||||
|
||||
/// Use readline-like input.
|
||||
#[clap(short = 'e')]
|
||||
use_readline: bool,
|
||||
|
||||
/// Provide text to use as initial input for readline.
|
||||
#[clap(short = 'i', value_name = "STR")]
|
||||
initial_text: Option<String>,
|
||||
|
||||
/// Read only the first N characters or until a specified
|
||||
/// delimiter is reached, whichever happens first.
|
||||
#[clap(short = 'n', value_name = "COUNT")]
|
||||
return_after_n_chars: Option<usize>,
|
||||
|
||||
/// Read exactly N characters, ignoring any specified delimiter.
|
||||
#[clap(short = 'N', value_name = "COUNT")]
|
||||
return_after_n_chars_no_delimiter: Option<usize>,
|
||||
|
||||
/// Prompt to display before reading.
|
||||
#[clap(short = 'p')]
|
||||
prompt: Option<String>,
|
||||
|
||||
/// Read input in raw mode; no escape sequences.
|
||||
#[clap(short = 'r')]
|
||||
raw_mode: bool,
|
||||
|
||||
/// Do not echo input.
|
||||
#[clap(short = 's')]
|
||||
silent: bool,
|
||||
|
||||
/// Specify timeout in seconds; fail if the timeout elapses before
|
||||
/// input is completed.
|
||||
#[clap(short = 't', value_name = "SECONDS", allow_hyphen_values = true)]
|
||||
timeout_in_seconds: Option<f64>,
|
||||
|
||||
/// File descriptor to read from instead of stdin.
|
||||
#[clap(short = 'u', name = "FD")]
|
||||
fd_num_to_read: Option<u8>,
|
||||
|
||||
/// Optionally, names of variables to receive read input.
|
||||
variable_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ReadCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.use_readline {
|
||||
return error::unimp("read -e");
|
||||
}
|
||||
if self.initial_text.is_some() {
|
||||
return error::unimp("read -i");
|
||||
}
|
||||
|
||||
// Validate timeout value if provided.
|
||||
if let Some(result) = self.validate_timeout(&context)? {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// Find the input stream to use.
|
||||
let fd_num = self.fd_num_to_read.map_or(
|
||||
brush_core::openfiles::OpenFiles::STDIN_FD,
|
||||
brush_core::ShellFd::from,
|
||||
);
|
||||
|
||||
// Retrieve the file.
|
||||
let input_stream = context
|
||||
.try_fd(fd_num)
|
||||
.ok_or_else(|| ErrorKind::BadFileDescriptor(fd_num))?;
|
||||
|
||||
// Retrieve effective value of IFS for splitting.
|
||||
// We convert to owned String to release the borrow before the mutable borrow
|
||||
// needed for variable assignment.
|
||||
let ifs = context.shell.ifs().into_owned();
|
||||
|
||||
// Convert timeout to Duration.
|
||||
let timeout = self.timeout_in_seconds.map(Duration::from_secs_f64);
|
||||
|
||||
// Perform the read operation (potentially with timeout).
|
||||
let read_result = self.read_line(input_stream, context.stderr(), timeout)?;
|
||||
|
||||
// Determine whether to skip IFS splitting (for -N option).
|
||||
let skip_ifs_splitting = self.return_after_n_chars_no_delimiter.is_some();
|
||||
|
||||
// Extract the input line and determine exit code based on result.
|
||||
let (input_line, result) = match &read_result {
|
||||
ReadResult::Line(line) => (Some(line.clone()), brush_core::ExecutionResult::success()),
|
||||
ReadResult::Eof(Some(line)) => (
|
||||
Some(line.clone()),
|
||||
brush_core::ExecutionResult::general_error(),
|
||||
),
|
||||
ReadResult::Eof(None) | ReadResult::Interrupted | ReadResult::InputNotReady => {
|
||||
(None, brush_core::ExecutionResult::general_error())
|
||||
}
|
||||
ReadResult::TimedOut(partial) => (
|
||||
partial.clone(),
|
||||
brush_core::ExecutionResult::new(TIMEOUT_EXIT_CODE),
|
||||
),
|
||||
ReadResult::InputReady => (None, brush_core::ExecutionResult::success()),
|
||||
};
|
||||
|
||||
// Assign input to variables based on options.
|
||||
assign_input_to_variables(
|
||||
context.shell,
|
||||
input_line.as_deref(),
|
||||
&ifs,
|
||||
skip_ifs_splitting,
|
||||
self.array_variable.as_deref(),
|
||||
&self.variable_names,
|
||||
)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Assigns read input to shell variables based on the specified options.
|
||||
///
|
||||
/// This handles three modes:
|
||||
/// - Array mode (`-a`): Split input by IFS and assign to array elements
|
||||
/// - Named variables: Split input by IFS and assign to each variable, with remainder to last
|
||||
/// - Default (`REPLY`): Assign entire input line to the `REPLY` variable
|
||||
fn assign_input_to_variables(
|
||||
shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
input_line: Option<&str>,
|
||||
ifs: &str,
|
||||
skip_ifs_splitting: bool,
|
||||
array_variable: Option<&str>,
|
||||
variable_names: &[String],
|
||||
) -> Result<(), brush_core::Error> {
|
||||
if let Some(array_variable) = array_variable {
|
||||
let literal_fields = build_array_fields(input_line, ifs, skip_ifs_splitting);
|
||||
shell.env_mut().update_or_add(
|
||||
array_variable,
|
||||
variables::ShellValueLiteral::Array(variables::ArrayLiteral(literal_fields)),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
} else if !variable_names.is_empty() {
|
||||
assign_to_named_variables(shell, input_line, ifs, skip_ifs_splitting, variable_names)?;
|
||||
} else {
|
||||
shell.env_mut().update_or_add(
|
||||
"REPLY",
|
||||
variables::ShellValueLiteral::Scalar(input_line.unwrap_or_default().to_owned()),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assigns split fields to named variables.
|
||||
///
|
||||
/// Fields are assigned one per variable, with any remaining fields joined by space
|
||||
/// and assigned to the last variable. If there are more variables than fields,
|
||||
/// the extra variables are set to empty strings.
|
||||
fn assign_to_named_variables(
|
||||
shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
input_line: Option<&str>,
|
||||
ifs: &str,
|
||||
skip_ifs_splitting: bool,
|
||||
variable_names: &[String],
|
||||
) -> Result<(), brush_core::Error> {
|
||||
let mut fields =
|
||||
build_variable_fields(input_line, ifs, skip_ifs_splitting, variable_names.len());
|
||||
|
||||
for (i, name) in variable_names.iter().enumerate() {
|
||||
let is_last = i == variable_names.len() - 1;
|
||||
|
||||
let value = if fields.is_empty() {
|
||||
String::new()
|
||||
} else if is_last {
|
||||
// Last variable gets all remaining fields joined by space.
|
||||
std::mem::take(&mut fields).into_iter().join(" ")
|
||||
} else {
|
||||
fields.pop_front().unwrap_or_default()
|
||||
};
|
||||
|
||||
shell.env_mut().update_or_add(
|
||||
name,
|
||||
variables::ShellValueLiteral::Scalar(value),
|
||||
|_| Ok(()),
|
||||
env::EnvironmentLookup::Anywhere,
|
||||
env::EnvironmentScope::Global,
|
||||
)?;
|
||||
|
||||
if is_last {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds array field values from input, optionally splitting by IFS.
|
||||
fn build_array_fields(
|
||||
input_line: Option<&str>,
|
||||
ifs: &str,
|
||||
skip_ifs_splitting: bool,
|
||||
) -> Vec<(Option<String>, String)> {
|
||||
match input_line {
|
||||
Some(line) if skip_ifs_splitting => {
|
||||
// With -N, don't split - put entire input as single element.
|
||||
vec![(None, line.to_string())]
|
||||
}
|
||||
Some(line) => {
|
||||
let fields: VecDeque<_> = split_line_by_ifs(ifs, line, None /* max_fields */);
|
||||
fields.into_iter().map(|f| (None, f)).collect()
|
||||
}
|
||||
None => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds field values from input for assignment to named variables.
|
||||
fn build_variable_fields(
|
||||
input_line: Option<&str>,
|
||||
ifs: &str,
|
||||
skip_ifs_splitting: bool,
|
||||
num_variables: usize,
|
||||
) -> VecDeque<String> {
|
||||
match input_line {
|
||||
Some(line) if skip_ifs_splitting => {
|
||||
// With -N, don't split - put entire input in first variable.
|
||||
let mut fields = VecDeque::new();
|
||||
fields.push_back(line.to_string());
|
||||
fields
|
||||
}
|
||||
Some(line) => split_line_by_ifs(ifs, line, Some(num_variables)),
|
||||
None => VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of a `read` operation.
|
||||
///
|
||||
/// This enum clearly represents all possible outcomes of `read_line()`,
|
||||
/// making the contract with callers explicit.
|
||||
enum ReadResult {
|
||||
/// Successfully read a complete line (delimiter or char limit reached).
|
||||
Line(String),
|
||||
/// Reached end of input. Contains any partial content read before EOF.
|
||||
Eof(Option<String>),
|
||||
/// Input was interrupted (e.g., Ctrl+C). No content is returned.
|
||||
Interrupted,
|
||||
/// The operation timed out. Contains any partial content read before timeout.
|
||||
TimedOut(Option<String>),
|
||||
/// For `-t 0`: input is immediately available (exit 0).
|
||||
InputReady,
|
||||
/// For `-t 0`: no input immediately available (exit 1).
|
||||
InputNotReady,
|
||||
}
|
||||
|
||||
/// Helper struct that encapsulates the state for reading input character by character.
|
||||
///
|
||||
/// This separates the concerns of character-level I/O with timeout handling from the
|
||||
/// higher-level logic of line building and escape processing.
|
||||
struct InputReader {
|
||||
/// The input source.
|
||||
input: brush_core::openfiles::OpenFile,
|
||||
/// Optional deadline for timeout.
|
||||
deadline: Option<Instant>,
|
||||
/// Single-byte read buffer.
|
||||
///
|
||||
/// TODO(utf-8): This only handles ASCII correctly. Multi-byte UTF-8 characters
|
||||
/// will be read as separate bytes and incorrectly interpreted. To fix this,
|
||||
/// we would need to buffer up to 4 bytes and decode incrementally using
|
||||
/// `std::str::from_utf8`. Note that bash's `-n` counts bytes, not Unicode
|
||||
/// codepoints, so the fix needs to preserve that behavior.
|
||||
buffer: [u8; 1],
|
||||
/// Terminal mode guard - kept alive for RAII cleanup on drop.
|
||||
/// The guard restores original terminal settings when dropped, even though
|
||||
/// we don't access the field directly after construction.
|
||||
///
|
||||
/// The leading underscore suppresses the "unused field" warning while making
|
||||
/// it explicit this field exists solely for its `Drop` implementation.
|
||||
_term_mode: Option<brush_core::terminal::AutoModeGuard>,
|
||||
}
|
||||
|
||||
/// Events that can occur when reading input.
|
||||
enum InputEvent {
|
||||
/// A regular character was read.
|
||||
Char(char),
|
||||
/// End of file was reached.
|
||||
Eof,
|
||||
/// The read operation timed out.
|
||||
Timeout,
|
||||
/// Ctrl+C was pressed.
|
||||
CtrlC,
|
||||
/// Ctrl+D was pressed.
|
||||
CtrlD,
|
||||
}
|
||||
|
||||
impl InputReader {
|
||||
/// Creates a new input reader with optional timeout.
|
||||
fn new(
|
||||
input: brush_core::openfiles::OpenFile,
|
||||
timeout: Option<Duration>,
|
||||
term_mode: Option<brush_core::terminal::AutoModeGuard>,
|
||||
) -> Self {
|
||||
Self {
|
||||
input,
|
||||
deadline: timeout.map(|t| Instant::now() + t),
|
||||
buffer: [0; 1],
|
||||
_term_mode: term_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if input is immediately available (for `-t 0`). Returns `false` if an error
|
||||
/// occurs while checking for available input.
|
||||
fn check_input_available(&self) -> bool {
|
||||
brush_core::sys::poll::poll_for_input(&self.input, Duration::ZERO).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Reads the next input event, handling timeout and control characters.
|
||||
fn read_event(&mut self) -> Result<InputEvent, brush_core::Error> {
|
||||
// Check timeout before attempting read.
|
||||
if let Some(deadline) = self.deadline {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
return Ok(InputEvent::Timeout);
|
||||
}
|
||||
|
||||
// Poll for input with remaining timeout.
|
||||
match brush_core::sys::poll::poll_for_input(&self.input, remaining) {
|
||||
Ok(true) => { /* Data available, proceed. */ }
|
||||
Ok(false) => return Ok(InputEvent::Timeout),
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
let n = self.input.read(&mut self.buffer)?;
|
||||
if n == 0 {
|
||||
return Ok(InputEvent::Eof);
|
||||
}
|
||||
|
||||
let ch = self.buffer[0] as char;
|
||||
|
||||
// Map control characters to events.
|
||||
Ok(match ch {
|
||||
CTRL_C => InputEvent::CtrlC,
|
||||
CTRL_D => InputEvent::CtrlD,
|
||||
_ => InputEvent::Char(ch),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for line reading behavior.
|
||||
struct LineReaderConfig {
|
||||
/// Character that terminates input (None for -N mode).
|
||||
delimiter: Option<char>,
|
||||
/// Maximum characters to read (for -n or -N).
|
||||
char_limit: Option<usize>,
|
||||
/// Whether to process backslash escapes (false for -r mode).
|
||||
process_escapes: bool,
|
||||
}
|
||||
|
||||
/// Reads a complete line of input using the given reader and configuration.
|
||||
///
|
||||
/// Returns a `ReadResult` indicating success, EOF, timeout, or interruption.
|
||||
///
|
||||
/// Note on character counting for `-n` limit:
|
||||
/// Bash counts OUTPUT characters (after escape processing) toward the limit.
|
||||
/// For example, with `-n 3` and input `a\bc` (4 bytes):
|
||||
/// - Bash processes: 'a' (output 1), '\b' → 'b' (output 2), 'c' (output 3) → "abc"
|
||||
/// - The backslash is consumed but doesn't count toward the limit
|
||||
fn read_line_with_reader(
|
||||
reader: &mut InputReader,
|
||||
config: &LineReaderConfig,
|
||||
) -> Result<ReadResult, brush_core::Error> {
|
||||
let mut line = String::new();
|
||||
let mut pending_backslash = false;
|
||||
|
||||
loop {
|
||||
let event = reader.read_event()?;
|
||||
|
||||
match event {
|
||||
InputEvent::Eof => {
|
||||
// Bash discards pending backslash on EOF.
|
||||
return Ok(ReadResult::Eof(if line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(line)
|
||||
}));
|
||||
}
|
||||
|
||||
InputEvent::Timeout => {
|
||||
// Include pending backslash on timeout (different from EOF).
|
||||
if pending_backslash {
|
||||
line.push(BACKSLASH);
|
||||
}
|
||||
return Ok(ReadResult::TimedOut(if line.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(line)
|
||||
}));
|
||||
}
|
||||
|
||||
InputEvent::CtrlC => {
|
||||
return Ok(ReadResult::Interrupted);
|
||||
}
|
||||
|
||||
InputEvent::CtrlD => {
|
||||
// At line start = EOF, mid-input = flush current input.
|
||||
// Bash discards pending backslash here too.
|
||||
return Ok(if line.is_empty() && !pending_backslash {
|
||||
ReadResult::Eof(None)
|
||||
} else {
|
||||
ReadResult::Line(line)
|
||||
});
|
||||
}
|
||||
|
||||
InputEvent::Char(ch) => {
|
||||
// Handle backslash escape processing (when enabled).
|
||||
if config.process_escapes {
|
||||
if pending_backslash {
|
||||
pending_backslash = false;
|
||||
|
||||
// Backslash-delimiter is line continuation.
|
||||
if let Some(delim) = config.delimiter
|
||||
&& ch == delim
|
||||
{
|
||||
continue; // Line continuation.
|
||||
}
|
||||
|
||||
// For other chars, add char literally (backslash consumed).
|
||||
line.push(ch);
|
||||
|
||||
// Check character limit (based on output length).
|
||||
if let Some(limit) = config.char_limit
|
||||
&& line.len() >= limit
|
||||
{
|
||||
return Ok(ReadResult::Line(line));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == BACKSLASH {
|
||||
pending_backslash = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for delimiter.
|
||||
if let Some(delim) = config.delimiter
|
||||
&& ch == delim
|
||||
{
|
||||
return Ok(ReadResult::Line(line));
|
||||
}
|
||||
|
||||
// Ignore non-whitespace control characters.
|
||||
if ch.is_ascii_control() && !ch.is_ascii_whitespace() {
|
||||
continue;
|
||||
}
|
||||
|
||||
line.push(ch);
|
||||
|
||||
// Check character limit (based on output length).
|
||||
if let Some(limit) = config.char_limit
|
||||
&& line.len() >= limit
|
||||
{
|
||||
return Ok(ReadResult::Line(line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadCommand {
|
||||
/// Reads a line of input, optionally with a timeout.
|
||||
///
|
||||
/// Handles backslash escape processing:
|
||||
/// - Without `-r`: backslash-newline is line continuation, other backslashes escape the next
|
||||
/// char
|
||||
/// - With `-r`: backslash is treated as a literal character
|
||||
fn read_line(
|
||||
&self,
|
||||
input_file: brush_core::openfiles::OpenFile,
|
||||
mut stderr_file: impl std::io::Write,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ReadResult, brush_core::Error> {
|
||||
let term_mode = self.setup_terminal_settings(&input_file)?;
|
||||
|
||||
// Display prompt on stderr, but only if input is from a terminal (per bash behavior).
|
||||
if let Some(prompt) = &self.prompt {
|
||||
if input_file.is_terminal() {
|
||||
write!(stderr_file, "{prompt}")?;
|
||||
stderr_file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine delimiter based on options.
|
||||
let delimiter = if self.return_after_n_chars_no_delimiter.is_some() {
|
||||
None
|
||||
} else if let Some(delimiter_str) = &self.delimiter {
|
||||
if delimiter_str.is_empty() {
|
||||
Some(NUL_DELIMITER)
|
||||
} else {
|
||||
delimiter_str.chars().next()
|
||||
}
|
||||
} else {
|
||||
Some(DEFAULT_DELIMITER)
|
||||
};
|
||||
|
||||
let char_limit = self
|
||||
.return_after_n_chars_no_delimiter
|
||||
.or(self.return_after_n_chars);
|
||||
|
||||
// Create the input reader.
|
||||
let mut reader = InputReader::new(input_file, timeout, term_mode);
|
||||
|
||||
// Handle -t 0 special case: just check if input is available without reading.
|
||||
if timeout == Some(Duration::ZERO) {
|
||||
return Ok(if reader.check_input_available() {
|
||||
ReadResult::InputReady
|
||||
} else {
|
||||
ReadResult::InputNotReady
|
||||
});
|
||||
}
|
||||
|
||||
// Configure and perform the read.
|
||||
let config = LineReaderConfig {
|
||||
delimiter,
|
||||
char_limit,
|
||||
process_escapes: !self.raw_mode,
|
||||
};
|
||||
|
||||
read_line_with_reader(&mut reader, &config)
|
||||
}
|
||||
|
||||
fn setup_terminal_settings(
|
||||
&self,
|
||||
file: &brush_core::openfiles::OpenFile,
|
||||
) -> Result<Option<brush_core::terminal::AutoModeGuard>, brush_core::Error> {
|
||||
let mode = brush_core::terminal::AutoModeGuard::new(file.to_owned()).ok();
|
||||
if let Some(mode) = &mode {
|
||||
let config = brush_core::terminal::Settings::builder()
|
||||
.line_input(false)
|
||||
.interrupt_signals(false)
|
||||
.echo_input(!self.silent)
|
||||
.build();
|
||||
|
||||
mode.apply_settings(&config)?;
|
||||
}
|
||||
|
||||
Ok(mode)
|
||||
}
|
||||
|
||||
/// Validates the timeout value and returns an error result if invalid.
|
||||
///
|
||||
/// Returns `Ok(Some(result))` if the timeout is invalid (caller should return early),
|
||||
/// `Ok(None)` if the timeout is valid or not specified.
|
||||
///
|
||||
/// TODO(read): Bash uses $TMOUT as a default timeout for `read` when -t is not specified.
|
||||
fn validate_timeout(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<Option<brush_core::ExecutionResult>, brush_core::Error> {
|
||||
if let Some(timeout) = self.timeout_in_seconds {
|
||||
if timeout < 0.0 {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: -t: invalid timeout specification",
|
||||
context.command_name
|
||||
)?;
|
||||
return Ok(Some(brush_core::ExecutionResult::general_error()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits a line by IFS (Internal Field Separator) according to shell rules.
|
||||
///
|
||||
/// Shell IFS splitting has special rules:
|
||||
/// - Whitespace IFS chars (space, tab, newline) are "IFS whitespace"
|
||||
/// - Leading/trailing IFS whitespace is trimmed from the input
|
||||
/// - Consecutive IFS whitespace chars act as a single delimiter
|
||||
/// - Non-whitespace IFS chars each act as individual delimiters
|
||||
/// - Trailing non-whitespace delimiter does NOT create an empty final field
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ifs` - The IFS string (typically " \t\n")
|
||||
/// * `line` - The input line to split
|
||||
/// * `max_fields` - Optional limit on number of fields (for `read var1 var2`)
|
||||
fn split_line_by_ifs(ifs: &str, line: &str, max_fields: Option<usize>) -> VecDeque<String> {
|
||||
let ifs_chars: Vec<char> = ifs.chars().collect();
|
||||
|
||||
// Helper to check if a char is IFS whitespace (space, tab, or newline AND in IFS).
|
||||
let is_ifs_whitespace =
|
||||
|c: char| -> bool { (c == ' ' || c == '\t' || c == '\n') && ifs_chars.contains(&c) };
|
||||
|
||||
// Trim leading/trailing IFS whitespace from the input.
|
||||
let trimmed_line = line.trim_matches(&is_ifs_whitespace);
|
||||
if trimmed_line.is_empty() {
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
let max_fields = max_fields.unwrap_or(usize::MAX);
|
||||
|
||||
// State machine for splitting:
|
||||
// - `consuming_whitespace_run`: Currently skipping consecutive IFS whitespace
|
||||
// - `prev_was_non_ws_delim`: Previous char was a non-whitespace delimiter
|
||||
// - `collecting_remainder`: We've hit max_fields, collect everything into last field
|
||||
let mut fields = VecDeque::new();
|
||||
let mut current_field = String::new();
|
||||
let mut consuming_whitespace_run = false;
|
||||
let mut prev_was_non_ws_delim = false;
|
||||
let mut collecting_remainder = false;
|
||||
|
||||
for c in trimmed_line.chars() {
|
||||
// Skip consecutive IFS whitespace (they act as single delimiter).
|
||||
if consuming_whitespace_run && is_ifs_whitespace(c) {
|
||||
continue;
|
||||
}
|
||||
consuming_whitespace_run = false;
|
||||
|
||||
let is_delimiter = ifs_chars.contains(&c);
|
||||
let at_field_limit = fields.len() + 1 >= max_fields;
|
||||
|
||||
if !at_field_limit && is_delimiter {
|
||||
// Normal case: delimiter ends current field, start new one.
|
||||
fields.push_back(std::mem::take(&mut current_field));
|
||||
consuming_whitespace_run = is_ifs_whitespace(c);
|
||||
prev_was_non_ws_delim = !consuming_whitespace_run;
|
||||
} else if at_field_limit && !collecting_remainder && is_delimiter {
|
||||
// At field limit but haven't started last field content yet.
|
||||
// Skip leading IFS whitespace for the final field.
|
||||
if is_ifs_whitespace(c) {
|
||||
consuming_whitespace_run = true;
|
||||
} else {
|
||||
// Non-whitespace delimiters at boundary: include in remainder.
|
||||
// e.g., "x::y" with IFS=":" and 2 vars gives ["x", ":y"]
|
||||
collecting_remainder = true;
|
||||
current_field.push(c);
|
||||
}
|
||||
} else {
|
||||
// Regular character: add to current field.
|
||||
collecting_remainder = at_field_limit;
|
||||
current_field.push(c);
|
||||
prev_was_non_ws_delim = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize: push last field unless it's empty AND we ended with non-ws delimiter.
|
||||
// e.g., "a,b,c," with IFS="," gives ["a", "b", "c"], not ["a", "b", "c", ""].
|
||||
if !current_field.is_empty() || !prev_was_non_ws_delim {
|
||||
fields.push_back(current_field);
|
||||
}
|
||||
|
||||
fields
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::assert_equal;
|
||||
|
||||
use super::*;
|
||||
|
||||
// ==================== split_line_by_ifs tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_basic() {
|
||||
let result = split_line_by_ifs(",", "a,b,c", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_leading_or_trailing_space() {
|
||||
let result = split_line_by_ifs(" ", " a b c ", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_extra_interior_space() {
|
||||
let result = split_line_by_ifs(" ", "a b c", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_leading_non_space_delimiter() {
|
||||
let result = split_line_by_ifs(",", ",a,b,c", None);
|
||||
assert_equal(result, VecDeque::from(vec!["", "a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_trailing_non_space_delimiter() {
|
||||
// Bash does NOT include empty trailing field when input ends with non-ws delimiter.
|
||||
let result = split_line_by_ifs(",", "a,b,c,", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_max_fields() {
|
||||
// With max_fields=2, remainder goes into second field.
|
||||
let result = split_line_by_ifs(" ", "a b c d", Some(2));
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b c d"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_max_fields_with_non_ws_delimiter() {
|
||||
// With max_fields and non-whitespace delimiter.
|
||||
let result = split_line_by_ifs(",", "a,b,c,d", Some(2));
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b,c,d"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_consecutive_delimiters_at_boundary() {
|
||||
// Consecutive non-whitespace delimiters at field boundary should be preserved.
|
||||
// e.g., "x::y" with IFS=":" and 2 vars gives ["x", ":y"]
|
||||
let result = split_line_by_ifs(":", "x::y", Some(2));
|
||||
assert_equal(result, VecDeque::from(vec!["x", ":y"]));
|
||||
|
||||
// Triple delimiter at boundary.
|
||||
let result = split_line_by_ifs(":", "x:::y", Some(2));
|
||||
assert_equal(result, VecDeque::from(vec!["x", "::y"]));
|
||||
|
||||
// Delimiter in middle of remainder is also preserved.
|
||||
let result = split_line_by_ifs(":", "x:y:z:w", Some(2));
|
||||
assert_equal(result, VecDeque::from(vec!["x", "y:z:w"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_mixed_delimiters() {
|
||||
// Mixed whitespace and non-whitespace in IFS.
|
||||
let result = split_line_by_ifs(": ", "a:b c:d", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c", "d"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_empty_input() {
|
||||
let result = split_line_by_ifs(" ", "", None);
|
||||
assert_equal(result, VecDeque::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_whitespace_only() {
|
||||
let result = split_line_by_ifs(" ", " ", None);
|
||||
assert_equal(result, VecDeque::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_line_by_ifs_consecutive_non_ws_delimiters() {
|
||||
// Consecutive non-whitespace delimiters create empty fields.
|
||||
let result = split_line_by_ifs(",", "a,,b", None);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "", "b"]));
|
||||
}
|
||||
|
||||
// ==================== build_array_fields tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_build_array_fields_basic() {
|
||||
let result = build_array_fields(Some("a b c"), " ", false);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
(None, "a".to_string()),
|
||||
(None, "b".to_string()),
|
||||
(None, "c".to_string())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_array_fields_skip_splitting() {
|
||||
// With -N option, entire input goes as single element.
|
||||
let result = build_array_fields(Some("a b c"), " ", true);
|
||||
assert_eq!(result, vec![(None, "a b c".to_string())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_array_fields_none_input() {
|
||||
let result = build_array_fields(None, " ", false);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
// ==================== build_variable_fields tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_build_variable_fields_basic() {
|
||||
let result = build_variable_fields(Some("a b c"), " ", false, 3);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b", "c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_variable_fields_fewer_vars_than_fields() {
|
||||
// Last variable gets remainder.
|
||||
let result = build_variable_fields(Some("a b c d"), " ", false, 2);
|
||||
assert_equal(result, VecDeque::from(vec!["a", "b c d"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_variable_fields_skip_splitting() {
|
||||
// With -N option, entire input goes to first variable.
|
||||
let result = build_variable_fields(Some("a b c"), " ", true, 3);
|
||||
assert_equal(result, VecDeque::from(vec!["a b c"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_variable_fields_none_input() {
|
||||
let result = build_variable_fields(None, " ", false, 3);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionControlFlow, ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Return from the current function.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ReturnCommand {
|
||||
/// The exit code to return.
|
||||
code: Option<i32>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ReturnCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
let code_8bit = if let Some(code_32bit) = &self.code {
|
||||
(code_32bit & 0xFF) as u8
|
||||
} else {
|
||||
context.shell.last_exit_status()
|
||||
};
|
||||
|
||||
if context.shell.in_function() || context.shell.in_sourced_script() {
|
||||
let mut result = ExecutionResult::new(code_8bit);
|
||||
result.next_control_flow = ExecutionControlFlow::ReturnFromFunctionOrScript;
|
||||
|
||||
Ok(result)
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
context.stderr(),
|
||||
"return: can only be used in a function or sourced script"
|
||||
);
|
||||
Ok(ExecutionExitCode::InvalidUsage.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins, variables};
|
||||
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
ExportVariablesOnModification,
|
||||
'a',
|
||||
"Export variables on modification"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
NotifyJobTerminationImmediately,
|
||||
'b',
|
||||
"Notify job termination immediately"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
ExitOnNonzeroCommandExit,
|
||||
'e',
|
||||
"Exit on nonzero command exit"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(DisableFilenameGlobbing, 'f', "Disable filename globbing");
|
||||
crate::minus_or_plus_flag_arg!(RememberCommandLocations, 'h', "Remember command locations");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
PlaceAllAssignmentArgsInCommandEnv,
|
||||
'k',
|
||||
"Place all assignment args in command environment"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(EnableJobControl, 'm', "Enable job control");
|
||||
crate::minus_or_plus_flag_arg!(DoNotExecuteCommands, 'n', "Do not execute commands");
|
||||
crate::minus_or_plus_flag_arg!(RealEffectiveUidMismatch, 'p', "Real effective UID mismatch");
|
||||
crate::minus_or_plus_flag_arg!(ExitAfterOneCommand, 't', "Exit after one command");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
TreatUnsetVariablesAsError,
|
||||
'u',
|
||||
"Treat unset variables as error"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(PrintShellInputLines, 'v', "Print shell input lines");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
PrintCommandsAndArguments,
|
||||
'x',
|
||||
"Print commands and arguments"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(PerformBraceExpansion, 'B', "Perform brace expansion");
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
DisallowOverwritingRegularFilesViaOutputRedirection,
|
||||
'C',
|
||||
"Disallow overwriting regular files via output redirection"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
ShellFunctionsInheritErrTrap,
|
||||
'E',
|
||||
"Shell functions inherit ERR trap"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
EnableBangStyleHistorySubstitution,
|
||||
'H',
|
||||
"Enable bang style history substitution"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
DoNotResolveSymlinksWhenChangingDir,
|
||||
'P',
|
||||
"Do not resolve symlinks when changing dir"
|
||||
);
|
||||
crate::minus_or_plus_flag_arg!(
|
||||
ShellFunctionsInheritDebugAndReturnTraps,
|
||||
'T',
|
||||
"Shell functions inherit DEBUG and RETURN traps"
|
||||
);
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub(crate) struct SetOption {
|
||||
#[arg(short = 'o', name = "setopt_enable", num_args=0..=1, value_name = "OPT")]
|
||||
enable: Option<Vec<String>>,
|
||||
#[arg(long = concat!("+o"), name = "setopt_disable", hide = true, num_args=0..=1)]
|
||||
disable: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Manage set-based shell options.
|
||||
#[derive(Parser)]
|
||||
#[clap(disable_help_flag = true)]
|
||||
pub(crate) struct SetCommand {
|
||||
/// Display help for this command.
|
||||
#[clap(long, action = clap::ArgAction::HelpLong)]
|
||||
help: Option<bool>,
|
||||
|
||||
#[clap(flatten)]
|
||||
export_variables_on_modification: ExportVariablesOnModification,
|
||||
#[clap(flatten)]
|
||||
notify_job_termination_immediately: NotifyJobTerminationImmediately,
|
||||
#[clap(flatten)]
|
||||
exit_on_nonzero_command_exit: ExitOnNonzeroCommandExit,
|
||||
#[clap(flatten)]
|
||||
disable_filename_globbing: DisableFilenameGlobbing,
|
||||
#[clap(flatten)]
|
||||
remember_command_locations: RememberCommandLocations,
|
||||
#[clap(flatten)]
|
||||
place_all_assignment_args_in_command_env: PlaceAllAssignmentArgsInCommandEnv,
|
||||
#[clap(flatten)]
|
||||
enable_job_control: EnableJobControl,
|
||||
#[clap(flatten)]
|
||||
do_not_execute_commands: DoNotExecuteCommands,
|
||||
#[clap(flatten)]
|
||||
real_effective_uid_mismatch: RealEffectiveUidMismatch,
|
||||
#[clap(flatten)]
|
||||
exit_after_one_command: ExitAfterOneCommand,
|
||||
#[clap(flatten)]
|
||||
treat_unset_variables_as_error: TreatUnsetVariablesAsError,
|
||||
#[clap(flatten)]
|
||||
print_shell_input_lines: PrintShellInputLines,
|
||||
#[clap(flatten)]
|
||||
print_commands_and_arguments: PrintCommandsAndArguments,
|
||||
#[clap(flatten)]
|
||||
perform_brace_expansion: PerformBraceExpansion,
|
||||
#[clap(flatten)]
|
||||
disallow_overwriting_regular_files_via_output_redirection:
|
||||
DisallowOverwritingRegularFilesViaOutputRedirection,
|
||||
#[clap(flatten)]
|
||||
shell_functions_inherit_err_trap: ShellFunctionsInheritErrTrap,
|
||||
#[clap(flatten)]
|
||||
enable_bang_style_history_substitution: EnableBangStyleHistorySubstitution,
|
||||
#[clap(flatten)]
|
||||
do_not_resolve_symlinks_when_changing_dir: DoNotResolveSymlinksWhenChangingDir,
|
||||
#[clap(flatten)]
|
||||
shell_functions_inherit_debug_and_return_traps: ShellFunctionsInheritDebugAndReturnTraps,
|
||||
|
||||
#[clap(flatten)]
|
||||
set_option: SetOption,
|
||||
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
positional_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for SetCommand {
|
||||
fn takes_plus_options() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Override the default [`builtins::Command::new`] function to handle clap's limitation related
|
||||
/// to `--`. See [`builtins::parse_known`] for more information
|
||||
/// TODO(set): we can safely remove this after the issue is resolved
|
||||
fn new<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
//
|
||||
// TODO(set): This is getting pretty messy; we need to see how to avoid this -- handling
|
||||
// from leaking into too many commands' custom parsing.
|
||||
//
|
||||
|
||||
// Apply the same workaround from the default implementation of Command::new to handle '+'
|
||||
// args.
|
||||
let mut updated_args = vec![];
|
||||
let mut now_parsing_positional_args = false;
|
||||
let mut next_arg_is_option_value = false;
|
||||
for (i, arg) in args.into_iter().enumerate() {
|
||||
if now_parsing_positional_args || next_arg_is_option_value {
|
||||
updated_args.push(arg);
|
||||
|
||||
next_arg_is_option_value = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if arg == "-" || arg == "--" || (i > 0 && !arg.starts_with(['-', '+'])) {
|
||||
now_parsing_positional_args = true;
|
||||
}
|
||||
|
||||
if let Some(plus_options) = arg.strip_prefix("+") {
|
||||
next_arg_is_option_value = plus_options.ends_with('o');
|
||||
for c in plus_options.chars() {
|
||||
updated_args.push(format!("--+{c}"));
|
||||
}
|
||||
} else {
|
||||
next_arg_is_option_value = arg.starts_with('-') && arg.ends_with('o');
|
||||
updated_args.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(updated_args)?;
|
||||
if let Some(args) = rest_args {
|
||||
this.positional_args.extend(args);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
type Error = brush_core::Error;
|
||||
|
||||
#[expect(clippy::too_many_lines)]
|
||||
#[allow(clippy::useless_let_if_seq)]
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
let mut saw_option = false;
|
||||
|
||||
if let Some(value) = self.print_commands_and_arguments.to_bool() {
|
||||
context.shell.options_mut().print_commands_and_arguments = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.export_variables_on_modification.to_bool() {
|
||||
context.shell.options_mut().export_variables_on_modification = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.notify_job_termination_immediately.to_bool() {
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.notify_job_termination_immediately = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.exit_on_nonzero_command_exit.to_bool() {
|
||||
context.shell.options_mut().exit_on_nonzero_command_exit = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.disable_filename_globbing.to_bool() {
|
||||
context.shell.options_mut().disable_filename_globbing = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.remember_command_locations.to_bool() {
|
||||
context.shell.options_mut().remember_command_locations = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.place_all_assignment_args_in_command_env.to_bool() {
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.place_all_assignment_args_in_command_env = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.enable_job_control.to_bool() {
|
||||
context.shell.options_mut().enable_job_control = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.do_not_execute_commands.to_bool() {
|
||||
context.shell.options_mut().do_not_execute_commands = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.real_effective_uid_mismatch.to_bool() {
|
||||
context.shell.options_mut().real_effective_uid_mismatch = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.exit_after_one_command.to_bool() {
|
||||
context.shell.options_mut().exit_after_one_command = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.treat_unset_variables_as_error.to_bool() {
|
||||
context.shell.options_mut().treat_unset_variables_as_error = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.print_shell_input_lines.to_bool() {
|
||||
context.shell.options_mut().print_shell_input_lines = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.print_commands_and_arguments.to_bool() {
|
||||
context.shell.options_mut().print_commands_and_arguments = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.perform_brace_expansion.to_bool() {
|
||||
context.shell.options_mut().perform_brace_expansion = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self
|
||||
.disallow_overwriting_regular_files_via_output_redirection
|
||||
.to_bool()
|
||||
{
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.disallow_overwriting_regular_files_via_output_redirection = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.shell_functions_inherit_err_trap.to_bool() {
|
||||
context.shell.options_mut().shell_functions_inherit_err_trap = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.enable_bang_style_history_substitution.to_bool() {
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.enable_bang_style_history_substitution = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self.do_not_resolve_symlinks_when_changing_dir.to_bool() {
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.do_not_resolve_symlinks_when_changing_dir = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
if let Some(value) = self
|
||||
.shell_functions_inherit_debug_and_return_traps
|
||||
.to_bool()
|
||||
{
|
||||
context
|
||||
.shell
|
||||
.options_mut()
|
||||
.shell_functions_inherit_debug_and_return_traps = value;
|
||||
saw_option = true;
|
||||
}
|
||||
|
||||
let mut named_options: HashMap<String, bool> = HashMap::new();
|
||||
if let Some(option_names) = &self.set_option.disable {
|
||||
saw_option = true;
|
||||
if option_names.is_empty() {
|
||||
for option in brush_core::namedoptions::options(
|
||||
brush_core::namedoptions::ShellOptionKind::SetO,
|
||||
)
|
||||
.iter()
|
||||
.sorted_by_key(|option| option.name)
|
||||
{
|
||||
let option_value = option.definition.get(context.shell.options());
|
||||
let option_value_str = if option_value { "-o" } else { "+o" };
|
||||
writeln!(context.stdout(), "set {option_value_str} {}", option.name)?;
|
||||
}
|
||||
} else {
|
||||
for option_name in option_names {
|
||||
named_options.insert(option_name.to_owned(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(option_names) = &self.set_option.enable {
|
||||
saw_option = true;
|
||||
if option_names.is_empty() {
|
||||
for option in brush_core::namedoptions::options(
|
||||
brush_core::namedoptions::ShellOptionKind::SetO,
|
||||
)
|
||||
.iter()
|
||||
.sorted_by_key(|option| option.name)
|
||||
{
|
||||
let option_value = option.definition.get(context.shell.options());
|
||||
let option_value_str = if option_value { "on" } else { "off" };
|
||||
writeln!(context.stdout(), "{:15}\t{option_value_str}", option.name)?;
|
||||
}
|
||||
} else {
|
||||
for option_name in option_names {
|
||||
named_options.insert(option_name.to_owned(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (option_name, value) in named_options {
|
||||
if let Some(option_def) =
|
||||
brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::SetO)
|
||||
.get(option_name.as_str())
|
||||
{
|
||||
option_def.set(context.shell.options_mut(), value);
|
||||
} else {
|
||||
result = ExecutionExitCode::InvalidUsage.into();
|
||||
}
|
||||
}
|
||||
|
||||
let args = context.shell.current_shell_args_mut();
|
||||
|
||||
let skip = match self.positional_args.first() {
|
||||
Some(x) if x == "-" => {
|
||||
if self.positional_args.len() > 1 {
|
||||
args.clear();
|
||||
}
|
||||
1
|
||||
}
|
||||
Some(x) if x == "--" => {
|
||||
args.clear();
|
||||
1
|
||||
}
|
||||
Some(_) => {
|
||||
args.clear();
|
||||
0
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
for arg in self.positional_args.iter().skip(skip) {
|
||||
args.push(arg.to_owned());
|
||||
}
|
||||
|
||||
saw_option = saw_option || !self.positional_args.is_empty();
|
||||
|
||||
// If we *still* haven't seen any options, then we need to display all variables and
|
||||
// functions.
|
||||
if !saw_option {
|
||||
display_all(&context)?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
fn display_all(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
// Display variables.
|
||||
for (name, var) in context.shell.env().iter().sorted_by_key(|v| v.0) {
|
||||
if !var.is_enumerable() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO(set): For now, skip all dynamic variables. The current behavior
|
||||
// of bash is not quite clear. We've empirically found that some
|
||||
// special variables don't get displayed until they're observed
|
||||
// at least once.
|
||||
if matches!(var.value(), variables::ShellValue::Dynamic { .. }) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip variables that have been declared but are unset.
|
||||
if !var.value().is_set() {
|
||||
continue;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{name}={}",
|
||||
var.value()
|
||||
.format(variables::FormatStyle::Basic, context.shell)?,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Display functions... unless we're in posix compliance mode.
|
||||
if !context.shell.options().posix_mode {
|
||||
for (_name, registration) in context.shell.funcs().iter().sorted_by_key(|v| v.0) {
|
||||
writeln!(context.stdout(), "{}", registration.definition())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Shift positional arguments.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ShiftCommand {
|
||||
/// Number of positions to shift the arguments by (defaults to 1).
|
||||
n: Option<i32>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ShiftCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let n = self.n.unwrap_or(1);
|
||||
|
||||
if n < 0 {
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_sign_loss)]
|
||||
let n = n as usize;
|
||||
|
||||
let args = context.shell.current_shell_args_mut();
|
||||
|
||||
if n > args.len() {
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
args.drain(0..n);
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Manage shopt-style options.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct ShoptCommand {
|
||||
/// Manage set -o options.
|
||||
#[arg(short = 'o')]
|
||||
set_o_names_only: bool,
|
||||
|
||||
/// Print options' current values.
|
||||
#[arg(short = 'p')]
|
||||
print: bool,
|
||||
|
||||
/// Suppress typical output.
|
||||
#[arg(short = 'q')]
|
||||
quiet: bool,
|
||||
|
||||
/// Set the specified options.
|
||||
#[arg(short = 's')]
|
||||
set: bool,
|
||||
|
||||
/// Unset the specified options.
|
||||
#[arg(short = 'u')]
|
||||
unset: bool,
|
||||
|
||||
/// Names of options to operate on.
|
||||
options: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ShoptCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if self.set && self.unset {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"cannot set and unset shell options simultaneously"
|
||||
)?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
if self.options.is_empty() {
|
||||
if self.quiet {
|
||||
return Ok(ExecutionResult::success());
|
||||
}
|
||||
|
||||
// Enumerate all options of the selected type.
|
||||
let options = if self.set_o_names_only {
|
||||
brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::SetO)
|
||||
.iter()
|
||||
.sorted_by_key(|opt| opt.name)
|
||||
} else {
|
||||
brush_core::namedoptions::options(brush_core::namedoptions::ShellOptionKind::Shopt)
|
||||
.iter()
|
||||
.sorted_by_key(|opt| opt.name)
|
||||
};
|
||||
|
||||
for option in options {
|
||||
let option_value = option.definition.get(context.shell.options());
|
||||
if self.set && !option_value {
|
||||
continue;
|
||||
}
|
||||
if self.unset && option_value {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.print {
|
||||
if self.set_o_names_only {
|
||||
let option_value_str = if option_value { "-o" } else { "+o" };
|
||||
writeln!(context.stdout(), "set {option_value_str} {}", option.name)?;
|
||||
} else {
|
||||
let option_value_str = if option_value { "-s" } else { "-u" };
|
||||
writeln!(context.stdout(), "shopt {option_value_str} {}", option.name)?;
|
||||
}
|
||||
} else {
|
||||
let option_value_str = if option_value { "on" } else { "off" };
|
||||
writeln!(context.stdout(), "{:20}\t{option_value_str}", option.name)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
let mut return_value = ExecutionResult::success();
|
||||
|
||||
// Enumerate only the specified options.
|
||||
for option_name in &self.options {
|
||||
let option_definition = if self.set_o_names_only {
|
||||
brush_core::namedoptions::options(
|
||||
brush_core::namedoptions::ShellOptionKind::SetO,
|
||||
)
|
||||
.get(option_name.as_str())
|
||||
} else {
|
||||
brush_core::namedoptions::options(
|
||||
brush_core::namedoptions::ShellOptionKind::Shopt,
|
||||
)
|
||||
.get(option_name.as_str())
|
||||
};
|
||||
|
||||
if let Some(option_definition) = option_definition {
|
||||
if self.set {
|
||||
option_definition.set(context.shell.options_mut(), true);
|
||||
} else if self.unset {
|
||||
option_definition.set(context.shell.options_mut(), false);
|
||||
} else {
|
||||
let option_value = option_definition.get(context.shell.options());
|
||||
if !option_value {
|
||||
return_value = ExecutionResult::general_error();
|
||||
}
|
||||
|
||||
if !self.quiet {
|
||||
if self.print {
|
||||
if self.set_o_names_only {
|
||||
let option_value_str = if option_value { "-o" } else { "+o" };
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"set {option_value_str} {option_name}"
|
||||
)?;
|
||||
} else {
|
||||
let option_value_str = if option_value { "-s" } else { "-u" };
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"shopt {option_value_str} {option_name}"
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
let option_value_str = if option_value { "on" } else { "off" };
|
||||
writeln!(context.stdout(), "{option_name:20}\t{option_value_str}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {}: invalid shell option name",
|
||||
context.command_name,
|
||||
option_name
|
||||
)?;
|
||||
return_value = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(return_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins};
|
||||
|
||||
/// Suspend the shell.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct SuspendCommand {
|
||||
/// Force suspend login shells.
|
||||
#[arg(short = 'f')]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
impl builtins::Command for SuspendCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if context.shell.options().login_shell && !self.force {
|
||||
writeln!(context.stderr(), "login shell cannot be suspended")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
|
||||
#[expect(clippy::cast_possible_wrap)]
|
||||
brush_core::sys::signal::kill_process(
|
||||
std::process::id() as i32,
|
||||
brush_core::traps::TrapSignal::Signal(nix::sys::signal::SIGSTOP),
|
||||
)?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{
|
||||
ErrorKind, ExecutionExitCode, ExecutionParameters, ExecutionResult, Shell, builtins, tests,
|
||||
};
|
||||
|
||||
/// Evaluate test expression.
|
||||
#[derive(Parser)]
|
||||
#[clap(disable_help_flag = true, disable_version_flag = true)]
|
||||
pub(crate) struct TestCommand {
|
||||
#[clap(allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for TestCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
/// Override the default [`builtins::Command::new`] function to handle clap's limitation related
|
||||
/// to `--`. See [`builtins::parse_known`] for more information
|
||||
/// TODO(test): we can safely remove this after the issue is resolved
|
||||
fn new<I>(args: I) -> Result<Self, clap::Error>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let (mut this, rest_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
if let Some(args) = rest_args {
|
||||
this.args.extend(args);
|
||||
}
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut args = self.args.as_slice();
|
||||
|
||||
if context.command_name == "[" {
|
||||
match args.last() {
|
||||
Some(s) if s == "]" => (),
|
||||
None | Some(_) => {
|
||||
writeln!(context.stderr(), "[: missing ']'")?;
|
||||
return Ok(ExecutionExitCode::InvalidUsage.into());
|
||||
}
|
||||
}
|
||||
|
||||
args = &args[0..args.len() - 1];
|
||||
}
|
||||
|
||||
if execute_test(context.shell, &context.params, args)? {
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_test(
|
||||
shell: &mut Shell<impl brush_core::ShellExtensions>,
|
||||
params: &ExecutionParameters,
|
||||
args: &[String],
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
let test_command =
|
||||
brush_parser::test_command::parse(args).map_err(ErrorKind::TestCommandParseError)?;
|
||||
tests::eval_expr(&test_command, shell, params)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins, timing};
|
||||
|
||||
/// Report on usage time.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct TimesCommand {}
|
||||
|
||||
impl builtins::Command for TimesCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
let (self_user, self_system) = brush_core::sys::resource::get_self_user_and_system_time()?;
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{} {}",
|
||||
timing::format_duration_non_posixly(&self_user),
|
||||
timing::format_duration_non_posixly(&self_system),
|
||||
)?;
|
||||
|
||||
let (children_user, children_system) =
|
||||
brush_core::sys::resource::get_children_user_and_system_time()?;
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{} {}",
|
||||
timing::format_duration_non_posixly(&children_user),
|
||||
timing::format_duration_non_posixly(&children_system),
|
||||
)?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::traps::TrapSignal;
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Manage signal traps.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct TrapCommand {
|
||||
/// List all signal names.
|
||||
#[arg(short = 'l')]
|
||||
list_signals: bool,
|
||||
|
||||
/// Print registered trap commands.
|
||||
#[arg(short = 'p')]
|
||||
print_trap_commands: bool,
|
||||
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for TrapCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
mut context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if self.list_signals {
|
||||
brush_core::traps::format_signals(context.stdout(), TrapSignal::iterator())
|
||||
.map(|()| ExecutionResult::success())
|
||||
} else if self.print_trap_commands || self.args.is_empty() {
|
||||
if !self.args.is_empty() {
|
||||
for signal_type in &self.args {
|
||||
Self::display_handlers_for(&context, signal_type.parse()?)?;
|
||||
}
|
||||
} else {
|
||||
Self::display_all_handlers(&context)?;
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
} else if self.args.len() == 1 {
|
||||
// When only a single argument is given, it is assumed to be a signal name
|
||||
// and an indication to remove the handlers for that signal.
|
||||
let signal = self.args[0].as_str();
|
||||
Self::remove_all_handlers(&mut context, signal.parse()?);
|
||||
Ok(ExecutionResult::success())
|
||||
} else if self.args[0] == "-" {
|
||||
// "-" as the first argument indicates that the remaining
|
||||
// arguments are signal names and we need to remove the handlers for them.
|
||||
for signal in &self.args[1..] {
|
||||
Self::remove_all_handlers(&mut context, signal.parse()?);
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
let handler = &self.args[0];
|
||||
|
||||
let mut signal_types = vec![];
|
||||
for signal in &self.args[1..] {
|
||||
signal_types.push(signal.parse()?);
|
||||
}
|
||||
|
||||
Self::register_handler(&mut context, signal_types, handler.as_str());
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TrapCommand {
|
||||
fn display_all_handlers(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
for (signal, _) in context.shell.traps().iter_handlers() {
|
||||
Self::display_handlers_for(context, signal)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_handlers_for(
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
signal_type: TrapSignal,
|
||||
) -> Result<(), brush_core::Error> {
|
||||
if let Some(handler) = context.shell.traps().get_handler(signal_type) {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"trap -- '{}' {signal_type}",
|
||||
&handler.command
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_all_handlers(
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
signal: TrapSignal,
|
||||
) {
|
||||
context.shell.traps_mut().remove_handlers(signal);
|
||||
}
|
||||
|
||||
fn register_handler(
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
signals: Vec<TrapSignal>,
|
||||
handler: &str,
|
||||
) {
|
||||
// Our new source context is relative to the current position.
|
||||
// TODO(source-info): Provide the location of the specific token that makes up
|
||||
// `self.args[0]`.
|
||||
let source_info = context.shell.call_stack().current_pos_as_source_info();
|
||||
|
||||
for signal in signals {
|
||||
context.shell.traps_mut().register_handler(
|
||||
signal,
|
||||
handler.to_owned(),
|
||||
source_info.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use brush_core::{ExecutionResult, builtins, error};
|
||||
|
||||
/// No-op command. Same with :.
|
||||
pub(crate) struct TrueCommand {}
|
||||
|
||||
impl builtins::SimpleCommand for TrueCommand {
|
||||
fn get_content(
|
||||
_name: &str,
|
||||
content_type: builtins::ContentType,
|
||||
_options: &builtins::ContentOptions,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
match content_type {
|
||||
builtins::ContentType::DetailedHelp => Ok("Returns a successful exit status.".into()),
|
||||
builtins::ContentType::ShortUsage => Ok("true".into()),
|
||||
builtins::ContentType::ShortDescription => Ok("true - success".into()),
|
||||
builtins::ContentType::ManPage => error::unimp("man page not yet implemented"),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute<SE: brush_core::ShellExtensions, I: Iterator<Item = S>, S: AsRef<str>>(
|
||||
_context: brush_core::ExecutionContext<'_, SE>,
|
||||
_args: I,
|
||||
) -> Result<ExecutionResult, brush_core::Error> {
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::sys::{self, fs::PathExt};
|
||||
use brush_core::{ExecutionResult, Shell, builtins, parser::ast};
|
||||
|
||||
/// Inspect the type of a named shell item.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct TypeCommand {
|
||||
/// Display all locations of the specified name, not just the first.
|
||||
#[arg(short = 'a')]
|
||||
all_locations: bool,
|
||||
|
||||
/// Don't consider functions when resolving the name.
|
||||
#[arg(short = 'f')]
|
||||
suppress_func_lookup: bool,
|
||||
|
||||
/// Force searching by file path, even if the name is an alias, built-in
|
||||
/// command, or shell function.
|
||||
#[arg(short = 'P')]
|
||||
force_path_search: bool,
|
||||
|
||||
/// Show file path only.
|
||||
#[arg(short = 'p')]
|
||||
show_path_only: bool,
|
||||
|
||||
/// Only display the type of the specified name.
|
||||
#[arg(short = 't')]
|
||||
type_only: bool,
|
||||
|
||||
/// Names to search for.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
enum ResolvedType<'a> {
|
||||
Alias(String),
|
||||
Keyword,
|
||||
Function(&'a ast::FunctionDefinition),
|
||||
Builtin,
|
||||
File { path: PathBuf, hashed: bool },
|
||||
}
|
||||
|
||||
impl builtins::Command for TypeCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
for name in &self.names {
|
||||
let resolved_types = self.resolve_types(context.shell, name);
|
||||
|
||||
if resolved_types.is_empty() {
|
||||
if !self.type_only && !self.force_path_search && !self.show_path_only {
|
||||
writeln!(context.stderr(), "type: {name} not found")?;
|
||||
}
|
||||
|
||||
result = ExecutionResult::general_error();
|
||||
continue;
|
||||
}
|
||||
|
||||
for resolved_type in resolved_types {
|
||||
if self.show_path_only && !matches!(resolved_type, ResolvedType::File { .. }) {
|
||||
// Do nothing.
|
||||
} else if self.type_only {
|
||||
match resolved_type {
|
||||
ResolvedType::Alias(_) => {
|
||||
writeln!(context.stdout(), "alias")?;
|
||||
}
|
||||
ResolvedType::Keyword => {
|
||||
writeln!(context.stdout(), "keyword")?;
|
||||
}
|
||||
ResolvedType::Function(_) => {
|
||||
writeln!(context.stdout(), "function")?;
|
||||
}
|
||||
ResolvedType::Builtin => {
|
||||
writeln!(context.stdout(), "builtin")?;
|
||||
}
|
||||
ResolvedType::File { path, .. } => {
|
||||
if self.show_path_only || self.force_path_search {
|
||||
writeln!(context.stdout(), "{}", path.to_string_lossy())?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "file")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match resolved_type {
|
||||
ResolvedType::Alias(target) => {
|
||||
writeln!(context.stdout(), "{name} is aliased to `{target}'")?;
|
||||
}
|
||||
ResolvedType::Keyword => {
|
||||
writeln!(context.stdout(), "{name} is a shell keyword")?;
|
||||
}
|
||||
ResolvedType::Function(def) => {
|
||||
writeln!(context.stdout(), "{name} is a function")?;
|
||||
writeln!(context.stdout(), "{def}")?;
|
||||
}
|
||||
ResolvedType::Builtin => {
|
||||
writeln!(context.stdout(), "{name} is a shell builtin")?;
|
||||
}
|
||||
ResolvedType::File { path, hashed } => {
|
||||
if hashed && self.all_locations && !self.force_path_search {
|
||||
// Do nothing. When we're displaying all locations, then
|
||||
// we don't show hashed paths.
|
||||
} else if self.show_path_only || self.force_path_search {
|
||||
writeln!(context.stdout(), "{}", path.to_string_lossy())?;
|
||||
} else if hashed {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{name} is hashed ({path})",
|
||||
name = name,
|
||||
path = path.to_string_lossy()
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{name} is {path}",
|
||||
name = name,
|
||||
path = path.to_string_lossy()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we only want the first, then break after the first.
|
||||
if !self.all_locations {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeCommand {
|
||||
fn resolve_types<'a, SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
shell: &'a Shell<SE>,
|
||||
name: &str,
|
||||
) -> Vec<ResolvedType<'a>> {
|
||||
let mut types = vec![];
|
||||
|
||||
if !self.force_path_search {
|
||||
// Check for aliases.
|
||||
if let Some(a) = shell.aliases().get(name) {
|
||||
types.push(ResolvedType::Alias(a.clone()));
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keywords.
|
||||
if shell.is_keyword(name) {
|
||||
types.push(ResolvedType::Keyword);
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for functions.
|
||||
if !self.suppress_func_lookup {
|
||||
if let Some(registration) = shell.funcs().get(name) {
|
||||
types.push(ResolvedType::Function(registration.definition()));
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for builtins.
|
||||
if shell.builtins().get(name).is_some_and(|b| !b.disabled) {
|
||||
types.push(ResolvedType::Builtin);
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look in path.
|
||||
if sys::fs::contains_path_separator(name) {
|
||||
if shell.absolute_path(Path::new(name)).executable() {
|
||||
types.push(ResolvedType::File {
|
||||
path: PathBuf::from(name),
|
||||
hashed: false,
|
||||
});
|
||||
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(path) = shell.program_location_cache().get(name) {
|
||||
types.push(ResolvedType::File { path, hashed: true });
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
|
||||
for item in shell.find_executables_in_path(name) {
|
||||
types.push(ResolvedType::File {
|
||||
path: item,
|
||||
hashed: false,
|
||||
});
|
||||
|
||||
if !self.all_locations {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
types
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
use clap::{
|
||||
Parser,
|
||||
builder::{IntoResettable, StyledStr},
|
||||
};
|
||||
use std::{
|
||||
io::{self, ErrorKind, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Unit {
|
||||
Block,
|
||||
Bytes,
|
||||
HalfKBytes,
|
||||
KBytes,
|
||||
Micros,
|
||||
Number,
|
||||
Seconds,
|
||||
}
|
||||
|
||||
impl Unit {
|
||||
const fn scale(self) -> u64 {
|
||||
match self {
|
||||
Self::Block | Self::HalfKBytes => 512,
|
||||
Self::KBytes => 1024,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Virtual {
|
||||
Pipe,
|
||||
VMem,
|
||||
}
|
||||
|
||||
impl Virtual {
|
||||
fn get(self) -> std::io::Result<(u64, u64)> {
|
||||
match self {
|
||||
Self::Pipe => {
|
||||
let lim = nix::unistd::PathconfVar::PIPE_BUF as u64 * 512;
|
||||
Ok((lim, lim))
|
||||
}
|
||||
Self::VMem => rlimit::Resource::AS
|
||||
.get()
|
||||
.or_else(|_| rlimit::Resource::VMEM.get()),
|
||||
}
|
||||
}
|
||||
fn set(self, soft: u64, hard: u64) -> std::io::Result<()> {
|
||||
match self {
|
||||
Self::Pipe => Err(std::io::Error::from(ErrorKind::Unsupported)),
|
||||
Self::VMem => rlimit::Resource::AS
|
||||
.set(soft, hard)
|
||||
.or_else(|_| rlimit::Resource::VMEM.set(soft, hard)),
|
||||
}
|
||||
}
|
||||
const fn is_supported(self) -> bool {
|
||||
match self {
|
||||
Self::Pipe => true,
|
||||
Self::VMem => {
|
||||
rlimit::Resource::AS.is_supported() || rlimit::Resource::VMEM.is_supported()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Resource {
|
||||
Phy(rlimit::Resource),
|
||||
Virt(Virtual),
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
fn get(self) -> std::io::Result<(u64, u64)> {
|
||||
match self {
|
||||
Self::Phy(res) => res.get(),
|
||||
Self::Virt(res) => res.get(),
|
||||
}
|
||||
}
|
||||
fn set(self, soft: u64, hard: u64) -> std::io::Result<()> {
|
||||
match self {
|
||||
Self::Phy(res) => res.set(soft, hard),
|
||||
Self::Virt(res) => res.set(soft, hard),
|
||||
}
|
||||
}
|
||||
const fn is_supported(self) -> bool {
|
||||
match self {
|
||||
Self::Phy(res) => res.is_supported(),
|
||||
Self::Virt(res) => res.is_supported(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ResourceDescription {
|
||||
resource: Resource,
|
||||
help: &'static str,
|
||||
description: &'static str,
|
||||
short: char,
|
||||
unit: Unit,
|
||||
}
|
||||
|
||||
impl ResourceDescription {
|
||||
const SBSIZE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::SBSIZE),
|
||||
help: "the socket buffer size",
|
||||
description: "socket buffer size",
|
||||
short: 'b',
|
||||
unit: Unit::Bytes,
|
||||
};
|
||||
const CORE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::CORE),
|
||||
help: "the maximum size of core files created",
|
||||
description: "core file size",
|
||||
short: 'c',
|
||||
unit: Unit::Block,
|
||||
};
|
||||
const DATA: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::DATA),
|
||||
help: "the maximum size of a process's data segment",
|
||||
description: "data seg size",
|
||||
short: 'd',
|
||||
unit: Unit::KBytes,
|
||||
};
|
||||
const NICE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::NICE),
|
||||
help: "the maximum scheduling priority (`nice`)",
|
||||
description: "scheduling priority",
|
||||
short: 'e',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const FSIZE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::FSIZE),
|
||||
help: "the maximum size of files written by the shell and its children",
|
||||
description: "file size",
|
||||
short: 'f',
|
||||
unit: Unit::Block,
|
||||
};
|
||||
const SIGPENDING: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::SIGPENDING),
|
||||
help: "the maximum number of pending signals",
|
||||
description: "pending signals",
|
||||
short: 'i',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const MEMLOCK: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::MEMLOCK),
|
||||
help: "the maximum size a process may lock into memory",
|
||||
description: "max locked memory",
|
||||
short: 'l',
|
||||
unit: Unit::KBytes,
|
||||
};
|
||||
const KQUEUES: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::KQUEUES),
|
||||
help: "the maximum number of kqueues allocated for this process",
|
||||
description: "max kqueues",
|
||||
short: 'k',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const RSS: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::RSS),
|
||||
help: "the maximum resident set size",
|
||||
description: "max memory size",
|
||||
short: 'm',
|
||||
unit: Unit::KBytes,
|
||||
};
|
||||
const LOCKS: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::LOCKS),
|
||||
help: "the maximum number of file locks",
|
||||
description: "file locks",
|
||||
short: 'x',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const NOFILE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::NOFILE),
|
||||
help: "the maximum number of open file descriptors",
|
||||
description: "open files",
|
||||
short: 'n',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const MSGQUEUE: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::MSGQUEUE),
|
||||
help: "the maximum number of bytes in POSIX message queues",
|
||||
description: "POSIX message queues",
|
||||
short: 'q',
|
||||
unit: Unit::Bytes,
|
||||
};
|
||||
const PIPE: Self = Self {
|
||||
resource: Resource::Virt(Virtual::Pipe),
|
||||
help: "the pipe buffer size",
|
||||
description: "pipe size",
|
||||
short: 'p',
|
||||
unit: Unit::HalfKBytes,
|
||||
};
|
||||
const RTPRIO: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::RTPRIO),
|
||||
help: "the maximum real-time scheduling priority",
|
||||
description: "real-time priority",
|
||||
short: 'r',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const RTTIME: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::RTTIME),
|
||||
help: "the maximum real-time scheduling priority",
|
||||
description: "real-time non-blocking time",
|
||||
short: 'R',
|
||||
unit: Unit::Micros,
|
||||
};
|
||||
const STACK: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::STACK),
|
||||
help: "the maximum stack size",
|
||||
description: "stack size",
|
||||
short: 's',
|
||||
unit: Unit::KBytes,
|
||||
};
|
||||
const CPU: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::CPU),
|
||||
help: "the maximum amount of cpu time in seconds",
|
||||
description: "cpu time",
|
||||
short: 't',
|
||||
unit: Unit::Seconds,
|
||||
};
|
||||
const NPROC: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::NPROC),
|
||||
help: "the maximum number of user processes",
|
||||
description: "max user processes",
|
||||
short: 'u',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const VMEM: Self = Self {
|
||||
resource: Resource::Virt(Virtual::VMem),
|
||||
help: "the size of virtual memory",
|
||||
description: "virtual memory",
|
||||
short: 'v',
|
||||
unit: Unit::KBytes,
|
||||
};
|
||||
const THREADS: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::THREADS),
|
||||
help: "the maximum number of threads",
|
||||
description: "number of threads",
|
||||
short: 'T',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
const NPTS: Self = Self {
|
||||
resource: Resource::Phy(rlimit::Resource::NPTS),
|
||||
help: "the maximum number of pseudoterminals",
|
||||
description: "number of pseudoterminals",
|
||||
short: 'P',
|
||||
unit: Unit::Number,
|
||||
};
|
||||
|
||||
fn get(&self, hard: bool) -> std::io::Result<String> {
|
||||
let (soft_limit, hard_limit) = self.resource.get()?;
|
||||
let val = if hard { hard_limit } else { soft_limit };
|
||||
|
||||
if val == rlimit::INFINITY {
|
||||
Ok("unlimited".into())
|
||||
} else {
|
||||
Ok(format!("{}", val / self.unit.scale()))
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&self, set_hard: bool, value: LimitValue) -> std::io::Result<()> {
|
||||
let (soft, hard) = self.resource.get()?;
|
||||
let value = match value {
|
||||
LimitValue::Soft => soft,
|
||||
LimitValue::Hard => hard,
|
||||
LimitValue::Unlimited => rlimit::INFINITY,
|
||||
LimitValue::Value(v) => v * self.unit.scale(),
|
||||
LimitValue::Unset => return Ok(()),
|
||||
};
|
||||
|
||||
if set_hard {
|
||||
self.resource.set(soft, value)
|
||||
} else {
|
||||
self.resource.set(value, hard)
|
||||
}
|
||||
}
|
||||
|
||||
/// Print either soft or hard limit
|
||||
fn print(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
hard: bool,
|
||||
) -> io::Result<()> {
|
||||
if !self.resource.is_supported() {
|
||||
return Ok(());
|
||||
}
|
||||
let unit = match self.unit {
|
||||
Unit::Block => format!("(block, -{})", self.short),
|
||||
Unit::Bytes => format!("(bytes, -{})", self.short),
|
||||
Unit::HalfKBytes => format!("(512 bytes, -{})", self.short),
|
||||
Unit::KBytes => format!("(kbytes, -{})", self.short),
|
||||
Unit::Micros => format!("(microseconds, -{})", self.short),
|
||||
Unit::Number => format!("(-{})", self.short),
|
||||
Unit::Seconds => format!("(seconds, -{})", self.short),
|
||||
};
|
||||
let resource = self.get(hard).unwrap_or_else(|e| format!("{e}"));
|
||||
writeln!(
|
||||
context.stdout(),
|
||||
"{:<26}{:>16} {}",
|
||||
self.description,
|
||||
unit,
|
||||
resource
|
||||
)
|
||||
}
|
||||
|
||||
/// Provide the matching help String
|
||||
fn help(&self) -> String {
|
||||
format!(
|
||||
"{} {}",
|
||||
self.help,
|
||||
if self.resource.is_supported() {
|
||||
"(supported)"
|
||||
} else {
|
||||
"(unsupported)"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResettable<StyledStr> for ResourceDescription {
|
||||
fn into_resettable(self) -> clap::builder::Resettable<StyledStr> {
|
||||
clap::builder::Resettable::Value(self.help().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum LimitValue {
|
||||
Unset,
|
||||
Unlimited,
|
||||
Soft,
|
||||
Hard,
|
||||
Value(u64),
|
||||
}
|
||||
|
||||
impl FromStr for LimitValue {
|
||||
type Err = <u64 as FromStr>::Err;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let v = match s {
|
||||
"" => Self::Unset,
|
||||
"unlimited" => Self::Unlimited,
|
||||
"soft" => Self::Soft,
|
||||
"hard" => Self::Hard,
|
||||
_ => Self::Value(s.parse()?),
|
||||
};
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify shell resource limits.
|
||||
///
|
||||
/// Provides control over the resources available to the shell and processes
|
||||
/// it creates, on systems that allow such control.
|
||||
#[derive(Parser, Debug)]
|
||||
pub(crate) struct ULimitCommand {
|
||||
/// use the `soft` resource limit
|
||||
#[arg(short = 'S')]
|
||||
soft: bool,
|
||||
/// use the `hard` resource limit
|
||||
#[arg(short = 'H')]
|
||||
hard: bool,
|
||||
/// all current limits are reported
|
||||
#[arg(short = 'a')]
|
||||
all: bool,
|
||||
/// the maximum socket buffer size
|
||||
#[arg(short = 'b', default_missing_value = "", num_args(0..=1), help = ResourceDescription::SBSIZE)]
|
||||
sbsize: Option<LimitValue>,
|
||||
/// the maximum size of core files created
|
||||
#[arg(short = 'c', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CORE)]
|
||||
core: Option<LimitValue>,
|
||||
/// the maximum size of a process's data segment
|
||||
#[arg(short = 'd', default_missing_value = "", num_args(0..=1), help = ResourceDescription::DATA)]
|
||||
data: Option<LimitValue>,
|
||||
/// the maximum scheduling priority (`nice`)
|
||||
#[arg(short = 'e', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NICE)]
|
||||
nice: Option<LimitValue>,
|
||||
/// the maximum size of files written by the shell and its children
|
||||
#[arg(short = 'f', default_missing_value = "", num_args(0..=1), help = ResourceDescription::FSIZE)]
|
||||
file_size: Option<LimitValue>,
|
||||
/// the maximum number of pending signals
|
||||
#[arg(short = 'i', default_missing_value = "", num_args(0..=1), help = ResourceDescription::SIGPENDING)]
|
||||
sigpending: Option<LimitValue>,
|
||||
/// the maximum size a process may lock into memory
|
||||
#[arg(short = 'l', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MEMLOCK)]
|
||||
memlock: Option<LimitValue>,
|
||||
/// the maximum number of kqueues allocated for this process
|
||||
#[arg(short = 'k', default_missing_value = "", num_args(0..=1), help = ResourceDescription::KQUEUES)]
|
||||
kqueues: Option<LimitValue>,
|
||||
/// the maximum resident set size
|
||||
#[arg(short = 'm', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RSS)]
|
||||
rss: Option<LimitValue>,
|
||||
/// the maximum number of open file descriptors
|
||||
#[arg(short = 'n', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NOFILE)]
|
||||
file_open: Option<LimitValue>,
|
||||
/// the pipe buffer size
|
||||
#[arg(short = 'p', default_missing_value = "", num_args(0..=1), help = ResourceDescription::PIPE)]
|
||||
pipe: Option<LimitValue>,
|
||||
/// the maximum number of bytes in POSIX message queues
|
||||
#[arg(short = 'q', default_missing_value = "", num_args(0..=1), help = ResourceDescription::MSGQUEUE)]
|
||||
msgqueue: Option<LimitValue>,
|
||||
/// the maximum real-time scheduling priority
|
||||
#[arg(short = 'r', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTPRIO)]
|
||||
rtprio: Option<LimitValue>,
|
||||
/// the maximum stack size
|
||||
#[arg(short = 's', default_missing_value = "", num_args(0..=1), help = ResourceDescription::STACK)]
|
||||
stack: Option<LimitValue>,
|
||||
/// the maximum amount of cpu time in seconds
|
||||
#[arg(short = 't', default_missing_value = "", num_args(0..=1), help = ResourceDescription::CPU)]
|
||||
cpu: Option<LimitValue>,
|
||||
/// the size of virtual memory
|
||||
#[arg(short = 'u', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPROC)]
|
||||
nproc: Option<LimitValue>,
|
||||
/// the size of virtual memory
|
||||
#[arg(short = 'v', default_missing_value = "", num_args(0..=1), help = ResourceDescription::VMEM)]
|
||||
vmem: Option<LimitValue>,
|
||||
/// the maximum number of file locks
|
||||
#[arg(short = 'x', default_missing_value = "", num_args(0..=1), help = ResourceDescription::LOCKS)]
|
||||
file_lock: Option<LimitValue>,
|
||||
/// the maximum number of pseudoterminals
|
||||
#[arg(short = 'P', default_missing_value = "", num_args(0..=1), help = ResourceDescription::NPTS)]
|
||||
npts: Option<LimitValue>,
|
||||
/// real-time non-blocking time
|
||||
#[arg(short = 'R', default_missing_value = "", num_args(0..=1), help = ResourceDescription::RTTIME)]
|
||||
rttime: Option<LimitValue>,
|
||||
/// the maximum number of threads
|
||||
#[arg(short = 'T', default_missing_value = "", num_args(0..=1), help = ResourceDescription::THREADS)]
|
||||
threads: Option<LimitValue>,
|
||||
|
||||
/// argument for the implicit limit (`-f`)
|
||||
limit: Option<LimitValue>,
|
||||
}
|
||||
|
||||
impl builtins::Command for ULimitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let exit_code = ExecutionResult::success();
|
||||
let mut resources_to_set = Vec::new();
|
||||
let mut resources_to_get = Vec::new();
|
||||
|
||||
let mut set_or_get = |val, descr| {
|
||||
match val {
|
||||
Some(LimitValue::Unset) => resources_to_get.push(descr),
|
||||
Some(v) => resources_to_set.push((descr, v)),
|
||||
None => {}
|
||||
}
|
||||
if self.all {
|
||||
resources_to_get.push(descr);
|
||||
}
|
||||
};
|
||||
|
||||
set_or_get(self.sbsize, ResourceDescription::SBSIZE);
|
||||
set_or_get(self.core, ResourceDescription::CORE);
|
||||
set_or_get(self.data, ResourceDescription::DATA);
|
||||
set_or_get(self.file_size, ResourceDescription::FSIZE);
|
||||
set_or_get(self.sigpending, ResourceDescription::SIGPENDING);
|
||||
set_or_get(self.kqueues, ResourceDescription::KQUEUES);
|
||||
set_or_get(self.memlock, ResourceDescription::MEMLOCK);
|
||||
set_or_get(self.rss, ResourceDescription::RSS);
|
||||
set_or_get(self.file_lock, ResourceDescription::LOCKS);
|
||||
set_or_get(self.file_open, ResourceDescription::NOFILE);
|
||||
set_or_get(self.pipe, ResourceDescription::PIPE);
|
||||
set_or_get(self.npts, ResourceDescription::NPTS);
|
||||
set_or_get(self.nice, ResourceDescription::NICE);
|
||||
set_or_get(self.msgqueue, ResourceDescription::MSGQUEUE);
|
||||
set_or_get(self.rtprio, ResourceDescription::RTPRIO);
|
||||
set_or_get(self.rttime, ResourceDescription::RTTIME);
|
||||
set_or_get(self.stack, ResourceDescription::STACK);
|
||||
set_or_get(self.threads, ResourceDescription::THREADS);
|
||||
set_or_get(self.cpu, ResourceDescription::CPU);
|
||||
set_or_get(self.nproc, ResourceDescription::NPROC);
|
||||
set_or_get(self.vmem, ResourceDescription::VMEM);
|
||||
|
||||
if resources_to_set.is_empty() {
|
||||
if resources_to_get.is_empty() {
|
||||
if let Some(fsize) = self.limit {
|
||||
resources_to_set.push((ResourceDescription::FSIZE, fsize));
|
||||
} else {
|
||||
resources_to_get.push(ResourceDescription::FSIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (resource, value) in resources_to_set {
|
||||
resource.set(self.hard, value)?;
|
||||
}
|
||||
|
||||
if resources_to_get.len() == 1 {
|
||||
writeln!(context.stdout(), "{}", resources_to_get[0].get(self.hard)?)?;
|
||||
} else {
|
||||
for resource in resources_to_get {
|
||||
resource.print(&context, self.hard)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use brush_core::{ErrorKind, ExecutionResult, builtins};
|
||||
use cfg_if::cfg_if;
|
||||
use clap::Parser;
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
use nix::sys::stat::Mode;
|
||||
use std::io::Write;
|
||||
|
||||
/// Manage the process umask.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct UmaskCommand {
|
||||
/// If MODE is omitted, output in a form that may be reused as input.
|
||||
#[arg(short = 'p')]
|
||||
print_roundtrippable: bool,
|
||||
|
||||
/// Makes the output symbolic; otherwise an octal number is given.
|
||||
#[arg(short = 'S')]
|
||||
symbolic_output: bool,
|
||||
|
||||
/// Mode mask.
|
||||
mode: Option<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UmaskCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
if let Some(mode) = &self.mode {
|
||||
if mode.starts_with(|c: char| c.is_digit(8)) {
|
||||
let parsed = brush_core::int_utils::parse(mode.as_str(), 8)?;
|
||||
set_umask(parsed)?;
|
||||
} else {
|
||||
return brush_core::error::unimp("umask setting mode from symbolic value");
|
||||
}
|
||||
} else {
|
||||
let umask = get_umask()?;
|
||||
|
||||
let formatted = if self.symbolic_output {
|
||||
let u = symbolic_mask_from_bits((!umask & 0o700) >> 6);
|
||||
let g = symbolic_mask_from_bits((!umask & 0o070) >> 3);
|
||||
let o = symbolic_mask_from_bits(!umask & 0o007);
|
||||
std::format!("u={u},g={g},o={o}")
|
||||
} else {
|
||||
std::format!("{umask:04o}")
|
||||
};
|
||||
|
||||
if self.print_roundtrippable {
|
||||
writeln!(context.stdout(), "umask {formatted}")?;
|
||||
} else {
|
||||
writeln!(context.stdout(), "{formatted}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(any(target_os = "linux", target_os = "android"))] {
|
||||
fn get_umask() -> Result<u32, brush_core::Error> {
|
||||
let umask = procfs::process::Process::myself().ok().and_then(|me| me.status().ok()).and_then(|status| status.umask);
|
||||
umask.ok_or_else(|| brush_core::ErrorKind::InvalidUmask.into())
|
||||
}
|
||||
} else {
|
||||
#[expect(clippy::unnecessary_wraps)]
|
||||
fn get_umask() -> Result<u32, brush_core::Error> {
|
||||
let u = nix::sys::stat::umask(Mode::empty());
|
||||
nix::sys::stat::umask(u);
|
||||
Ok(u32::from(u.bits()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_umask(value: nix::sys::stat::mode_t) -> Result<(), brush_core::Error> {
|
||||
// value of mode_t can be platform dependent
|
||||
let mode = nix::sys::stat::Mode::from_bits(value).ok_or_else(|| ErrorKind::InvalidUmask)?;
|
||||
nix::sys::stat::umask(mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn symbolic_mask_from_bits(bits: u32) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
if (bits & 0b100) != 0 {
|
||||
result.push('r');
|
||||
}
|
||||
if (bits & 0b010) != 0 {
|
||||
result.push('w');
|
||||
}
|
||||
if (bits & 0b001) != 0 {
|
||||
result.push('x');
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionResult, builtins};
|
||||
|
||||
/// Unset a shell alias.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct UnaliasCommand {
|
||||
/// Remove all aliases.
|
||||
#[arg(short = 'a')]
|
||||
remove_all: bool,
|
||||
|
||||
/// Names of aliases to operate on.
|
||||
aliases: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UnaliasCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
let mut exit_code = ExecutionResult::success();
|
||||
|
||||
if self.remove_all {
|
||||
context.shell.aliases_mut().clear();
|
||||
} else {
|
||||
for alias in &self.aliases {
|
||||
if context.shell.aliases_mut().remove(alias).is_none() {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: {}: not found",
|
||||
context.command_name,
|
||||
alias
|
||||
)?;
|
||||
exit_code = ExecutionResult::general_error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use brush_core::{ExecutionExitCode, builtins, trace_categories};
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
/// (UNIMPLEMENTED COMMAND)
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct UnimplementedCommand {
|
||||
#[clap(allow_hyphen_values = true)]
|
||||
args: Vec<String>,
|
||||
|
||||
#[clap(skip)]
|
||||
declarations: Vec<brush_core::CommandArg>,
|
||||
}
|
||||
|
||||
impl builtins::Command for UnimplementedCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
tracing::warn!(target: trace_categories::UNIMPLEMENTED,
|
||||
"unimplemented built-in: {} {}",
|
||||
context.command_name,
|
||||
self.args.join(" ")
|
||||
);
|
||||
Ok(ExecutionExitCode::Unimplemented.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::DeclarationCommand for UnimplementedCommand {
|
||||
fn set_declarations(&mut self, declarations: Vec<brush_core::CommandArg>) {
|
||||
self.declarations = declarations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use brush_core::{ExecutionResult, Shell, ShellValue, builtins, variables::ShellValueUnsetType};
|
||||
|
||||
/// Unset a variable.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct UnsetCommand {
|
||||
#[clap(flatten)]
|
||||
name_interpretation: UnsetNameInterpretation,
|
||||
|
||||
/// Names of variables to unset.
|
||||
names: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(group = clap::ArgGroup::new("name-interpretation").multiple(false).required(false))]
|
||||
pub(crate) struct UnsetNameInterpretation {
|
||||
/// Treat each name as a shell function.
|
||||
#[arg(short = 'f', group = "name-interpretation")]
|
||||
shell_functions: bool,
|
||||
|
||||
/// Treat each name as a shell variable.
|
||||
#[arg(short = 'v', group = "name-interpretation")]
|
||||
shell_variables: bool,
|
||||
|
||||
/// Treat each name as a name reference.
|
||||
#[arg(short = 'n', group = "name-interpretation")]
|
||||
name_references: bool,
|
||||
}
|
||||
|
||||
impl UnsetNameInterpretation {
|
||||
pub const fn unspecified(&self) -> bool {
|
||||
!self.shell_functions && !self.shell_variables && !self.name_references
|
||||
}
|
||||
}
|
||||
|
||||
impl builtins::Command for UnsetCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<brush_core::ExecutionResult, Self::Error> {
|
||||
//
|
||||
// TODO(nameref): implement nameref
|
||||
//
|
||||
if self.name_interpretation.name_references {
|
||||
return brush_core::error::unimp("unset: name references are not yet implemented");
|
||||
}
|
||||
|
||||
let unspecified = self.name_interpretation.unspecified();
|
||||
|
||||
#[expect(clippy::needless_continue)]
|
||||
for name in &self.names {
|
||||
if unspecified || self.name_interpretation.shell_variables {
|
||||
// Try to parse the name as a parameter. If we can't, don't bail; it may not be a
|
||||
// valid variable name/parameter but could still be a function name.
|
||||
if let Ok(parameter) =
|
||||
brush_parser::word::parse_parameter(name, &context.shell.parser_options())
|
||||
{
|
||||
let result = match parameter {
|
||||
brush_parser::word::Parameter::Positional(_) => continue,
|
||||
brush_parser::word::Parameter::Special(_) => continue,
|
||||
brush_parser::word::Parameter::Named(name) => {
|
||||
context.shell.env_mut().unset(name.as_str())?.is_some()
|
||||
}
|
||||
brush_parser::word::Parameter::NamedWithIndex { name, index } => {
|
||||
unset_array_index(context.shell, name.as_str(), index.as_str())?
|
||||
}
|
||||
brush_parser::word::Parameter::NamedWithAllIndices {
|
||||
name: _,
|
||||
concatenate: _,
|
||||
} => continue,
|
||||
};
|
||||
|
||||
if result {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(unset): Deal with readonly functions
|
||||
if unspecified || self.name_interpretation.shell_functions {
|
||||
if context.shell.undefine_func(name) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
|
||||
fn unset_array_index(
|
||||
shell: &mut Shell<impl brush_core::ShellExtensions>,
|
||||
name: &str,
|
||||
index: &str,
|
||||
) -> Result<bool, brush_core::Error> {
|
||||
// First check to see if it's an associative array.
|
||||
let is_assoc_array = if let Some((_, var)) = shell.env().get(name) {
|
||||
matches!(
|
||||
var.value(),
|
||||
ShellValue::AssociativeArray(_)
|
||||
| ShellValue::Unset(ShellValueUnsetType::AssociativeArray)
|
||||
)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Compute which index we should actually use. For indexed arrays, we need to evaluate
|
||||
// the index string as an arithmetic expression first.
|
||||
let index_to_use: Cow<'_, str> = if is_assoc_array {
|
||||
index.into()
|
||||
} else {
|
||||
// First evaluate the index expression.
|
||||
let index_as_expr = brush_parser::arithmetic::parse(index)?;
|
||||
let evaluated_index = shell.eval_arithmetic(&index_as_expr)?;
|
||||
evaluated_index.to_string().into()
|
||||
};
|
||||
|
||||
// Now we can try to unset, and return the result.
|
||||
shell.env_mut().unset_index(name, index_to_use.as_ref())
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use clap::Parser;
|
||||
use std::io::Write;
|
||||
|
||||
use brush_core::{ExecutionExitCode, ExecutionResult, builtins, error};
|
||||
|
||||
/// Wait for jobs to terminate.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct WaitCommand {
|
||||
/// Wait for specified job to terminate (instead of change status).
|
||||
#[arg(short = 'f')]
|
||||
wait_for_terminate: bool,
|
||||
|
||||
/// Wait for a single job to change status; if jobs are specified, waits for
|
||||
/// the first to change status, and otherwise waits for the next change.
|
||||
#[arg(short = 'n')]
|
||||
wait_for_first_or_next: bool,
|
||||
|
||||
/// Name of variable to receive the job ID of the job whose status is indicated.
|
||||
#[arg(short = 'p', value_name = "VAR_NAME")]
|
||||
variable_to_receive_id: Option<String>,
|
||||
|
||||
/// Process IDs or job specs to wait for.
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl builtins::Command for WaitCommand {
|
||||
type Error = brush_core::Error;
|
||||
|
||||
async fn execute<SE: brush_core::ShellExtensions>(
|
||||
&self,
|
||||
context: brush_core::ExecutionContext<'_, SE>,
|
||||
) -> Result<ExecutionResult, Self::Error> {
|
||||
if self.wait_for_terminate {
|
||||
return error::unimp("wait -f");
|
||||
}
|
||||
if self.wait_for_first_or_next {
|
||||
return error::unimp("wait -n");
|
||||
}
|
||||
if self.variable_to_receive_id.is_some() {
|
||||
return error::unimp("wait -p");
|
||||
}
|
||||
|
||||
let mut result = ExecutionResult::success();
|
||||
|
||||
if !self.ids.is_empty() {
|
||||
for id in &self.ids {
|
||||
if id.starts_with('%') {
|
||||
// It's a job spec.
|
||||
if let Some(job) = context.shell.jobs_mut().resolve_job_spec(id) {
|
||||
job.wait().await?;
|
||||
} else {
|
||||
writeln!(
|
||||
context.stderr(),
|
||||
"{}: no such job: {}",
|
||||
context.command_name,
|
||||
id
|
||||
)?;
|
||||
|
||||
result = ExecutionExitCode::GeneralError.into();
|
||||
}
|
||||
} else {
|
||||
// It's a process ID.
|
||||
return error::unimp("wait with process IDs");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wait for all jobs.
|
||||
let jobs = context.shell.jobs_mut().wait_all().await?;
|
||||
|
||||
if context.shell.options().enable_job_control {
|
||||
for job in jobs {
|
||||
writeln!(context.stdout(), "{job}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user