diff --git a/README.md b/README.md index d2285b94..4aafb721 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ There's a lot that *is* working, but also non-trivial gaps in compatibility. Mos You can run `some-command &` but it's proof-of-concept quality at best. Standard job management via `fg`, `bg`, and `jobs` is not fully implemented. This would be a great area for enthusiastic contributors to dive in :). * **Honoring `set` options (e.g., `set -e`).** The `set` builtin is implemented, as is `set -x` and a few other options, but most of the behaviors aren't there. `set -e`, for example, will execute but its semantics aren't applied across execution. -* **Backtick (`` ` ``) expansions** - Modern command expansions (e.g. `$(command)`) work fine. It's just the tokenizing and parsing of backtick syntax that isn't there. Shell built-ins are a mixed bag. Some are completely and fully implemented (e.g. echo), while some only support their most commonly used options. Some aren't implemented at all. diff --git a/cli/tests/cases/builtins/enable.yaml b/cli/tests/cases/builtins/enable.yaml new file mode 100644 index 00000000..909d1aad --- /dev/null +++ b/cli/tests/cases/builtins/enable.yaml @@ -0,0 +1,30 @@ +name: "Builtins: enable" +cases: + - name: "List special builtins" + stdin: enable -s + + - name: "List default-disabled builtins" + stdin: enable -n + + - name: "List all builtins" + stdin: enable + + - name: "Disable builtins" + ignore_stderr: true + stdin: | + type printf + + # Disable the builtin + PATH= + enable -n printf + + # Check + type printf + print "Gone\n" + + # Re-enable + enable printf + + # Re-check + type printf + printf "Back\n" diff --git a/cli/tests/cases/builtins/jobs.yaml b/cli/tests/cases/builtins/jobs.yaml new file mode 100644 index 00000000..b13ceb8c --- /dev/null +++ b/cli/tests/cases/builtins/jobs.yaml @@ -0,0 +1,7 @@ +name: "Builtins: job-related builtins" +cases: + - name: "Basic async job" + stdin: | + echo hi & + wait + jobs diff --git a/cli/tests/cases/word_expansion.yaml b/cli/tests/cases/word_expansion.yaml index 4bdac52d..ea865d3d 100644 --- a/cli/tests/cases/word_expansion.yaml +++ b/cli/tests/cases/word_expansion.yaml @@ -56,6 +56,14 @@ cases: echo "$(var="updated"; echo ${var})" echo "var=${var}" + - name: "Backtick command substitution" + stdin: | + echo `echo hi` + + - name: "Backtick command substitution with escaping" + stdin: | + echo `echo \`echo hi\`` + - name: "String length" stdin: | x="abc" diff --git a/interactive-shell/src/interactive_shell.rs b/interactive-shell/src/interactive_shell.rs index 7dcecdc4..ce564ac6 100644 --- a/interactive-shell/src/interactive_shell.rs +++ b/interactive-shell/src/interactive_shell.rs @@ -80,6 +80,9 @@ impl InteractiveShell { pub async fn run_interactively(&mut self) -> Result<(), InteractiveShellError> { loop { + // Check for any completed jobs. + self.shell_mut().check_for_completed_jobs()?; + let result = self.run_interactively_once().await?; match result { InteractiveExecutionResult::Executed(shell::ExecutionResult { diff --git a/parser/src/word.rs b/parser/src/word.rs index f1a2dc72..f4ae801a 100644 --- a/parser/src/word.rs +++ b/parser/src/word.rs @@ -412,7 +412,7 @@ peg::parser! { pub(crate) rule command_substitution() -> WordPiece = "$(" c:command() ")" { WordPiece::CommandSubstitution(c.to_owned()) } / - "`" backquoted_command() "`" { todo!("UNIMPLEMENTED: backquoted command substitution") } + "`" c:backquoted_command() "`" { WordPiece::CommandSubstitution(c) } pub(crate) rule command() -> &'input str = $(command_piece()*) @@ -421,8 +421,12 @@ peg::parser! { word_piece() {} / ([' ' | '\t'])+ {} - rule backquoted_command() -> () = - "" {} + rule backquoted_command() -> String = + chars:(backquoted_char()*) { chars.into_iter().collect() } + + rule backquoted_char() -> char = + "\\`" { '`' } / + [^'`'] rule arithmetic_expansion() -> WordPiece = "$((" e:$(arithmetic_word()) "))" { WordPiece::ArithmeticExpression(ast::UnexpandedArithmeticExpr { value: e.to_owned() } ) } diff --git a/shell/src/builtin.rs b/shell/src/builtin.rs index 906dc0de..fa6961be 100644 --- a/shell/src/builtin.rs +++ b/shell/src/builtin.rs @@ -115,6 +115,11 @@ pub trait BuiltinCommand: Parser { &self, context: context::CommandExecutionContext<'_>, ) -> Result; + + #[allow(dead_code)] + fn get_long_help() -> String { + Self::command().render_long_help().to_string() + } } #[allow(clippy::module_name_repetitions)] @@ -122,3 +127,22 @@ pub trait BuiltinCommand: Parser { pub trait BuiltinDeclarationCommand: BuiltinCommand { fn set_declarations(&mut self, declarations: Vec); } + +#[allow(clippy::module_name_repetitions)] +#[derive(Clone)] +pub struct BuiltinRegistration { + /// Function to execute the builtin. + pub execute_func: BuiltinCommandExecuteFunc, + + /// Function to retrieve the builtin's detailed help text. + pub help_func: fn() -> String, + + /// Has this registration been disabled? + pub disabled: bool, + + /// Is the builtin classified as "special" by specification? + pub special_builtin: bool, + + /// Is this builtin one that takes specially handled declarations? + pub declaration_builtin: bool, +} diff --git a/shell/src/builtins/enable.rs b/shell/src/builtins/enable.rs new file mode 100644 index 00000000..1a73179c --- /dev/null +++ b/shell/src/builtins/enable.rs @@ -0,0 +1,86 @@ +use clap::Parser; +use itertools::Itertools; +use std::io::Write; + +use crate::builtin::{BuiltinCommand, BuiltinExitCode}; +use crate::error; + +#[derive(Parser)] +pub(crate) struct EnableCommand { + #[arg(short = 'a')] + print_list: bool, + + #[arg(short = 'n')] + disable: bool, + + #[arg(short = 'p')] + print_reusably: bool, + + #[arg(short = 's')] + special_only: bool, + + #[arg(short = 'f')] + shared_object_path: Option, + + #[arg(short = 'd')] + remove_loaded_builtin: bool, + + names: Vec, +} + +#[async_trait::async_trait] +impl BuiltinCommand for EnableCommand { + async fn execute( + &self, + context: crate::context::CommandExecutionContext<'_>, + ) -> Result { + let mut result = BuiltinExitCode::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.builtins.get_mut(name) { + builtin.disabled = self.disable; + } else { + writeln!(context.stderr(), "{name}: not a shell builtin")?; + result = BuiltinExitCode::Custom(1); + } + } + } 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) + } +} diff --git a/shell/src/builtins/help.rs b/shell/src/builtins/help.rs index 3a584589..36ffbe27 100644 --- a/shell/src/builtins/help.rs +++ b/shell/src/builtins/help.rs @@ -1,9 +1,21 @@ -use crate::builtin::{BuiltinCommand, BuiltinExitCode}; +use crate::builtin::{BuiltinCommand, BuiltinExitCode, BuiltinRegistration}; use clap::Parser; +use itertools::Itertools; use std::io::Write; #[derive(Parser)] -pub(crate) struct HelpCommand {} +pub(crate) struct HelpCommand { + #[arg(short = 'd')] + short_description: bool, + + #[arg(short = 'm')] + man_page_style: bool, + + #[arg(short = 's')] + short_usage: bool, + + topic_patterns: Vec, +} const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -13,6 +25,24 @@ impl BuiltinCommand for HelpCommand { &self, context: crate::context::CommandExecutionContext<'_>, ) -> Result { + 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(BuiltinExitCode::Success) + } +} + +impl HelpCommand { + fn display_general_help( + context: &crate::context::CommandExecutionContext<'_>, + ) -> Result<(), crate::error::Error> { + const COLUMN_COUNT: usize = 3; + writeln!(context.stdout(), "brush version {VERSION}\n")?; writeln!( @@ -20,21 +50,56 @@ impl BuiltinCommand for HelpCommand { "The following commands are implemented as shell built-ins:" )?; - let builtin_names = context.shell.get_builtin_names(); - - const COLUMN_COUNT: usize = 3; - - let items_per_column = (builtin_names.len() + COLUMN_COUNT - 1) / COLUMN_COUNT; + let builtins: Vec<(&String, &BuiltinRegistration)> = context + .shell + .builtins + .iter() + .sorted_by_key(|(name, _)| *name) + .collect(); + let items_per_column = (builtins.len() + COLUMN_COUNT - 1) / COLUMN_COUNT; for i in 0..items_per_column { for j in 0..COLUMN_COUNT { - if let Some(name) = builtin_names.get(i + j * items_per_column) { - write!(context.stdout(), " {name:<20}")?; // adjust 20 to the desired column width + 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(BuiltinExitCode::Success) + Ok(()) + } + + fn display_help_for_topic_pattern( + &self, + context: &crate::context::CommandExecutionContext<'_>, + topic_pattern: &str, + ) -> Result<(), crate::error::Error> { + let pattern = crate::patterns::Pattern::from(topic_pattern); + + let mut found_count = 0; + for (builtin_name, builtin_registration) in &context.shell.builtins { + if pattern.exactly_matches(builtin_name.as_str(), false)? { + self.display_help_for_builtin(context, builtin_registration)?; + found_count += 1; + } + } + + if found_count == 0 { + writeln!(context.stderr(), "No help topics match '{topic_pattern}'")?; + } + + Ok(()) + } + + #[allow(clippy::unused_self)] + fn display_help_for_builtin( + &self, + context: &crate::context::CommandExecutionContext<'_>, + registration: &BuiltinRegistration, + ) -> Result<(), crate::error::Error> { + writeln!(context.stdout(), "{}", (registration.help_func)())?; + Ok(()) } } diff --git a/shell/src/builtins/jobs.rs b/shell/src/builtins/jobs.rs index 69e8fe13..a39d606d 100644 --- a/shell/src/builtins/jobs.rs +++ b/shell/src/builtins/jobs.rs @@ -51,22 +51,7 @@ impl BuiltinCommand for JobsCommand { } for job in &context.shell.jobs.background_jobs { - let annotation = if job.is_current() { - "+" - } else if job.is_prev() { - "-" - } else { - "" - }; - - writeln!( - context.stdout(), - "[{}]{:3}{:24}{}", - job.id, - annotation, - job.state.to_string(), - job.command_line - )?; + writeln!(context.stdout(), "{job}")?; } Ok(BuiltinExitCode::Success) diff --git a/shell/src/builtins/mod.rs b/shell/src/builtins/mod.rs index 68211269..0c187476 100644 --- a/shell/src/builtins/mod.rs +++ b/shell/src/builtins/mod.rs @@ -1,10 +1,9 @@ use futures::future::BoxFuture; -use itertools::Itertools; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::io::Write; use crate::builtin::{ - self, BuiltinCommand, BuiltinCommandExecuteFunc, BuiltinDeclarationCommand, BuiltinResult, + self, BuiltinCommand, BuiltinDeclarationCommand, BuiltinRegistration, BuiltinResult, }; use crate::commands::CommandArg; use crate::context; @@ -21,6 +20,7 @@ mod declare; mod dirs; mod dot; mod echo; +mod enable; mod eval; #[cfg(unix)] mod exec; @@ -50,6 +50,52 @@ mod umask; mod unalias; mod unimp; mod unset; +mod wait; + +fn builtin() -> BuiltinRegistration { + BuiltinRegistration { + execute_func: exec_builtin::, + help_func: get_builtin_help::, + disabled: false, + special_builtin: false, + declaration_builtin: false, + } +} + +fn special_builtin() -> BuiltinRegistration { + BuiltinRegistration { + execute_func: exec_builtin::, + help_func: get_builtin_help::, + disabled: false, + special_builtin: true, + declaration_builtin: false, + } +} + +fn decl_builtin() -> BuiltinRegistration { + BuiltinRegistration { + execute_func: exec_declaration_builtin::, + help_func: get_builtin_help::, + disabled: false, + special_builtin: false, + declaration_builtin: true, + } +} + +#[allow(dead_code)] +fn special_decl_builtin() -> BuiltinRegistration { + BuiltinRegistration { + execute_func: exec_declaration_builtin::, + help_func: get_builtin_help::, + disabled: false, + special_builtin: true, + declaration_builtin: true, + } +} + +fn get_builtin_help() -> String { + T::get_long_help() +} fn exec_builtin( context: context::CommandExecutionContext<'_>, @@ -124,34 +170,12 @@ async fn exec_declaration_builtin_impl( }) } -lazy_static::lazy_static! { - pub(crate) static ref SPECIAL_BUILTINS: HashMap<&'static str, BuiltinCommandExecuteFunc> = get_special_builtins(); - pub(crate) static ref BUILTINS: HashMap<&'static str, BuiltinCommandExecuteFunc> = get_builtins(true); - pub(crate) static ref POSIX_ONLY_BUILTINS: HashMap<&'static str, BuiltinCommandExecuteFunc> = get_builtins(false); - pub(crate) static ref DECLARATION_BUILTINS: HashSet<&'static str> = get_declaration_builtin_names(); -} - -pub(crate) fn get_all_builtin_names() -> Vec { - SPECIAL_BUILTINS - .iter() - .chain(BUILTINS.iter()) - .map(|(name, _)| (*name).to_owned()) - .sorted() - .collect::>() -} +#[allow(clippy::too_many_lines)] +pub(crate) fn get_default_builtins( + options: &crate::CreateOptions, +) -> HashMap { + let mut m = HashMap::::new(); -fn get_declaration_builtin_names() -> HashSet<&'static str> { - let mut s = HashSet::new(); - s.insert("alias"); - s.insert("declare"); - s.insert("export"); - s.insert("local"); - s.insert("readonly"); - s.insert("typeset"); - s -} - -fn get_special_builtins() -> HashMap<&'static str, BuiltinCommandExecuteFunc> { // // POSIX special builtins // @@ -159,87 +183,97 @@ fn get_special_builtins() -> HashMap<&'static str, BuiltinCommandExecuteFunc> { // should be a special built-in. // - let mut m = HashMap::<&'static str, BuiltinCommandExecuteFunc>::new(); - - m.insert("break", exec_builtin::); - m.insert(":", exec_builtin::); - m.insert("continue", exec_builtin::); - m.insert(".", exec_builtin::); - m.insert("eval", exec_builtin::); + m.insert("break".into(), special_builtin::()); + m.insert(":".into(), special_builtin::()); + m.insert( + "continue".into(), + special_builtin::(), + ); + m.insert(".".into(), special_builtin::()); + m.insert("eval".into(), special_builtin::()); #[cfg(unix)] - m.insert("exec", exec_builtin::); - m.insert("exit", exec_builtin::); - m.insert("export", exec_builtin::); // TODO: should be exec_declaration_builtin - m.insert("readonly", exec_builtin::); // TODO: should be exec_declaration_builtin - m.insert("return", exec_builtin::); - m.insert("set", exec_builtin::); - m.insert("shift", exec_builtin::); - m.insert("times", exec_builtin::); - m.insert("trap", exec_builtin::); - m.insert("unset", exec_builtin::); + m.insert("exec".into(), special_builtin::()); + m.insert("exit".into(), special_builtin::()); + m.insert("export".into(), special_builtin::()); // TODO: should be exec_declaration_builtin + m.insert("return".into(), special_builtin::()); + m.insert("set".into(), special_builtin::()); + m.insert("shift".into(), special_builtin::()); + m.insert("trap".into(), special_builtin::()); + m.insert("unset".into(), special_builtin::()); - m -} + // TODO: Unimplemented special builtins + m.insert( + "readonly".into(), + special_builtin::(), + ); // TODO: should be exec_declaration_builtin + m.insert( + "times".into(), + special_builtin::(), + ); + + // + // Non-special builtins + // -fn get_builtins(include_extended: bool) -> HashMap<&'static str, BuiltinCommandExecuteFunc> { - let mut m = HashMap::<&'static str, BuiltinCommandExecuteFunc>::new(); - - m.insert("alias", exec_builtin::); // TODO: should be exec_declaration_builtin - m.insert("bg", exec_builtin::); - m.insert("cd", exec_builtin::); - m.insert("command", exec_builtin::); - m.insert("false", exec_builtin::); - m.insert("fc", exec_builtin::); - m.insert("fg", exec_builtin::); - m.insert("getopts", exec_builtin::); - m.insert("hash", exec_builtin::); - m.insert("help", exec_builtin::); - m.insert("jobs", exec_builtin::); + m.insert("alias".into(), builtin::()); // TODO: should be exec_declaration_builtin + m.insert("bg".into(), builtin::()); + m.insert("cd".into(), builtin::()); + m.insert("false".into(), builtin::()); + m.insert("fg".into(), builtin::()); + m.insert("getopts".into(), builtin::()); + m.insert("help".into(), builtin::()); + m.insert("jobs".into(), builtin::()); #[cfg(unix)] - m.insert("kill", exec_builtin::); - m.insert("newgrp", exec_builtin::); - m.insert("pwd", exec_builtin::); - m.insert("read", exec_builtin::); - m.insert("true", exec_builtin::); - m.insert("type", exec_builtin::); - m.insert("ulimit", exec_builtin::); - m.insert("umask", exec_builtin::); - m.insert("unalias", exec_builtin::); - m.insert("wait", exec_builtin::); + m.insert("kill".into(), builtin::()); + m.insert("pwd".into(), builtin::()); + m.insert("read".into(), builtin::()); + m.insert("true".into(), builtin::()); + m.insert("type".into(), builtin::()); + m.insert("umask".into(), builtin::()); + m.insert("unalias".into(), builtin::()); + m.insert("wait".into(), builtin::()); + + // TODO: Unimplemented non-special builtins + m.insert("command".into(), builtin::()); + m.insert("fc".into(), builtin::()); + m.insert("hash".into(), builtin::()); + m.insert("ulimit".into(), builtin::()); // TODO: does this belong? - m.insert("local", exec_declaration_builtin::); - - if include_extended { - m.insert("bind", exec_builtin::); - m.insert("builtin", exec_builtin::); - m.insert("caller", exec_builtin::); - m.insert( - "declare", - exec_declaration_builtin::, - ); - m.insert("echo", exec_builtin::); - m.insert("enable", exec_builtin::); - m.insert("let", exec_builtin::); - m.insert("logout", exec_builtin::); - m.insert("mapfile", exec_builtin::); - m.insert("printf", exec_builtin::); - m.insert("readarray", exec_builtin::); - m.insert("shopt", exec_builtin::); - m.insert("source", exec_builtin::); - m.insert("test", exec_builtin::); - m.insert("[", exec_builtin::); - m.insert("typeset", exec_builtin::); + m.insert("local".into(), decl_builtin::()); + + if !options.sh_mode { + m.insert("declare".into(), decl_builtin::()); + m.insert("echo".into(), builtin::()); + m.insert("enable".into(), builtin::()); + m.insert("printf".into(), builtin::()); + m.insert("shopt".into(), builtin::()); + m.insert("source".into(), special_builtin::()); + m.insert("test".into(), builtin::()); + m.insert("[".into(), builtin::()); + m.insert("typeset".into(), builtin::()); // Completion builtins - m.insert("complete", exec_builtin::); - m.insert("compgen", exec_builtin::); - m.insert("compopt", exec_builtin::); + m.insert("complete".into(), builtin::()); + m.insert("compgen".into(), builtin::()); + m.insert("compopt".into(), builtin::()); // Dir stack builtins - m.insert("dirs", exec_builtin::); - m.insert("popd", exec_builtin::); - m.insert("pushd", exec_builtin::); + m.insert("dirs".into(), builtin::()); + m.insert("popd".into(), builtin::()); + m.insert("pushd".into(), builtin::()); + + // TODO: Unimplemented builtins + m.insert("bind".into(), builtin::()); + m.insert("builtin".into(), builtin::()); + m.insert("caller".into(), builtin::()); + m.insert("disown".into(), builtin::()); + m.insert("history".into(), builtin::()); + m.insert("let".into(), builtin::()); + m.insert("logout".into(), builtin::()); + m.insert("mapfile".into(), builtin::()); + m.insert("readarray".into(), builtin::()); + m.insert("suspend".into(), builtin::()); } m diff --git a/shell/src/builtins/typ.rs b/shell/src/builtins/typ.rs index 469b974d..17f5e3f9 100644 --- a/shell/src/builtins/typ.rs +++ b/shell/src/builtins/typ.rs @@ -147,9 +147,7 @@ impl TypeCommand { } // Check for builtins. - if crate::builtins::SPECIAL_BUILTINS.contains_key(name) - || crate::builtins::BUILTINS.contains_key(name) - { + if shell.builtins.get(name).is_some_and(|b| !b.disabled) { types.push(ResolvedType::Builtin); } } diff --git a/shell/src/builtins/wait.rs b/shell/src/builtins/wait.rs new file mode 100644 index 00000000..e248bdf1 --- /dev/null +++ b/shell/src/builtins/wait.rs @@ -0,0 +1,43 @@ +use clap::Parser; + +use crate::builtin::{BuiltinCommand, BuiltinExitCode}; +use crate::error; + +#[derive(Parser)] +pub(crate) struct WaitCommand { + #[arg(short = 'f')] + wait_for_terminate: bool, + + #[arg(short = 'n')] + wait_for_first_or_next: bool, + + #[arg(short = 'p')] + variable_to_receive_id: Option, + + job_specs: Vec, +} + +#[async_trait::async_trait] +impl BuiltinCommand for WaitCommand { + async fn execute( + &self, + context: crate::context::CommandExecutionContext<'_>, + ) -> Result { + 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"); + } + if !self.job_specs.is_empty() { + return error::unimp("wait with job specs"); + } + + context.shell.jobs.wait_all().await?; + + Ok(BuiltinExitCode::Success) + } +} diff --git a/shell/src/commands.rs b/shell/src/commands.rs index db7a78d5..d0763b7e 100644 --- a/shell/src/commands.rs +++ b/shell/src/commands.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use parser::ast; #[derive(Clone, Debug)] -pub(crate) enum CommandArg { +pub enum CommandArg { String(String), Assignment(ast::Assignment), } diff --git a/shell/src/completion.rs b/shell/src/completion.rs index c6a3856c..fbac6900 100644 --- a/shell/src/completion.rs +++ b/shell/src/completion.rs @@ -192,8 +192,9 @@ impl CompletionSpec { } CompleteAction::Binding => tracing::debug!("UNIMPLEMENTED: complete -A binding"), CompleteAction::Builtin => { - let mut builtin_names = shell.get_builtin_names(); - candidates.append(&mut builtin_names); + for name in shell.builtins.keys() { + candidates.push(name.to_owned()); + } } CompleteAction::Command => { let mut command_completions = get_command_completions(shell, context); @@ -208,10 +209,11 @@ impl CompletionSpec { tracing::debug!("UNIMPLEMENTED: complete -A disabled"); } CompleteAction::Enabled => { - // For now, all builtins are enabled - tracing::debug!("UNIMPLEMENTED: complete -A enabled"); - let mut builtin_names = shell.get_builtin_names(); - candidates.append(&mut builtin_names); + for (name, registration) in &shell.builtins { + if !registration.disabled { + candidates.push(name.to_owned()); + } + } } CompleteAction::Export => { for (key, value) in shell.env.iter() { diff --git a/shell/src/error.rs b/shell/src/error.rs index 34fddf0a..45b2713a 100644 --- a/shell/src/error.rs +++ b/shell/src/error.rs @@ -97,6 +97,9 @@ pub enum Error { #[error("{0}")] TestCommandParseError(#[from] parser::TestCommandParseError), + + #[error("threading error")] + ThreadingError(#[from] tokio::task::JoinError), } pub(crate) fn unimp(msg: &'static str) -> Result { diff --git a/shell/src/interp.rs b/shell/src/interp.rs index 63617af7..af3edbdf 100644 --- a/shell/src/interp.rs +++ b/shell/src/interp.rs @@ -19,7 +19,7 @@ use crate::shell::Shell; use crate::variables::{ ArrayLiteral, ShellValue, ShellValueLiteral, ShellValueUnsetType, ShellVariable, }; -use crate::{builtin, builtins, context, error, expansion, extendedtests, jobs, openfiles}; +use crate::{builtin, context, error, expansion, extendedtests, jobs, openfiles}; #[derive(Debug, Default)] pub struct ExecutionResult { @@ -154,8 +154,14 @@ impl Execute for ast::CompoundList { jobs::JobState::Running, )); let job_id = job.id; + let job_annotation = job.get_annotation(); + let job_pid = job + .get_pid()? + .map_or_else(|| String::from(""), |pid| pid.to_string()); - writeln!(shell.stderr(), "[{job_id}]+\t")?; + if shell.options.interactive { + writeln!(shell.stderr(), "[{job_id}]{job_annotation}\t{job_pid}")?; + } } else { result = ao_list.execute(shell, params).await?; } @@ -847,7 +853,12 @@ impl ExecuteInPipeline for ast::SimpleCommand { // Check if we're going to be invoking a special declaration builtin. That will // change how we parse and process args. - if builtins::DECLARATION_BUILTINS.contains(next_args[0].as_str()) { + if context + .shell + .builtins + .get(next_args[0].as_str()) + .is_some_and(|r| r.declaration_builtin) + { invoking_declaration_builtin = true; } } @@ -890,16 +901,22 @@ impl ExecuteInPipeline for ast::SimpleCommand { }; let execution_result = if !cmd_context.command_name.contains('/') { - let normal_builtin_lookup = if cmd_context.shell.options.sh_mode { - |s: &str| builtins::POSIX_ONLY_BUILTINS.get(s) - } else { - |s: &str| builtins::BUILTINS.get(s) - }; + let mut builtin = cmd_context + .shell + .builtins + .get(&cmd_context.command_name) + .cloned(); + + // Ignore the builtin if it's marked as disabled. + if builtin.as_ref().is_some_and(|b| b.disabled) { + builtin = None; + } - if let Some(builtin) = - builtins::SPECIAL_BUILTINS.get(cmd_context.command_name.as_str()) + if builtin + .as_ref() + .is_some_and(|r| !r.disabled && r.special_builtin) { - execute_builtin_command(*builtin, cmd_context, args).await + execute_builtin_command(&builtin.unwrap(), cmd_context, args).await } else if let Some(func_def) = cmd_context .shell .funcs @@ -907,10 +924,8 @@ impl ExecuteInPipeline for ast::SimpleCommand { { // Strip the function name off args. invoke_shell_function(func_def.clone(), cmd_context, &args[1..]).await - } else if let Some(builtin) = - normal_builtin_lookup(cmd_context.command_name.as_str()) - { - execute_builtin_command(*builtin, cmd_context, args).await + } else if let Some(builtin) = builtin { + execute_builtin_command(&builtin, cmd_context, args).await } else { // Strip the command name off args. execute_external_command(cmd_context, &args[1..]).await @@ -1264,11 +1279,11 @@ async fn execute_external_command( } async fn execute_builtin_command( - builtin: builtin::BuiltinCommandExecuteFunc, + builtin: &builtin::BuiltinRegistration, context: context::CommandExecutionContext<'_>, args: Vec, ) -> Result { - let exit_code = match builtin(context, args).await { + let exit_code = match (builtin.execute_func)(context, args).await { Ok(builtin_result) => match builtin_result.exit_code { builtin::BuiltinExitCode::Success => 0, builtin::BuiltinExitCode::InvalidUsage => 2, diff --git a/shell/src/jobs.rs b/shell/src/jobs.rs index a43abad4..a1e13490 100644 --- a/shell/src/jobs.rs +++ b/shell/src/jobs.rs @@ -1,10 +1,13 @@ use std::collections::VecDeque; use std::fmt::Display; +use futures::FutureExt; + use crate::error; use crate::ExecutionResult; pub(crate) type JobJoinHandle = tokio::task::JoinHandle>; +pub(crate) type JobResult = (Job, Result); #[derive(Default)] pub struct JobManager { @@ -33,8 +36,49 @@ impl JobManager { self.background_jobs.last_mut() } + #[allow(clippy::unused_self)] pub fn resolve_job_spec(&self, _job_spec: &str) -> Option<&mut Job> { - todo!("resolve_job_spec") + tracing::warn!("resolve_job_spec is not implemented"); + None + } + + pub async fn wait_all(&mut self) -> Result, error::Error> { + for job in &mut self.background_jobs { + job.wait().await?; + } + + Ok(self.sweep_completed_jobs()) + } + + pub fn poll(&mut self) -> Result, error::Error> { + let mut results = vec![]; + + let mut i = 0; + while i != self.background_jobs.len() { + if let Some(result) = self.background_jobs[i].poll_done()? { + let job = self.background_jobs.remove(i); + results.push((job, result)); + } else { + i += 1; + } + } + + Ok(results) + } + + fn sweep_completed_jobs(&mut self) -> Vec { + let mut completed_jobs = vec![]; + + let mut i = 0; + while i != self.background_jobs.len() { + if self.background_jobs[i].join_handles.is_empty() { + completed_jobs.push(self.background_jobs.remove(i)); + } else { + i += 1; + } + } + + completed_jobs } } @@ -43,6 +87,7 @@ pub enum JobState { Unknown, Running, Stopped, + Done, } impl Display for JobState { @@ -51,17 +96,28 @@ impl Display for JobState { JobState::Unknown => write!(f, "Unknown"), JobState::Running => write!(f, "Running"), JobState::Stopped => write!(f, "Stopped"), + JobState::Done => write!(f, "Done"), } } } -#[allow(dead_code)] -enum JobAnnotation { +#[derive(Clone)] +pub enum JobAnnotation { None, Current, Previous, } +impl Display for JobAnnotation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JobAnnotation::None => write!(f, ""), + JobAnnotation::Current => write!(f, "+"), + JobAnnotation::Previous => write!(f, "-"), + } + } +} + pub struct Job { join_handles: VecDeque, #[allow(dead_code)] @@ -73,6 +129,19 @@ pub struct Job { pub state: JobState, } +impl Display for Job { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[{}]{:3}{}\t{}", + self.id, + self.annotation.to_string(), + self.state, + self.command_line + ) + } +} + impl Job { pub(crate) fn new( join_handles: VecDeque, @@ -90,6 +159,10 @@ impl Job { } } + pub fn get_annotation(&self) -> JobAnnotation { + self.annotation.clone() + } + pub fn is_current(&self) -> bool { matches!(self.annotation, JobAnnotation::Current) } @@ -98,6 +171,31 @@ impl Job { matches!(self.annotation, JobAnnotation::Previous) } + pub fn poll_done( + &mut self, + ) -> Result>, error::Error> { + let mut result: Option> = None; + + while !self.join_handles.is_empty() { + let join_handle = &mut self.join_handles[0]; + match join_handle.now_or_never() { + Some(Ok(r)) => { + self.join_handles.remove(0); + result = Some(r); + } + Some(Err(e)) => { + self.join_handles.remove(0); + return Err(error::Error::ThreadingError(e)); + } + None => return Ok(None), + } + } + + self.state = JobState::Done; + + Ok(result) + } + pub async fn wait(&mut self) -> Result { let mut result = ExecutionResult::success(); @@ -159,12 +257,11 @@ impl Job { error::unimp("kill job") } - #[cfg(unix)] - fn get_pid(&self) -> Result, error::Error> { + #[allow(clippy::unnecessary_wraps)] + pub fn get_pid(&self) -> Result, error::Error> { if self.pids.is_empty() { - error::unimp("get pid for job") - } else if self.pids.len() > 1 { - error::unimp("get pid for job with multiple pids") + tracing::debug!("UNIMPLEMENTED: get pid for job"); + Ok(None) } else { Ok(Some(self.pids[0])) } diff --git a/shell/src/shell.rs b/shell/src/shell.rs index cd1a96d4..2e17d8e0 100644 --- a/shell/src/shell.rs +++ b/shell/src/shell.rs @@ -7,16 +7,12 @@ use std::sync::Arc; use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment}; use crate::interp::{self, Execute, ExecutionParameters, ExecutionResult}; -use crate::jobs; -use crate::openfiles; use crate::options::RuntimeOptions; -use crate::prompt::expand_prompt; use crate::variables::{self, ShellValue, ShellVariable}; -use crate::{builtins, error}; -use crate::{commands, patterns}; -use crate::{completion, users}; -use crate::{context, env}; -use crate::{expansion, keywords}; +use crate::{ + builtin, builtins, commands, completion, context, env, error, expansion, jobs, keywords, + openfiles, patterns, prompt, users, +}; pub struct Shell { // @@ -61,6 +57,9 @@ pub struct Shell { // Completion configuration. pub completion_config: completion::CompletionConfig, + + // Builtins. + pub builtins: HashMap, } impl Clone for Shell { @@ -83,6 +82,7 @@ impl Clone for Shell { directory_stack: self.directory_stack.clone(), current_line_number: self.current_line_number, completion_config: self.completion_config.clone(), + builtins: self.builtins.clone(), depth: self.depth + 1, } } @@ -130,6 +130,7 @@ impl Shell { directory_stack: vec![], current_line_number: 0, completion_config: completion::CompletionConfig::default(), + builtins: builtins::get_default_builtins(options), depth: 0, }; @@ -557,7 +558,7 @@ impl Shell { let ps1 = self.parameter_or_default("PS1", DEFAULT_PROMPT); // Expand it. - let formatted_prompt = expand_prompt(self, ps1.as_ref())?; + let formatted_prompt = prompt::expand_prompt(self, ps1.as_ref())?; // NOTE: We're having difficulty with xterm escape sequences going through rustyline; // so we strip them here. @@ -813,11 +814,6 @@ impl Shell { writeln!(self.stderr(), "{prefix}{}", command.as_ref()) } - #[allow(clippy::unused_self)] - pub fn get_builtin_names(&self) -> Vec { - builtins::get_all_builtin_names() - } - pub fn get_keywords(&self) -> Vec { if self.options.sh_mode { keywords::SH_MODE_KEYWORDS.iter().cloned().collect() @@ -825,4 +821,16 @@ impl Shell { keywords::KEYWORDS.iter().cloned().collect() } } + + pub fn check_for_completed_jobs(&mut self) -> Result<(), error::Error> { + let results = self.jobs.poll()?; + + if self.options.interactive { + for (job, _result) in results { + writeln!(self.stderr(), "{job}")?; + } + } + + Ok(()) + } }