Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
//! Types for brush command-line parsing.
|
||||
|
||||
use clap::{Parser, builder::styling};
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{events, productinfo};
|
||||
|
||||
const SHORT_DESCRIPTION: &str = "Bo[u]rn[e] RUsty SHell 🦀 (https://brush.sh)";
|
||||
|
||||
const LONG_DESCRIPTION: &str = r"brush is a bash-compatible, Rust-implemented, POSIX-style shell.
|
||||
|
||||
brush is distributed under the terms of the MIT license. If you encounter any issues or discrepancies in behavior from bash, please report them at https://github.com/reubeno/brush.
|
||||
|
||||
For more information, visit https://brush.sh.";
|
||||
|
||||
// nash: rebranded usage/version strings for the Nucleic agent shell fork.
|
||||
const USAGE: &str = color_print::cstr!(
|
||||
"<bold>nash</bold> <italics>[OPTIONS]</italics>... <italics>[SCRIPT_PATH [SCRIPT_ARGS]...]</italics>"
|
||||
);
|
||||
|
||||
const VERSION: &str = const_format::concatcp!(
|
||||
productinfo::PRODUCT_VERSION,
|
||||
" (Nucleic agent shell, brush fork; ",
|
||||
productinfo::PRODUCT_GIT_VERSION,
|
||||
")"
|
||||
);
|
||||
|
||||
const HEADING_STANDARD_OPTIONS: &str = "Standard shell options";
|
||||
|
||||
const HEADING_CONFIG_OPTIONS: &str = "Configuration options";
|
||||
|
||||
const HEADING_UI_OPTIONS: &str = "User interface options";
|
||||
|
||||
const HEADING_EXPERIMENTAL_OPTIONS: &str = "*Experimental* options (unstable)";
|
||||
|
||||
/// Identifies the input backend to use for the shell.
|
||||
#[derive(Clone, Copy, clap::ValueEnum)]
|
||||
pub enum InputBackendType {
|
||||
/// Richest input backend, based on reedline.
|
||||
Reedline,
|
||||
/// Basic input backend that provides minimal completion support for testing.
|
||||
Basic,
|
||||
/// Most minimal input backend.
|
||||
Minimal,
|
||||
}
|
||||
|
||||
/// Parsed command-line arguments for the brush shell.
|
||||
#[derive(Clone, Parser)]
|
||||
#[clap(name = productinfo::PRODUCT_NAME,
|
||||
version = VERSION,
|
||||
about = SHORT_DESCRIPTION,
|
||||
long_about = LONG_DESCRIPTION,
|
||||
author,
|
||||
override_usage = USAGE,
|
||||
disable_help_flag = true,
|
||||
disable_version_flag = true,
|
||||
styles = brush_help_styles())]
|
||||
pub struct CommandLineArgs {
|
||||
/// Display usage information.
|
||||
#[clap(long = "help", action = clap::ArgAction::HelpShort)]
|
||||
pub help: Option<bool>,
|
||||
|
||||
/// Display shell version.
|
||||
#[clap(long = "version", action = clap::ArgAction::Version)]
|
||||
pub version: Option<bool>,
|
||||
|
||||
/// Path to TOML-based `brush` config file (overrides default location).
|
||||
#[clap(long = "config", value_name = "FILE", help_heading = HEADING_CONFIG_OPTIONS)]
|
||||
pub config_file: Option<PathBuf>,
|
||||
|
||||
/// Disable loading of TOML-based `brush` config file.
|
||||
#[clap(long = "no-config", help_heading = HEADING_CONFIG_OPTIONS)]
|
||||
pub no_config: bool,
|
||||
|
||||
/// Enable `noclobber` shell option.
|
||||
#[arg(short = 'C', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disallow_overwriting_regular_files_via_output_redirection: bool,
|
||||
|
||||
/// Execute the provided command and then exit.
|
||||
#[arg(short = 'c', value_name = "COMMAND", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Enable error-on-exit behavior.
|
||||
#[clap(short = 'e', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub exit_on_nonzero_command_exit: bool,
|
||||
|
||||
/// Disable pathname expansion (also known as filename globbing).
|
||||
#[clap(short = 'f', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disable_pathname_expansion: bool,
|
||||
|
||||
/// Run in interactive mode.
|
||||
#[clap(short = 'i', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub interactive: bool,
|
||||
|
||||
/// Inherit the specified file descriptors injected by the parent process.
|
||||
#[clap(long = "inherit-fd", value_name = "FD", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub inherited_fds: Vec<i32>,
|
||||
|
||||
/// Make shell act as if it had been invoked as a login shell.
|
||||
#[clap(short = 'l', long = "login", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub login: bool,
|
||||
|
||||
/// Do not execute commands.
|
||||
#[clap(short = 'n', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub do_not_execute_commands: bool,
|
||||
|
||||
/// Don't use readline for input.
|
||||
#[clap(long = "noediting", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_editing: bool,
|
||||
|
||||
/// Don't process any profile/login files (`/etc/profile`, `~/.bash_profile`, `~/.bash_login`,
|
||||
/// `~/.profile`).
|
||||
#[clap(long = "noprofile", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_profile: bool,
|
||||
|
||||
/// Don't process "rc" files if the shell is interactive (e.g., `~/.bashrc`, `~/.brushrc`).
|
||||
#[clap(long = "norc", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub no_rc: bool,
|
||||
|
||||
/// Don't inherit environment variables from the calling process.
|
||||
#[clap(long = "noenv", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub do_not_inherit_env: bool,
|
||||
|
||||
/// Enable option (`set -o` option).
|
||||
#[clap(short = 'o', value_name = "OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub enabled_options: Vec<String>,
|
||||
|
||||
/// Disable option (`set -o` option).
|
||||
#[clap(long = "+o", value_name = "OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disabled_options: Vec<String>,
|
||||
|
||||
/// Enable `shopt` option.
|
||||
#[clap(short = 'O', value_name = "SHOPT_OPTION", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub enabled_shopt_options: Vec<String>,
|
||||
|
||||
/// Disable `shopt` option.
|
||||
#[clap(long = "+O", value_name = "SHOPT_OPTION", hide = true, help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub disabled_shopt_options: Vec<String>,
|
||||
|
||||
/// Disable non-POSIX extensions.
|
||||
#[clap(long = "posix", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub posix: bool,
|
||||
|
||||
/// Path to the rc file to load in interactive shells (instead of `bash.bashrc` and
|
||||
/// `~/.bashrc`).
|
||||
#[clap(long = "rcfile", alias = "init-file", value_name = "FILE", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub rc_file: Option<PathBuf>,
|
||||
|
||||
/// Read commands from standard input.
|
||||
#[clap(short = 's', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub read_commands_from_stdin: bool,
|
||||
|
||||
/// Run in `sh` compatibility mode, as if run as `/bin/sh`.
|
||||
#[clap(long = "sh")]
|
||||
pub sh_mode: bool,
|
||||
|
||||
/// Run only one command and then exit.
|
||||
#[clap(short = 't', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub exit_after_one_command: bool,
|
||||
|
||||
/// Treat expansion of an unset variable as an error.
|
||||
#[clap(short = 'u', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub treat_unset_variables_as_error: bool,
|
||||
|
||||
/// Print input when it's processed.
|
||||
#[clap(short = 'v', long = "verbose", help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Print commands as they execute.
|
||||
#[clap(short = 'x', help_heading = HEADING_STANDARD_OPTIONS)]
|
||||
pub print_commands_and_arguments: bool,
|
||||
|
||||
/// Enable xtrace and configure for the given output file.
|
||||
#[clap(long = "xtrace-file", value_name = "FILE", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub xtrace_file_path: Option<PathBuf>,
|
||||
|
||||
/// Disable bracketed paste.
|
||||
#[clap(long = "disable-bracketed-paste", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub disable_bracketed_paste: bool,
|
||||
|
||||
/// Disable colorized output.
|
||||
#[clap(long = "disable-color", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub disable_color: bool,
|
||||
|
||||
/// Enable syntax highlighting in input.
|
||||
#[clap(long = "enable-highlighting", help_heading = HEADING_UI_OPTIONS, default_value_t = crate::entry::DEFAULT_ENABLE_HIGHLIGHTING)]
|
||||
pub enable_highlighting: bool,
|
||||
|
||||
/// Enable experimental parser (not ready for use).
|
||||
#[cfg(feature = "experimental-parser")]
|
||||
#[clap(long = "experimental-parser", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub experimental_parser: bool,
|
||||
|
||||
/// Enable terminal integration (**experimental**).
|
||||
#[clap(long = "enable-terminal-integration", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub terminal_shell_integration: bool,
|
||||
|
||||
/// Enable zsh-style preexec/precmd hooks (**experimental**).
|
||||
#[clap(long = "enable-zsh-hooks", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub zsh_style_hooks: bool,
|
||||
|
||||
/// Input backend.
|
||||
#[clap(long = "input-backend", value_name = "BACKEND", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub input_backend: Option<InputBackendType>,
|
||||
|
||||
/// Load state from the given file; the saved state should be in JSON format
|
||||
/// and overrides any non-UI command-line options provided.
|
||||
#[cfg(feature = "experimental-load")]
|
||||
#[clap(long = "load", value_name = "FILE", help_heading = HEADING_EXPERIMENTAL_OPTIONS)]
|
||||
pub load_file: Option<PathBuf>,
|
||||
|
||||
/// Enable debug logging for classes of tracing events.
|
||||
#[clap(long = "debug", alias = "log-enable", value_name = "EVENT", help_heading = HEADING_UI_OPTIONS)]
|
||||
pub enabled_debug_events: Vec<events::TraceEvent>,
|
||||
|
||||
/// Disable logging for classes of tracing events (takes same event types as `--debug`).
|
||||
#[clap(
|
||||
long = "disable-event",
|
||||
alias = "log-disable",
|
||||
value_name = "EVENT",
|
||||
hide_possible_values = true,
|
||||
help_heading = HEADING_UI_OPTIONS
|
||||
)]
|
||||
pub disabled_events: Vec<events::TraceEvent>,
|
||||
|
||||
/// Path and arguments for script to execute (optional).
|
||||
#[clap(
|
||||
trailing_var_arg = true,
|
||||
allow_hyphen_values = false,
|
||||
value_name = "SCRIPT_PATH [SCRIPT_ARGS]..."
|
||||
)]
|
||||
pub script_args: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandLineArgs {
|
||||
/// Returns a `CommandLineArgs` with all clap-defined default values.
|
||||
///
|
||||
/// This is useful for detecting which CLI arguments were explicitly provided
|
||||
/// vs. which retained their default values (e.g., for config file merging).
|
||||
#[must_use]
|
||||
#[allow(
|
||||
clippy::missing_panics_doc,
|
||||
reason = "parsing defaults should not panic"
|
||||
)]
|
||||
pub fn default_values() -> Self {
|
||||
use clap::Parser;
|
||||
// Parse with just the program name to get all defaults.
|
||||
// This won't fail because all arguments have defaults or are optional.
|
||||
#[allow(clippy::expect_used)]
|
||||
Self::try_parse_from(["brush"]).expect("parsing defaults should never fail")
|
||||
}
|
||||
|
||||
/// Returns whether or not the arguments indicate that the shell should run in interactive mode.
|
||||
pub fn is_interactive(&self) -> bool {
|
||||
// If -i is provided, then that overrides any further consideration; it forces
|
||||
// interactive mode.
|
||||
if self.interactive {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If -c or non-option arguments are provided, then we're not in interactive mode.
|
||||
if self.command.is_some() || !self.script_args.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If *either* stdin or stderr is not a terminal, then we're not in interactive mode.
|
||||
if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// In all other cases, we assume interactive mode.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns clap styling to be used for command-line help.
|
||||
#[doc(hidden)]
|
||||
fn brush_help_styles() -> clap::builder::Styles {
|
||||
styling::Styles::styled()
|
||||
.header(
|
||||
styling::AnsiColor::Yellow.on_default()
|
||||
| styling::Effects::BOLD
|
||||
| styling::Effects::UNDERLINE,
|
||||
)
|
||||
.usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
|
||||
.literal(styling::AnsiColor::Magenta.on_default() | styling::Effects::BOLD)
|
||||
.placeholder(styling::AnsiColor::Cyan.on_default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_values() {
|
||||
let args = CommandLineArgs::default_values();
|
||||
// Verify some basic defaults
|
||||
assert!(!args.interactive);
|
||||
assert!(!args.login);
|
||||
assert!(args.command.is_none());
|
||||
assert!(args.script_args.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
use brush_core::{ExecutionResult, sys};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::io::Write;
|
||||
|
||||
use crate::events;
|
||||
|
||||
/// Extension trait for adding brush-specific built-in commands to a shell builder.
|
||||
pub(crate) trait ShellBuilderBrushBuiltinExt {
|
||||
/// Add brush-specific builtins to a shell being built.
|
||||
#[must_use]
|
||||
fn brush_builtins(self) -> Self;
|
||||
}
|
||||
|
||||
impl<SE: brush_core::extensions::ShellExtensions, S: brush_core::ShellBuilderState>
|
||||
ShellBuilderBrushBuiltinExt for brush_core::ShellBuilder<SE, S>
|
||||
{
|
||||
fn brush_builtins(self) -> Self {
|
||||
// For compatibility with previous releases, we register the command under both
|
||||
// `brushctl` and `brushinfo` names. It will behave identically across the two.
|
||||
self.builtin(
|
||||
"brushctl",
|
||||
brush_core::builtins::builtin::<BrushCtlCommand, SE>(),
|
||||
)
|
||||
.builtin(
|
||||
"brushinfo",
|
||||
brush_core::builtins::builtin::<BrushCtlCommand, SE>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the running brush shell.
|
||||
#[derive(Parser)]
|
||||
pub(crate) struct BrushCtlCommand {
|
||||
#[clap(subcommand)]
|
||||
command_group: CommandGroup,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum CommandGroup {
|
||||
#[clap(subcommand)]
|
||||
Complete(CompleteCommand),
|
||||
#[clap(subcommand)]
|
||||
Call(CallCommand),
|
||||
#[clap(subcommand)]
|
||||
Events(EventsCommand),
|
||||
#[clap(subcommand)]
|
||||
Process(ProcessCommand),
|
||||
}
|
||||
|
||||
/// Commands for inspecting call state.
|
||||
#[derive(Subcommand)]
|
||||
enum CallCommand {
|
||||
/// Display the current call stack.
|
||||
#[clap(name = "stack")]
|
||||
ShowCallStack {
|
||||
/// Whether to show more details.
|
||||
#[clap(short = 'd', long = "detailed")]
|
||||
detailed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for generating completions.
|
||||
#[derive(Subcommand)]
|
||||
enum CompleteCommand {
|
||||
/// Generate completions for an input line.
|
||||
#[clap(name = "line")]
|
||||
Line {
|
||||
/// The 0-indexed cursor position for generation.
|
||||
#[arg(long = "cursor", short = 'c')]
|
||||
cursor_index: Option<usize>,
|
||||
|
||||
/// The input line to generate completions for.
|
||||
line: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for configuring tracing events.
|
||||
#[derive(Subcommand)]
|
||||
enum EventsCommand {
|
||||
/// Display status of enabled events.
|
||||
Status,
|
||||
|
||||
/// Enable event.
|
||||
Enable {
|
||||
/// Event to enable.
|
||||
event: events::TraceEvent,
|
||||
},
|
||||
|
||||
/// Disable event.
|
||||
Disable {
|
||||
/// Event to disable.
|
||||
event: events::TraceEvent,
|
||||
},
|
||||
}
|
||||
|
||||
/// Commands for inspecting process state.
|
||||
#[expect(clippy::enum_variant_names)]
|
||||
#[derive(Subcommand)]
|
||||
enum ProcessCommand {
|
||||
/// Display process ID.
|
||||
#[clap(name = "pid")]
|
||||
ShowProcessId,
|
||||
/// Display process group ID.
|
||||
#[clap(name = "pgid")]
|
||||
ShowProcessGroupId,
|
||||
/// Display foreground process ID.
|
||||
#[clap(name = "fgpid")]
|
||||
ShowForegroundProcessId,
|
||||
/// Display parent process ID.
|
||||
#[clap(name = "ppid")]
|
||||
ShowParentProcessId,
|
||||
}
|
||||
|
||||
impl brush_core::builtins::Command for BrushCtlCommand {
|
||||
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> {
|
||||
match &self.command_group {
|
||||
CommandGroup::Call(call) => call.execute(&context),
|
||||
CommandGroup::Complete(complete) => complete.execute(&mut context).await,
|
||||
CommandGroup::Events(events) => events.execute(&context),
|
||||
CommandGroup::Process(process) => process.execute(&context),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CallCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::ShowCallStack { detailed } => {
|
||||
let stack = context.shell.call_stack();
|
||||
let format_options = brush_core::callstack::FormatOptions {
|
||||
show_args: *detailed,
|
||||
show_entry_points: *detailed,
|
||||
};
|
||||
|
||||
write!(context.stdout(), "{}", stack.format(&format_options))?;
|
||||
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompleteCommand {
|
||||
async fn execute(
|
||||
&self,
|
||||
context: &mut brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::Line { cursor_index, line } => {
|
||||
let completions = context
|
||||
.shell
|
||||
.complete(line, cursor_index.unwrap_or(line.len()))
|
||||
.await?;
|
||||
for candidate in completions.candidates {
|
||||
writeln!(context.stdout(), "{candidate}")?;
|
||||
}
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
let event_config = crate::entry::get_event_config();
|
||||
|
||||
let mut event_config = event_config.try_lock().map_err(|_| {
|
||||
brush_core::Error::from(brush_core::ErrorKind::Unimplemented(
|
||||
"Failed to acquire lock on event configuration",
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(event_config) = event_config.as_mut() {
|
||||
match self {
|
||||
Self::Status => {
|
||||
let enabled_events = event_config.get_enabled_events();
|
||||
for event in enabled_events {
|
||||
writeln!(context.stdout(), "{event}")?;
|
||||
}
|
||||
}
|
||||
Self::Enable { event } => event_config.enable(*event)?,
|
||||
Self::Disable { event } => event_config.disable(*event)?,
|
||||
}
|
||||
|
||||
Ok(brush_core::ExecutionResult::success())
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("event configuration not initialized").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProcessCommand {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &brush_core::ExecutionContext<'_, impl brush_core::ShellExtensions>,
|
||||
) -> Result<brush_core::ExecutionResult, brush_core::Error> {
|
||||
match self {
|
||||
Self::ShowProcessId => {
|
||||
writeln!(context.stdout(), "{}", std::process::id())?;
|
||||
Ok(ExecutionResult::success())
|
||||
}
|
||||
Self::ShowProcessGroupId => {
|
||||
if let Some(pgid) = sys::terminal::get_process_group_id() {
|
||||
writeln!(context.stdout(), "{pgid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get process group ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
Self::ShowForegroundProcessId => {
|
||||
if let Some(pid) = sys::terminal::get_foreground_pid() {
|
||||
writeln!(context.stdout(), "{pid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get foreground process ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
Self::ShowParentProcessId => {
|
||||
if let Some(pid) = sys::terminal::get_parent_process_id() {
|
||||
writeln!(context.stdout(), "{pid}")?;
|
||||
Ok(ExecutionResult::success())
|
||||
} else {
|
||||
writeln!(context.stderr(), "failed to get parent process ID")?;
|
||||
Ok(ExecutionResult::general_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
//! Bundled commands: utilities that ship inside the brush binary.
|
||||
//!
|
||||
//! Utilities are shipped busybox-style (one binary, many names) but execute
|
||||
//! as a subprocess of brush so that shell redirections, pipes, and
|
||||
//! process-group state are honored by code that reads/writes the host
|
||||
//! process's standard fds (e.g., uutils crates).
|
||||
//!
|
||||
//! ## Protocol
|
||||
//!
|
||||
//! The brush binary recognizes a hidden first-position argument
|
||||
//! [`DISPATCH_FLAG`] followed by `<NAME> [ARGS...]`. When present, brush
|
||||
//! dispatches early in `main()` to the registered function for `NAME`, before
|
||||
//! any shell state is built, and exits with the function's return code. The
|
||||
//! dispatched function has the same signature as `uutils`' `uumain`:
|
||||
//! `fn(Vec<OsString>) -> i32`, with the bundled name as `argv[0]`.
|
||||
//!
|
||||
//! ## Shell integration
|
||||
//!
|
||||
//! For every entry in the registry, [`register_shims`] installs a brush
|
||||
//! builtin (using `register_builtin_if_unset`, so brush's own builtins always
|
||||
//! win on conflict). The builtin's execution path uses brush-core's existing
|
||||
//! external-command machinery to spawn `current_exe() <DISPATCH_FLAG> <name>
|
||||
//! <args...>`, inheriting the shell's redirection state for free.
|
||||
//!
|
||||
//! The mechanism is generic — the registry is just `name → fn pointer`. The
|
||||
//! `experimental-bundled-coreutils` feature populates it with uutils, but
|
||||
//! anything matching the signature can be registered.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use brush_core::ExecutionExitCode;
|
||||
use brush_core::builtins::{BoxFuture, ContentOptions, ContentType, Registration};
|
||||
use brush_core::commands::{self, CommandArg, ExecutionContext};
|
||||
use brush_core::extensions::ShellExtensions;
|
||||
|
||||
/// The leading flag that signals a bundled-command dispatch.
|
||||
///
|
||||
/// Deliberately obscure so that it's unlikely to collide with future
|
||||
/// first-class shell flags or with scripts that happen to contain the
|
||||
/// literal token.
|
||||
pub const DISPATCH_FLAG: &str = "--invoke-bundled";
|
||||
|
||||
/// Signature of a bundled command's entry point — matches `uu_*::uumain`.
|
||||
pub type BundledFn = fn(args: Vec<OsString>) -> i32;
|
||||
|
||||
/// Process-wide registry. Set once at startup, read on each shim invocation
|
||||
/// (and during bundled-dispatch fast path).
|
||||
static REGISTRY: OnceLock<HashMap<String, BundledFn>> = OnceLock::new();
|
||||
|
||||
/// Cached path to the running brush executable. Populated lazily on first
|
||||
/// shim invocation; left as `Err`-equivalent if `current_exe()` fails.
|
||||
static SELF_EXE: OnceLock<Option<PathBuf>> = OnceLock::new();
|
||||
|
||||
/// Installs the bundled-command registry. Idempotent: only the first call
|
||||
/// takes effect.
|
||||
#[allow(
|
||||
clippy::implicit_hasher,
|
||||
reason = "registry uses the default hasher; callers build with HashMap::new()"
|
||||
)]
|
||||
pub fn install(commands: HashMap<String, BundledFn>) {
|
||||
let _ = REGISTRY.set(commands);
|
||||
}
|
||||
|
||||
/// Installs the registry from all compiled-in providers.
|
||||
///
|
||||
/// Providers are controlled by Cargo features. Binaries should call this
|
||||
/// once, before [`maybe_dispatch`], so both the dispatch fast path and the
|
||||
/// shell's shim builtins see a populated registry.
|
||||
pub fn install_default_providers() {
|
||||
#[allow(unused_mut)]
|
||||
let mut commands: HashMap<String, BundledFn> = HashMap::new();
|
||||
|
||||
#[cfg(feature = "experimental-bundled-coreutils")]
|
||||
commands.extend(brush_coreutils_builtins::bundled_commands());
|
||||
|
||||
install(commands);
|
||||
}
|
||||
|
||||
/// Returns the registered bundled commands, if [`install`] was called.
|
||||
#[must_use]
|
||||
pub fn registry() -> Option<&'static HashMap<String, BundledFn>> {
|
||||
REGISTRY.get()
|
||||
}
|
||||
|
||||
/// Runs the bundled-command fast path if the process was invoked for it.
|
||||
///
|
||||
/// If the process was invoked as `brush <DISPATCH_FLAG> <NAME> [ARGS...]`
|
||||
/// (with `<DISPATCH_FLAG>` as the very first argument after `argv[0]`), runs
|
||||
/// the registered function and returns its exit code as `Some(code)`. The
|
||||
/// caller is responsible for exiting the process with that code —
|
||||
/// centralizing the exit call in the binary's `main()` keeps destructors /
|
||||
/// panic hooks / tracing guards in the loop.
|
||||
///
|
||||
/// Returns `None` when the process was not invoked as a bundled dispatch, so
|
||||
/// normal shell startup can proceed.
|
||||
///
|
||||
/// The dispatch flag is only recognized in the leading position so that
|
||||
/// ordinary scripts and command lines containing the literal token elsewhere
|
||||
/// are not affected.
|
||||
#[must_use]
|
||||
pub fn maybe_dispatch() -> Option<i32> {
|
||||
let mut raw = std::env::args_os();
|
||||
let _argv0 = raw.next();
|
||||
let first = raw.next()?;
|
||||
if first != DISPATCH_FLAG {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Everything after `DISPATCH_FLAG` belongs to the bundled command. The
|
||||
// first such argument is the command name; subsequent arguments form its
|
||||
// argv (with the name itself supplied as argv[0] to match the convention
|
||||
// `uutils` and most CLI tools expect).
|
||||
let rest: Vec<OsString> = raw.collect();
|
||||
let Some((name, args)) = rest.split_first() else {
|
||||
eprintln!("brush: {DISPATCH_FLAG} requires a command name");
|
||||
return Some(exit_code(ExecutionExitCode::InvalidUsage));
|
||||
};
|
||||
|
||||
// The registry is keyed by UTF-8 `String`, so a non-UTF-8 name can never
|
||||
// match. Reject up front rather than allocating a lossy-substituted
|
||||
// lookup key that could accidentally collide with a real registration.
|
||||
let Some(name_str) = name.to_str() else {
|
||||
eprintln!("brush: unknown bundled command: {}", name.to_string_lossy());
|
||||
return Some(exit_code(ExecutionExitCode::NotFound));
|
||||
};
|
||||
|
||||
let Some(func) = REGISTRY.get().and_then(|r| r.get(name_str)) else {
|
||||
eprintln!("brush: unknown bundled command: {name_str}");
|
||||
return Some(exit_code(ExecutionExitCode::NotFound));
|
||||
};
|
||||
|
||||
let mut argv: Vec<OsString> = Vec::with_capacity(1 + args.len());
|
||||
argv.push(name.clone());
|
||||
argv.extend(args.iter().cloned());
|
||||
|
||||
Some(func(argv))
|
||||
}
|
||||
|
||||
fn exit_code(code: ExecutionExitCode) -> i32 {
|
||||
u8::from(code).into()
|
||||
}
|
||||
|
||||
/// Returns the path to the running brush executable (cached).
|
||||
fn self_exe() -> Option<&'static PathBuf> {
|
||||
SELF_EXE
|
||||
.get_or_init(|| std::env::current_exe().ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
/// Help/usage content provider for the shim builtin. brush calls this for
|
||||
/// `help <name>`, `type <name>`, etc.
|
||||
#[allow(
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::unnecessary_wraps,
|
||||
reason = "signature dictated by brush_core::builtins::CommandContentFunc"
|
||||
)]
|
||||
fn shim_content(
|
||||
name: &str,
|
||||
content_type: ContentType,
|
||||
_options: &ContentOptions,
|
||||
) -> Result<String, brush_core::Error> {
|
||||
match content_type {
|
||||
ContentType::ShortDescription => Ok(format!("{name} - bundled command")),
|
||||
ContentType::DetailedHelp => Ok(format!(
|
||||
"{name} - bundled command (executes via `brush {DISPATCH_FLAG} {name}`)\n"
|
||||
)),
|
||||
// A bundled command never contributes its own short-usage or man page
|
||||
// through this path; detailed help comes from the bundled utility
|
||||
// itself (`brush <DISPATCH_FLAG> <name> --help` or equivalent).
|
||||
ContentType::ShortUsage | ContentType::ManPage => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builtin execute function shared by all bundled commands. Looks up the
|
||||
/// invoked name from `context.command_name` and re-executes the running
|
||||
/// brush binary as `brush <DISPATCH_FLAG> <name> <args>`.
|
||||
///
|
||||
/// Reuses the same entry point the `command` builtin uses (see
|
||||
/// `brush-builtins/src/command.rs`): constructs a [`commands::SimpleCommand`]
|
||||
/// whose `command_name` is the absolute brush exe path. Because that contains
|
||||
/// a path separator, `SimpleCommand::execute` routes directly to the
|
||||
/// external-execution path, bypassing the builtin/function lookup that would
|
||||
/// otherwise re-enter this very shim.
|
||||
///
|
||||
/// `use_functions = false` is defensive: even though the path-separator
|
||||
/// branch already skips function dispatch, we don't want a hypothetical
|
||||
/// refactor of `SimpleCommand` to silently break us.
|
||||
//
|
||||
// TODO(bundled): Process-group propagation.
|
||||
// The shim leaves `SimpleCommand::process_group_id` as `None`, so when a
|
||||
// bundled command appears in a pipeline it doesn't join the pipeline's
|
||||
// pgid — job control and pipeline-wide signal delivery misbehave.
|
||||
// `ExecutionContext` doesn't currently carry the dispatcher's pgid, so
|
||||
// fixing this requires plumbing the pgid through the builtin dispatch
|
||||
// boundary (likely as a field on `ExecutionParameters` or a new
|
||||
// `ExecutionContext` accessor).
|
||||
//
|
||||
// TODO(bundled): Pipeline serialization.
|
||||
// The builtin contract returns an `ExecutionResult` (a completed command),
|
||||
// not an `ExecutionSpawnResult` (a spawn handle), so this function has to
|
||||
// `.await` the child to completion before returning. That's fine for a
|
||||
// standalone bundled command or for the tail of a pipeline, but for a
|
||||
// bundled stage in the middle of `a | b | c` it means stage N only
|
||||
// "starts" (from brush's perspective) after its child has fully exited —
|
||||
// downstream stages get no parallelism with it. Fixing this means
|
||||
// bypassing the builtin API for bundled dispatch: either detect the shim
|
||||
// inside `SimpleCommand::execute`'s dispatch table and return an
|
||||
// `ExecutionSpawnResult::StartedProcess` directly (same shape as external
|
||||
// dispatch), or generalize the builtin API so a builtin can return a
|
||||
// spawn handle instead of a finished result.
|
||||
fn shim_execute<SE: ShellExtensions>(
|
||||
context: ExecutionContext<'_, SE>,
|
||||
args: Vec<CommandArg>,
|
||||
) -> BoxFuture<'_, Result<brush_core::ExecutionResult, brush_core::Error>> {
|
||||
Box::pin(async move {
|
||||
let exe_path = if let Some(p) = self_exe() {
|
||||
p.to_string_lossy().into_owned()
|
||||
} else {
|
||||
let _ = writeln!(
|
||||
context.stderr(),
|
||||
"brush: cannot determine path to running executable"
|
||||
);
|
||||
return Ok(ExecutionExitCode::CannotExecute.into());
|
||||
};
|
||||
|
||||
// Build the argv for the spawned brush. `SimpleCommand::args[0]` is
|
||||
// dropped by the external-execution path (argv[0] of the spawned
|
||||
// process comes from `cmd.argv0` below), so a placeholder suffices;
|
||||
// args[1..] become the spawned process's argv[1..]. The caller's
|
||||
// `args[0]` is the bundled name by builtin-dispatch convention — we
|
||||
// replace it with an explicit `<name>` after `DISPATCH_FLAG` so the
|
||||
// child's dispatcher sees it in a fixed slot.
|
||||
let bundled_name = context.command_name.clone();
|
||||
let mut child_args: Vec<CommandArg> = Vec::with_capacity(args.len() + 2);
|
||||
child_args.push(CommandArg::String(String::new())); // args[0], dropped
|
||||
child_args.push(CommandArg::String(DISPATCH_FLAG.into()));
|
||||
child_args.push(CommandArg::String(bundled_name.clone()));
|
||||
child_args.extend(args.into_iter().skip(1));
|
||||
|
||||
let mut cmd = commands::SimpleCommand::new(
|
||||
commands::ShellForCommand::ParentShell(context.shell),
|
||||
context.params,
|
||||
exe_path,
|
||||
child_args,
|
||||
);
|
||||
cmd.use_functions = false;
|
||||
// Override the spawned process's argv[0] so tools that report errors
|
||||
// via their own argv[0] (uutils' `uucore::util_name()` reads
|
||||
// `std::env::args_os()[0]` into a LazyLock at first use) render as
|
||||
// `<name>:` rather than `brush:`. Without this the child sees the
|
||||
// brush exe path as argv[0] and misattributes errors.
|
||||
cmd.argv0 = Some(bundled_name);
|
||||
|
||||
let spawn_result = cmd.execute().await?;
|
||||
let wait_result = spawn_result.wait().await?;
|
||||
Ok(wait_result.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Constructs a [`Registration`] for the bundled-shim builtin. The same
|
||||
/// registration value can be reused for every bundled name; per-name
|
||||
/// dispatch happens via `context.command_name` at execution time.
|
||||
fn shim_registration<SE: ShellExtensions>() -> Registration<SE> {
|
||||
Registration {
|
||||
execute_func: shim_execute::<SE>,
|
||||
content_func: shim_content,
|
||||
disabled: false,
|
||||
special_builtin: false,
|
||||
declaration_builtin: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a shim builtin for every name in the installed bundled-command
|
||||
/// registry.
|
||||
///
|
||||
/// Uses `register_builtin_if_unset` so brush's own builtins (echo, printf,
|
||||
/// true, false, etc.) win on conflict.
|
||||
pub fn register_shims<SE: ShellExtensions>(shell: &mut brush_core::Shell<SE>) {
|
||||
let Some(registry) = REGISTRY.get() else {
|
||||
return;
|
||||
};
|
||||
for name in registry.keys() {
|
||||
shell.register_builtin_if_unset(name.clone(), shim_registration::<SE>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
//! Configuration file support for the brush shell.
|
||||
//!
|
||||
//! This module provides TOML-based configuration file loading with the following features:
|
||||
//! - Forward-compatible: unknown fields are ignored
|
||||
//! - Graceful degradation: parse errors are logged but don't prevent shell startup
|
||||
//! - Layered configuration: defaults < config file < command-line arguments
|
||||
|
||||
use brush_interactive::UIOptions;
|
||||
use etcetera::BaseStrategy;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::args::CommandLineArgs;
|
||||
|
||||
/// Root configuration structure for the brush shell.
|
||||
///
|
||||
/// All fields are optional to support forward compatibility and partial configuration.
|
||||
/// Unknown fields in the TOML file are silently ignored.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// User interface configuration options.
|
||||
pub ui: UiConfig,
|
||||
|
||||
/// Experimental features configuration.
|
||||
pub experimental: ExperimentalConfig,
|
||||
}
|
||||
|
||||
/// User interface configuration options.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct UiConfig {
|
||||
/// Enable syntax highlighting in the input line.
|
||||
#[serde(rename = "syntax-highlighting")]
|
||||
pub syntax_highlighting: Option<bool>,
|
||||
}
|
||||
|
||||
/// Experimental features configuration.
|
||||
///
|
||||
/// These options control unstable features that may change or be removed in future versions.
|
||||
#[derive(Debug, Default, Clone, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(default)]
|
||||
pub struct ExperimentalConfig {
|
||||
/// Enable zsh-style preexec/precmd hooks.
|
||||
#[serde(rename = "zsh-hooks")]
|
||||
pub zsh_hooks: Option<bool>,
|
||||
|
||||
/// Enable terminal shell integration.
|
||||
#[serde(rename = "terminal-shell-integration")]
|
||||
pub terminal_shell_integration: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Converts the configuration to [`UIOptions`], merging with CLI arguments.
|
||||
///
|
||||
/// Settings are applied with the following priority (highest to lowest):
|
||||
/// 1. CLI arguments (if explicitly set, i.e., different from default)
|
||||
/// 2. Config file values
|
||||
/// 3. Default values
|
||||
///
|
||||
/// CLI defaults are automatically inferred from clap's parsed defaults.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments
|
||||
#[must_use]
|
||||
pub fn to_ui_options(&self, args: &CommandLineArgs) -> UIOptions {
|
||||
// Get clap's defaults by parsing an empty argument list.
|
||||
// This lets us detect which CLI values were explicitly set vs. defaulted.
|
||||
let defaults = CommandLineArgs::default_values();
|
||||
|
||||
let enable_highlighting = merge_bool_setting(
|
||||
args.enable_highlighting,
|
||||
defaults.enable_highlighting,
|
||||
self.ui.syntax_highlighting,
|
||||
);
|
||||
let terminal_shell_integration = merge_bool_setting(
|
||||
args.terminal_shell_integration,
|
||||
defaults.terminal_shell_integration,
|
||||
self.experimental.terminal_shell_integration,
|
||||
);
|
||||
let zsh_style_hooks = merge_bool_setting(
|
||||
args.zsh_style_hooks,
|
||||
defaults.zsh_style_hooks,
|
||||
self.experimental.zsh_hooks,
|
||||
);
|
||||
|
||||
UIOptions::builder()
|
||||
.disable_bracketed_paste(args.disable_bracketed_paste)
|
||||
.disable_color(args.disable_color)
|
||||
.disable_highlighting(!enable_highlighting)
|
||||
.terminal_shell_integration(terminal_shell_integration)
|
||||
.zsh_style_hooks(zsh_style_hooks)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges a boolean setting from CLI args, config file, and defaults.
|
||||
///
|
||||
/// Priority: CLI (if explicitly set) > config file > default.
|
||||
///
|
||||
/// Since boolean CLI flags can't distinguish between "explicitly set to false" and
|
||||
/// "not provided" (both result in `false`), we use a heuristic:
|
||||
/// - If the CLI value differs from the default, the user explicitly provided it
|
||||
/// - Otherwise, use the config value if present, or fall back to the default
|
||||
const fn merge_bool_setting(
|
||||
cli_value: bool,
|
||||
cli_default: bool,
|
||||
config_value: Option<bool>,
|
||||
) -> bool {
|
||||
if cli_value != cli_default {
|
||||
// CLI was explicitly set to a non-default value
|
||||
cli_value
|
||||
} else if let Some(config) = config_value {
|
||||
// Use config file value
|
||||
config
|
||||
} else {
|
||||
// Fall back to default
|
||||
cli_default
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of attempting to load a configuration file.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ConfigLoadResult {
|
||||
/// The loaded configuration, or default if loading failed.
|
||||
pub config: Config,
|
||||
|
||||
/// The path that was used (or attempted) for loading.
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
/// Any error that occurred during loading.
|
||||
pub error: Option<ConfigLoadError>,
|
||||
|
||||
/// Whether the path was explicitly provided by the user (via `--config`).
|
||||
pub explicit_path: bool,
|
||||
}
|
||||
|
||||
impl ConfigLoadResult {
|
||||
/// Consumes the result and returns the configuration.
|
||||
///
|
||||
/// If an error occurred:
|
||||
/// - For explicit paths (user-provided via `--config`): returns `Err` with a formatted error
|
||||
/// - For default paths: logs a warning and returns the default configuration
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if an explicit config path was provided and loading failed.
|
||||
pub fn into_config_or_log(self) -> Result<Config, String> {
|
||||
let Some(err) = self.error else {
|
||||
return Ok(self.config);
|
||||
};
|
||||
|
||||
let path_display = self
|
||||
.path
|
||||
.as_ref()
|
||||
.map_or_else(|| String::from("<unknown>"), |p| p.display().to_string());
|
||||
|
||||
if self.explicit_path {
|
||||
// User explicitly provided --config; treat errors as fatal.
|
||||
return Err(format!("failed to load config from {path_display}: {err}"));
|
||||
}
|
||||
|
||||
// Default config path; log warning but continue with defaults.
|
||||
tracing::warn!("failed to load config from {path_display}: {err}");
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when loading configuration.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConfigLoadError {
|
||||
/// Failed to read the configuration file.
|
||||
#[error("failed to read config file: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Failed to parse the TOML content.
|
||||
#[error("failed to parse config file: {0}")]
|
||||
Parse(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
const CONFIG_SUBDIR_NAME: &str = "brush";
|
||||
const CONFIG_FILE_NAME: &str = "config.toml";
|
||||
|
||||
/// Returns the default configuration file path for the current platform.
|
||||
///
|
||||
/// Uses the XDG Base Directory specification on Linux/macOS and appropriate
|
||||
/// platform conventions on other systems via the `etcetera` crate.
|
||||
///
|
||||
/// Returns `None` if the platform's config directory cannot be determined.
|
||||
pub fn default_config_path() -> Option<PathBuf> {
|
||||
let strategy = etcetera::choose_base_strategy().ok()?;
|
||||
Some(
|
||||
strategy
|
||||
.config_dir()
|
||||
.join(CONFIG_SUBDIR_NAME)
|
||||
.join(CONFIG_FILE_NAME),
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads configuration from the specified path.
|
||||
///
|
||||
/// Returns a `ConfigLoadResult` containing:
|
||||
/// - The parsed configuration (or default on error)
|
||||
/// - The path that was used
|
||||
/// - Any error that occurred
|
||||
///
|
||||
/// Note: This function sets `explicit_path` to `false`. Use `load_config` for
|
||||
/// proper handling of explicit vs. default paths.
|
||||
pub fn load_from_path(path: &Path) -> ConfigLoadResult {
|
||||
let content = match std::fs::read_to_string(path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
return ConfigLoadResult {
|
||||
path: Some(path.to_path_buf()),
|
||||
error: Some(ConfigLoadError::Io(e)),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match toml::from_str(&content) {
|
||||
Ok(config) => ConfigLoadResult {
|
||||
config,
|
||||
path: Some(path.to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
Err(e) => ConfigLoadResult {
|
||||
path: Some(path.to_path_buf()),
|
||||
error: Some(ConfigLoadError::Parse(e)),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads configuration based on the provided options.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `disabled` - If true, skip loading and return defaults
|
||||
/// * `explicit_path` - If provided, use this path instead of the default
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A `ConfigLoadResult` containing the configuration and any errors encountered.
|
||||
/// If `explicit_path` is provided and loading fails, the result will have
|
||||
/// `explicit_path: true` to indicate that the error should be treated as fatal.
|
||||
pub fn load_config(disabled: bool, explicit_path: Option<&Path>) -> ConfigLoadResult {
|
||||
if disabled {
|
||||
return ConfigLoadResult::default();
|
||||
}
|
||||
|
||||
let is_explicit = explicit_path.is_some();
|
||||
|
||||
let path = match explicit_path {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => match default_config_path() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
// Can't determine config path; use defaults silently
|
||||
return ConfigLoadResult::default();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// If using default path and file doesn't exist, silently use defaults
|
||||
if !is_explicit && !path.exists() {
|
||||
return ConfigLoadResult {
|
||||
path: Some(path),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
let mut result = load_from_path(&path);
|
||||
result.explicit_path = is_explicit;
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn empty_config() {
|
||||
let config: Config = toml::from_str("").unwrap();
|
||||
assert!(config.ui.syntax_highlighting.is_none());
|
||||
assert!(config.experimental.zsh_hooks.is_none());
|
||||
assert!(config.experimental.terminal_shell_integration.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = true
|
||||
terminal-shell-integration = false
|
||||
";
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(true));
|
||||
assert_eq!(config.experimental.zsh_hooks, Some(true));
|
||||
assert_eq!(config.experimental.terminal_shell_integration, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = false
|
||||
";
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(false));
|
||||
assert!(config.experimental.zsh_hooks.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_fields_ignored() {
|
||||
let toml = r#"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
unknown-field = "should be ignored"
|
||||
another-unknown = 42
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = false
|
||||
future-feature = true
|
||||
|
||||
[unknown-section]
|
||||
foo = "bar"
|
||||
"#;
|
||||
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
assert_eq!(config.ui.syntax_highlighting, Some(true));
|
||||
assert_eq!(config.experimental.zsh_hooks, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_disabled() {
|
||||
let result = load_config(true, None);
|
||||
assert!(result.path.is_none());
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_nonexistent_default() {
|
||||
// When using default path and file doesn't exist, should return defaults without error
|
||||
let result = load_config(false, None);
|
||||
// We may or may not get a path depending on platform, but shouldn't error
|
||||
assert!(result.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_config_nonexistent_explicit() {
|
||||
let path = Path::new("/nonexistent/path/to/config.toml");
|
||||
let result = load_config(false, Some(path));
|
||||
assert!(result.error.is_some());
|
||||
assert!(matches!(result.error, Some(ConfigLoadError::Io(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_defaults_only() {
|
||||
let config = Config::default();
|
||||
let args = CommandLineArgs::default_values();
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_bracketed_paste);
|
||||
assert!(!ui.disable_color);
|
||||
// Note: whether highlighting is enabled by default depends on the compile-time
|
||||
// DEFAULT_ENABLE_HIGHLIGHTING constant (true with reedline, false without)
|
||||
assert!(!ui.terminal_shell_integration);
|
||||
assert!(!ui.zsh_style_hooks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_config_overrides_defaults() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = true
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = true
|
||||
terminal-shell-integration = true
|
||||
";
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
let args = CommandLineArgs::default_values();
|
||||
|
||||
// CLI values match defaults, so config should take effect
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_highlighting); // config enabled highlighting
|
||||
assert!(ui.terminal_shell_integration);
|
||||
assert!(ui.zsh_style_hooks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_cli_overrides_config() {
|
||||
let toml = r"
|
||||
[ui]
|
||||
syntax-highlighting = false
|
||||
|
||||
[experimental]
|
||||
zsh-hooks = false
|
||||
";
|
||||
let config: Config = toml::from_str(toml).unwrap();
|
||||
|
||||
// Simulate CLI explicitly setting values different from defaults
|
||||
// by parsing with the flags enabled
|
||||
let args = CommandLineArgs::try_parse_from([
|
||||
"brush",
|
||||
"--enable-highlighting",
|
||||
"--enable-zsh-hooks",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
// CLI explicitly enables highlighting and zsh-hooks (differs from default)
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(!ui.disable_highlighting); // CLI enabled highlighting
|
||||
assert!(ui.zsh_style_hooks); // CLI enabled
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_ui_options_cli_only_settings() {
|
||||
let config = Config::default();
|
||||
let args = CommandLineArgs::try_parse_from([
|
||||
"brush",
|
||||
"--disable-bracketed-paste",
|
||||
"--disable-color",
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let ui = config.to_ui_options(&args);
|
||||
|
||||
assert!(ui.disable_bracketed_paste);
|
||||
assert!(ui.disable_color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
//! Implements the command-line interface for the `brush` shell.
|
||||
|
||||
use crate::args::CommandLineArgs;
|
||||
use crate::args::InputBackendType;
|
||||
use crate::brushctl::ShellBuilderBrushBuiltinExt as _;
|
||||
use crate::bundled;
|
||||
use crate::config;
|
||||
use crate::error_formatter;
|
||||
use crate::events;
|
||||
use crate::productinfo;
|
||||
use brush_builtins::ShellBuilderExt as _;
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
use brush_experimental_builtins::ShellBuilderExt as _;
|
||||
use clap::CommandFactory;
|
||||
use std::sync::LazyLock;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[allow(unused_imports, reason = "only used in some configs")]
|
||||
use std::io::IsTerminal;
|
||||
|
||||
static TRACE_EVENT_CONFIG: LazyLock<Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>>> =
|
||||
LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
|
||||
|
||||
type BrushShellExtensions = brush_core::extensions::ShellExtensionsImpl<error_formatter::Formatter>;
|
||||
type BrushShell = brush_core::Shell<BrushShellExtensions>;
|
||||
|
||||
// WARN: this implementation shadows `clap::Parser::parse_from` one so it must be defined
|
||||
// after the `use clap::Parser`
|
||||
impl CommandLineArgs {
|
||||
// Work around clap's limitation handling `--` like a regular value
|
||||
// TODO(cmdline): We can safely remove this `impl` after the issue is resolved
|
||||
// https://github.com/clap-rs/clap/issues/5055
|
||||
// This function takes precedence over [`clap::Parser::parse_from`]
|
||||
fn try_parse_from(itr: impl IntoIterator<Item = String>) -> Result<Self, clap::Error> {
|
||||
let mut args: Vec<String> = itr.into_iter().collect();
|
||||
|
||||
// In bash, `-c` treats `--` as an option terminator and takes its
|
||||
// command string from the first argument *after* `--`. (Other
|
||||
// value-taking flags like `-o` and `-O` instead consume `--` as their
|
||||
// literal value in bash, rejecting it as an invalid option name.)
|
||||
//
|
||||
// Remove the `--` so that `-c` naturally consumes the next token as its
|
||||
// value via clap. Other value-taking flags are unaffected: for them
|
||||
// try_parse_known splits at `--` before clap sees it, so they still
|
||||
// produce an error for invocations like `-o --`/`-O --` (via a missing
|
||||
// value rather than an invalid option name). In both cases, we
|
||||
// intentionally do not treat `--` as an option terminator for those
|
||||
// flags.
|
||||
if let Some(dd_idx) = args.iter().position(|a| a == "--") {
|
||||
if let Some(flag_idx) = dd_idx
|
||||
.checked_sub(1)
|
||||
.filter(|&i| Self::has_pending_c_flag(&args[i]))
|
||||
{
|
||||
// Remove the option-terminating `--`.
|
||||
args.remove(dd_idx);
|
||||
|
||||
// If the command value (now at dd_idx) is itself `--`, merge it
|
||||
// into the flag as an attached value (e.g., "-c" + "--" → "-c--").
|
||||
// Clap parses `-c--` as `-c` with value `"--"` (standard POSIX
|
||||
// short-option-with-attached-value syntax). This prevents
|
||||
// try_parse_known from splitting at it again.
|
||||
if args.get(dd_idx).map(String::as_str) == Some("--") {
|
||||
let value = args.remove(dd_idx);
|
||||
args[flag_idx].push_str(&value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (mut this, script_args) = brush_core::builtins::try_parse_known::<Self>(args)?;
|
||||
|
||||
// Collect any args from after `--` (handled by try_parse_known) into
|
||||
// script_args, which become positional parameters ($0, $1, ...).
|
||||
if let Some(args) = script_args {
|
||||
this.script_args.extend(args);
|
||||
}
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
/// Returns true if `arg` is `-c` or a combined short-flag group ending in
|
||||
/// `c` (like `-ec`) where all preceding characters are boolean flags.
|
||||
///
|
||||
/// This specifically targets `-c` because it is the only short flag with
|
||||
/// special `--` option-terminator behavior in bash. Other value-taking flags
|
||||
/// (`-o`, `-O`) consume `--` as their literal value instead.
|
||||
///
|
||||
/// Uses clap's argument definitions to validate preceding flags, avoiding
|
||||
/// a hardcoded list of boolean flag characters.
|
||||
fn has_pending_c_flag(arg: &str) -> bool {
|
||||
// Must be a short flag group ending in 'c': "-c", "-ec", "-xec", etc.
|
||||
let Some(flags) = arg.strip_prefix('-') else {
|
||||
return false;
|
||||
};
|
||||
let Some(preceding) = flags.strip_suffix('c') else {
|
||||
return false;
|
||||
};
|
||||
// Reject long-option-like args (e.g., "--c").
|
||||
if preceding.starts_with('-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For "-c" alone, preceding is empty and the check below is vacuously
|
||||
// true. For combined flags like `-ec`, verify all chars before the
|
||||
// trailing `c` are boolean flags. If any preceding char takes a value
|
||||
// (like `o`), then `c` is consumed as that flag's value, not as `-c`.
|
||||
let cmd = Self::command();
|
||||
preceding.chars().all(|ch| {
|
||||
cmd.get_arguments().any(|a| {
|
||||
a.get_short() == Some(ch)
|
||||
&& !matches!(
|
||||
a.get_action(),
|
||||
clap::ArgAction::Set | clap::ArgAction::Append
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point for the `brush` shell.
|
||||
pub fn run() {
|
||||
//
|
||||
// Install the bundled-command registry so it's available both for
|
||||
// bundled dispatch (handled next) and for builtin shim registration
|
||||
// during shell construction. With no bundled-providing features enabled
|
||||
// the registry is empty and both code paths become no-ops.
|
||||
//
|
||||
bundled::install_default_providers();
|
||||
|
||||
//
|
||||
// If we were invoked as `brush <DISPATCH_FLAG> <name> [args...]`, run the
|
||||
// bundled command and exit before doing any shell setup. This is the
|
||||
// hot path for in-binary coreutils invocations.
|
||||
//
|
||||
if let Some(code) = bundled::maybe_dispatch() {
|
||||
std::process::exit(code);
|
||||
}
|
||||
|
||||
//
|
||||
// Install panic handlers to clean up on panic.
|
||||
//
|
||||
install_panic_handlers();
|
||||
|
||||
//
|
||||
// Parse args.
|
||||
//
|
||||
let mut args: Vec<_> = std::env::args().collect();
|
||||
|
||||
// Work around clap's limitations handling +O options.
|
||||
for arg in &mut args {
|
||||
if arg.starts_with("+O") {
|
||||
arg.insert_str(0, "--");
|
||||
}
|
||||
}
|
||||
|
||||
let parsed_args = match CommandLineArgs::try_parse_from(args.iter().cloned()) {
|
||||
Ok(parsed_args) => parsed_args,
|
||||
Err(e) => {
|
||||
let _ = e.print();
|
||||
|
||||
// Check for whether this is something we'd truly consider fatal. clap returns
|
||||
// errors for `--help`, `--version`, etc.
|
||||
let exit_code = match e.kind() {
|
||||
clap::error::ErrorKind::DisplayVersion => 0,
|
||||
clap::error::ErrorKind::DisplayHelp => 0,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Run.
|
||||
//
|
||||
#[cfg(any(unix, windows))]
|
||||
let mut builder = tokio::runtime::Builder::new_multi_thread();
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
let mut builder = tokio::runtime::Builder::new_current_thread();
|
||||
|
||||
let Ok(runtime) = builder.enable_all().build() else {
|
||||
tracing::error!("error: failed to create Tokio runtime");
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let result = runtime.block_on(run_async(args, parsed_args));
|
||||
|
||||
let exit_code = match result {
|
||||
Ok(code) => code,
|
||||
Err(err) => {
|
||||
tracing::error!("error: {err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
std::process::exit(i32::from(exit_code));
|
||||
}
|
||||
|
||||
/// Installs panic handlers to report our panic and cleanly exit on panic.
|
||||
fn install_panic_handlers() {
|
||||
//
|
||||
// Set up panic handler. On release builds, it will capture panic details to a
|
||||
// temporary .toml file and report a human-readable message to the screen.
|
||||
//
|
||||
human_panic::setup_panic!(
|
||||
human_panic::Metadata::new(productinfo::PRODUCT_NAME, productinfo::PRODUCT_VERSION)
|
||||
.homepage(env!("CARGO_PKG_HOMEPAGE"))
|
||||
.support("please post a GitHub issue at https://github.com/reubeno/brush/issues/new")
|
||||
);
|
||||
|
||||
//
|
||||
// If stdout is connected to a terminal, then register a new panic handler that
|
||||
// resets the terminal and then invokes the previously registered handler. In
|
||||
// dev/debug builds, the previously registered handler will be the default
|
||||
// handler; in release builds, it will be the one registered by `human_panic`.
|
||||
//
|
||||
if std::io::stdout().is_terminal() {
|
||||
let original_panic_handler = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// Best-effort attempt to reset the terminal to defaults.
|
||||
let _ = try_reset_terminal_to_defaults();
|
||||
|
||||
// Invoke the original handler
|
||||
original_panic_handler(panic_info);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental")]
|
||||
pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = true;
|
||||
#[cfg(not(feature = "experimental"))]
|
||||
pub(crate) const DEFAULT_ENABLE_HIGHLIGHTING: bool = false;
|
||||
|
||||
/// Run the brush shell. Returns the exit code.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cli_args` - The command-line arguments to the shell, in string form.
|
||||
/// * `args` - The already-parsed command-line arguments.
|
||||
#[doc(hidden)]
|
||||
async fn run_async(
|
||||
cli_args: Vec<String>,
|
||||
args: CommandLineArgs,
|
||||
) -> Result<u8, brush_interactive::ShellError> {
|
||||
// Initializing tracing.
|
||||
let mut event_config = TRACE_EVENT_CONFIG.lock().await;
|
||||
*event_config = Some(events::TraceEventConfig::init(
|
||||
&args.enabled_debug_events,
|
||||
&args.disabled_events,
|
||||
));
|
||||
drop(event_config);
|
||||
|
||||
// Load configuration file.
|
||||
let file_config = config::load_config(args.no_config, args.config_file.as_deref())
|
||||
.into_config_or_log()
|
||||
.map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
|
||||
|
||||
// Instantiate an appropriately configured shell and wrap it in an `Arc`. Note that we do
|
||||
// *not* run any code in the shell yet. We'll delay loading profiles and such until after
|
||||
// we've set up everything else (in `run_in_shell`).
|
||||
let shell: BrushShell = instantiate_shell(&args, cli_args).await?;
|
||||
let shell = Arc::new(Mutex::new(shell));
|
||||
|
||||
// Run with the selected input backend. Each branch instantiates the concrete
|
||||
// backend type and calls `run_in_shell`, preserving static dispatch.
|
||||
let default_backend = get_default_input_backend_type(&args);
|
||||
let selected_backend = args.input_backend.unwrap_or(default_backend);
|
||||
|
||||
// Build UI options by merging config file with CLI args.
|
||||
#[allow(unused_variables, reason = "not used when no backend features enabled")]
|
||||
let ui_options = file_config.to_ui_options(&args);
|
||||
|
||||
let result = match selected_backend {
|
||||
#[cfg(all(feature = "reedline", any(unix, windows)))]
|
||||
InputBackendType::Reedline => {
|
||||
let mut input_backend =
|
||||
brush_interactive::ReedlineInputBackend::new(&ui_options, &shell)?;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(any(not(feature = "reedline"), not(any(unix, windows))))]
|
||||
InputBackendType::Reedline => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
|
||||
#[cfg(feature = "basic")]
|
||||
InputBackendType::Basic => {
|
||||
let mut input_backend = brush_interactive::BasicInputBackend;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(not(feature = "basic"))]
|
||||
InputBackendType::Basic => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
|
||||
#[cfg(feature = "minimal")]
|
||||
InputBackendType::Minimal => {
|
||||
let mut input_backend = brush_interactive::MinimalInputBackend;
|
||||
run_in_shell(&shell, args.clone(), &mut input_backend, &ui_options).await
|
||||
}
|
||||
#[cfg(not(feature = "minimal"))]
|
||||
InputBackendType::Minimal => Err(brush_interactive::ShellError::InputBackendNotSupported),
|
||||
};
|
||||
|
||||
// Display any error that percolated up.
|
||||
let exit_code = match result {
|
||||
Ok(code) => code,
|
||||
Err(brush_interactive::ShellError::ShellError(e)) => {
|
||||
let shell = shell.lock().await;
|
||||
let mut stderr = shell.stderr();
|
||||
let _ = shell.display_error(&mut stderr, &e);
|
||||
drop(shell);
|
||||
1
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("error: {err:#}");
|
||||
1
|
||||
}
|
||||
};
|
||||
|
||||
Ok(exit_code)
|
||||
}
|
||||
|
||||
/// Determines whether `run_in_shell` will run the shell interactively. Must be sync'd with it.
|
||||
const fn will_run_interactively(args: &CommandLineArgs) -> bool {
|
||||
if args.command.is_some() {
|
||||
false
|
||||
} else if args.read_commands_from_stdin {
|
||||
true
|
||||
} else {
|
||||
args.script_args.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs the shell according to the provided command-line arguments.
|
||||
/// Also responsible for loading profiles and rc files as appropriate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell_ref` - A reference to the shell to run.
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `input_backend` - The input backend to use.
|
||||
/// * `ui_options` - The user interface options to use.
|
||||
async fn run_in_shell(
|
||||
shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
|
||||
args: CommandLineArgs,
|
||||
input_backend: &mut impl brush_interactive::InputBackend,
|
||||
ui_options: &brush_interactive::UIOptions,
|
||||
) -> Result<u8, brush_interactive::ShellError> {
|
||||
// First load profile and rc files as appropriate.
|
||||
initialize_shell(shell_ref, &args).await?;
|
||||
|
||||
// If a command was specified via -c, then run that command and then exit.
|
||||
if let Some(command) = args.command {
|
||||
shell_ref.lock().await.run_dash_c_command(command).await?;
|
||||
|
||||
// If -s was provided, then read commands from stdin. If there was a script (and optionally
|
||||
// args) passed on the command line via positional arguments, then we copy over the
|
||||
// parameters but do *not* execute it.
|
||||
} else if args.read_commands_from_stdin {
|
||||
let interactive_options = ui_options.into();
|
||||
brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
|
||||
.run_interactively()
|
||||
.await?;
|
||||
|
||||
// If a script path was provided, then run the script.
|
||||
} else if !args.script_args.is_empty() {
|
||||
// The path to a script was provided on the command line; run the script.
|
||||
shell_ref
|
||||
.lock()
|
||||
.await
|
||||
.run_script(
|
||||
Path::new(&args.script_args[0]),
|
||||
args.script_args.iter().skip(1),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If we got down here, then we don't have any commands to run. We'll be reading
|
||||
// them in from stdin one way or the other.
|
||||
} else {
|
||||
let interactive_options = ui_options.into();
|
||||
brush_interactive::InteractiveShell::new(shell_ref, input_backend, &interactive_options)?
|
||||
.run_interactively()
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Make sure to return the last result observed in the shell.
|
||||
let result = shell_ref.lock().await.last_exit_status();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Initializes a shell by loading profile and rc files as appropriate.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `shell_ref` - A reference to the shell to initialize.
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
async fn initialize_shell(
|
||||
shell_ref: &brush_interactive::ShellRef<impl brush_core::ShellExtensions>,
|
||||
args: &CommandLineArgs,
|
||||
) -> Result<(), brush_interactive::ShellError> {
|
||||
// Compute desired profile-loading behavior.
|
||||
let profile = if args.no_profile {
|
||||
brush_core::ProfileLoadBehavior::Skip
|
||||
} else {
|
||||
brush_core::ProfileLoadBehavior::LoadDefault
|
||||
};
|
||||
|
||||
// Compute desired rc-loading behavior.
|
||||
let rc = if args.no_rc {
|
||||
brush_core::RcLoadBehavior::Skip
|
||||
} else if let Some(rc_file) = &args.rc_file {
|
||||
brush_core::RcLoadBehavior::LoadCustom(rc_file.clone())
|
||||
} else {
|
||||
brush_core::RcLoadBehavior::LoadDefault
|
||||
};
|
||||
|
||||
shell_ref.lock().await.load_config(&profile, &rc).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Instantiates a shell from command-line arguments. Does *not* run any code in the shell.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `cli_args` - The raw command-line arguments.
|
||||
async fn instantiate_shell(
|
||||
args: &CommandLineArgs,
|
||||
cli_args: Vec<String>,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
#[cfg(feature = "experimental-load")]
|
||||
let mut shell = if let Some(load_file) = &args.load_file {
|
||||
instantiate_shell_from_file(load_file.as_path())?
|
||||
} else {
|
||||
instantiate_shell_from_args(args, cli_args).await?
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "experimental-load"))]
|
||||
let mut shell = instantiate_shell_from_args(args, cli_args).await?;
|
||||
|
||||
// Register shims for any bundled commands in the installed registry.
|
||||
// Done here (not inside the inner instantiators) so both paths are
|
||||
// covered from a single site.
|
||||
bundled::register_shims(&mut shell);
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-load")]
|
||||
fn instantiate_shell_from_file(
|
||||
file_path: &Path,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
let mut shell: BrushShell = serde_json::from_reader(std::fs::File::open(file_path)?)
|
||||
.map_err(|e| brush_interactive::ShellError::IoError(std::io::Error::other(e)))?;
|
||||
|
||||
// NOTE: We need to manually register builtins because we can't serialize/deserialize them.
|
||||
// TODO(serde): we should consider whether we could/should at least track *which* are enabled.
|
||||
let builtin_set = if shell.options().sh_mode {
|
||||
brush_builtins::BuiltinSet::ShMode
|
||||
} else {
|
||||
brush_builtins::BuiltinSet::BashMode
|
||||
};
|
||||
|
||||
let builtins = brush_builtins::default_builtins(builtin_set);
|
||||
|
||||
for (builtin_name, builtin) in builtins {
|
||||
shell.register_builtin(&builtin_name, builtin);
|
||||
}
|
||||
|
||||
// Add experimental builtins (if enabled).
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
for (builtin_name, builtin) in brush_experimental_builtins::experimental_builtins() {
|
||||
shell.register_builtin(&builtin_name, builtin);
|
||||
}
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
/// Instantiates a shell from command-line arguments. Does *not* run any code in the shell.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The parsed command-line arguments.
|
||||
/// * `cli_args` - The raw command-line arguments.
|
||||
async fn instantiate_shell_from_args(
|
||||
args: &CommandLineArgs,
|
||||
cli_args: Vec<String>,
|
||||
) -> Result<BrushShell, brush_interactive::ShellError> {
|
||||
// Compute login flag.
|
||||
let login = args.login || cli_args.first().is_some_and(|argv0| argv0.starts_with('-'));
|
||||
|
||||
// Compute shell name.
|
||||
let shell_name = if args.command.is_some() && !args.script_args.is_empty() {
|
||||
Some(args.script_args[0].clone())
|
||||
} else if !cli_args.is_empty() {
|
||||
Some(cli_args[0].clone())
|
||||
} else if args.sh_mode {
|
||||
// Simulate having been run as "sh".
|
||||
Some(String::from("sh"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Compute positional shell arguments.
|
||||
let shell_args = if args.command.is_some() {
|
||||
Some(args.script_args.iter().skip(1).cloned().collect())
|
||||
} else if args.read_commands_from_stdin {
|
||||
Some(args.script_args.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Commands are read from stdin if -s was provided, or if no command was specified (either via
|
||||
// -c or as a positional argument).
|
||||
let read_commands_from_stdin = (args.read_commands_from_stdin && args.command.is_none())
|
||||
|| (args.script_args.is_empty() && args.command.is_none());
|
||||
|
||||
let builtin_set = if args.sh_mode {
|
||||
brush_builtins::BuiltinSet::ShMode
|
||||
} else {
|
||||
brush_builtins::BuiltinSet::BashMode
|
||||
};
|
||||
|
||||
// Identify the file descriptors to inherit.
|
||||
let fds = args
|
||||
.inherited_fds
|
||||
.iter()
|
||||
.filter_map(|&fd| brush_core::sys::fd::try_get_file_for_open_fd(fd).map(|file| (fd, file)))
|
||||
.collect();
|
||||
|
||||
// Select parser implementation to use.
|
||||
#[cfg(feature = "experimental-parser")]
|
||||
let parser_impl = if args.experimental_parser {
|
||||
brush_core::parser::ParserImpl::Winnow
|
||||
} else {
|
||||
brush_core::parser::ParserImpl::Peg
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "experimental-parser"))]
|
||||
let parser_impl = brush_core::parser::ParserImpl::Peg;
|
||||
|
||||
// Set up the shell builder with the requested options.
|
||||
// NOTE: We skip loading profile and rc files here; that will be handled later after we've
|
||||
// fully instantiated everything we want set before running any code.
|
||||
let shell = brush_core::Shell::builder_with_extensions::<BrushShellExtensions>()
|
||||
.disable_options(args.disabled_options.clone())
|
||||
.disable_shopt_options(args.disabled_shopt_options.clone())
|
||||
.disallow_overwriting_regular_files_via_output_redirection(
|
||||
args.disallow_overwriting_regular_files_via_output_redirection,
|
||||
)
|
||||
.enable_options(args.enabled_options.clone())
|
||||
.enable_shopt_options(args.enabled_shopt_options.clone())
|
||||
.do_not_execute_commands(args.do_not_execute_commands)
|
||||
.exit_after_one_command(args.exit_after_one_command)
|
||||
.login(login)
|
||||
.interactive(args.is_interactive())
|
||||
.command_string_mode(args.command.is_some())
|
||||
.no_editing(args.no_editing)
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.do_not_inherit_env(args.do_not_inherit_env)
|
||||
.fds(fds)
|
||||
.maybe_shell_args(shell_args)
|
||||
.posix(args.posix || args.sh_mode)
|
||||
.print_commands_and_arguments(args.print_commands_and_arguments)
|
||||
.read_commands_from_stdin(read_commands_from_stdin)
|
||||
.maybe_shell_name(shell_name)
|
||||
.shell_product_display_str(productinfo::get_product_display_str())
|
||||
.sh_mode(args.sh_mode)
|
||||
.treat_unset_variables_as_error(args.treat_unset_variables_as_error)
|
||||
.exit_on_nonzero_command_exit(args.exit_on_nonzero_command_exit)
|
||||
.disable_pathname_expansion(args.disable_pathname_expansion)
|
||||
.verbose(args.verbose)
|
||||
.parser(parser_impl)
|
||||
.error_formatter(new_error_behavior(args))
|
||||
.shell_version(env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
// Add builtins.
|
||||
let shell = shell.default_builtins(builtin_set).brush_builtins();
|
||||
|
||||
// Add experimental builtins (if enabled).
|
||||
#[cfg(feature = "experimental-builtins")]
|
||||
let shell = shell.experimental_builtins();
|
||||
|
||||
// Build the shell.
|
||||
let mut shell = shell.build().await?;
|
||||
|
||||
// Make adjustments.
|
||||
if let Some(xtrace_file_path) = &args.xtrace_file_path {
|
||||
enable_xtrace_to_file(&mut shell, xtrace_file_path)?;
|
||||
}
|
||||
|
||||
Ok(shell)
|
||||
}
|
||||
|
||||
fn enable_xtrace_to_file(
|
||||
shell: &mut brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
file_path: &Path,
|
||||
) -> Result<(), brush_interactive::ShellError> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file_path)
|
||||
.map_err(|e| {
|
||||
brush_interactive::ShellError::FailedToCreateXtraceFile(file_path.to_path_buf(), e)
|
||||
})?;
|
||||
|
||||
let file = brush_core::openfiles::OpenFile::from(file);
|
||||
let file_fd = shell.open_files_mut().add(file)?;
|
||||
|
||||
shell.options_mut().print_commands_and_arguments = true;
|
||||
shell.set_env_global(
|
||||
"BASH_XTRACEFD",
|
||||
brush_core::ShellVariable::new(file_fd.to_string()),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const fn new_error_behavior(args: &CommandLineArgs) -> error_formatter::Formatter {
|
||||
error_formatter::Formatter {
|
||||
use_color: !args.disable_color,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_input_backend_type(args: &CommandLineArgs) -> InputBackendType {
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
// If stdin isn't a terminal, then `reedline` doesn't do the right thing
|
||||
// (reference: https://github.com/nushell/reedline/issues/509). Switch to
|
||||
// the minimal input backend instead for that scenario.
|
||||
if std::io::stdin().is_terminal() && will_run_interactively(args) {
|
||||
InputBackendType::Reedline
|
||||
} else {
|
||||
InputBackendType::Minimal
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
let _args = args;
|
||||
InputBackendType::Minimal
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_event_config() -> Arc<tokio::sync::Mutex<Option<events::TraceEventConfig>>> {
|
||||
TRACE_EVENT_CONFIG.clone()
|
||||
}
|
||||
|
||||
fn try_reset_terminal_to_defaults() -> Result<(), std::io::Error> {
|
||||
#[cfg(any(unix, windows))]
|
||||
{
|
||||
// Reset the console.
|
||||
let exec_result = crossterm::execute!(
|
||||
std::io::stdout(),
|
||||
crossterm::terminal::LeaveAlternateScreen,
|
||||
crossterm::terminal::EnableLineWrap,
|
||||
crossterm::style::ResetColor,
|
||||
crossterm::event::DisableMouseCapture,
|
||||
crossterm::event::DisableBracketedPaste,
|
||||
crossterm::cursor::Show,
|
||||
crossterm::cursor::MoveToNextLine(1),
|
||||
);
|
||||
|
||||
let raw_result = crossterm::terminal::disable_raw_mode();
|
||||
|
||||
exec_result?;
|
||||
raw_result?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::panic_in_result_fn)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
|
||||
fn args(strs: &[&str]) -> Vec<String> {
|
||||
strs.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush"]))?;
|
||||
assert_matches!(parsed_args.script_args.as_slice(), []);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_script_and_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&[
|
||||
"brush",
|
||||
"some-script",
|
||||
"-x",
|
||||
"1",
|
||||
"--option",
|
||||
]))?;
|
||||
assert_eq!(
|
||||
parsed_args.script_args,
|
||||
["some-script", "-x", "1", "--option"]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_script_and_args_with_double_dash_in_script_args() -> Result<()> {
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "some-script", "--"]))?;
|
||||
assert_eq!(parsed_args.script_args, ["some-script", "--"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_unknown_args() {
|
||||
let result = CommandLineArgs::try_parse_from(args(&["brush", "--unknown-option"]));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_separator() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo hello", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hello".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_no_command() {
|
||||
assert!(CommandLineArgs::try_parse_from(args(&["brush", "-c", "--"])).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_command_is_double_dash() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "--", "echo", "hi"]))?;
|
||||
assert_eq!(parsed_args.command, Some("--".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["echo", "hi"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ec_with_double_dash_separator() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-ec", "--", "echo hello", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hello".to_string()));
|
||||
assert!(parsed_args.exit_on_nonzero_command_exit);
|
||||
assert_eq!(parsed_args.script_args, ["arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_value_before_double_dash_unchanged() -> Result<()> {
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "echo hi", "--", "arg0"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo hi".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["--", "arg0"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_o_with_double_dash_is_not_transformed() {
|
||||
// Unlike -c, bash's -o consumes -- as its literal value (invalid option
|
||||
// name), not as an option terminator. Verify we don't transform it.
|
||||
let result = CommandLineArgs::try_parse_from(args(&["brush", "-o", "--"]));
|
||||
// Here, try_parse_from / try_parse_known splits at --, so -o ends up
|
||||
// without a value and parsing correctly fails. The key assertion is
|
||||
// that we MUST NOT reinterpret -- as an option terminator for -o and
|
||||
// then take any later argument as its value.
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_oc_not_treated_as_pending_c() -> Result<()> {
|
||||
// -oc means -o with value "c", not -o flag + -c flag. The --
|
||||
// should NOT be treated as an option terminator for -c.
|
||||
let parsed_args = CommandLineArgs::try_parse_from(args(&["brush", "-oc", "--", "echo"]))?;
|
||||
// -o consumed "c" as its value; -- split the rest; no -c command.
|
||||
assert!(parsed_args.command.is_none());
|
||||
assert_eq!(parsed_args.script_args, ["--", "echo"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_bool_flag_before_double_dash_not_transformed() -> Result<()> {
|
||||
// -e is a boolean flag, not -c. The -- should NOT be removed;
|
||||
// everything from -- onward becomes positional (including -c).
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-e", "--", "-c", "echo"]))?;
|
||||
assert!(parsed_args.command.is_none());
|
||||
assert!(parsed_args.exit_on_nonzero_command_exit);
|
||||
assert_eq!(parsed_args.script_args, ["--", "-c", "echo"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_c_with_double_dash_and_later_double_dash() -> Result<()> {
|
||||
// After removing the first --, -c gets "echo". The second -- is
|
||||
// handled by try_parse_known and appears in script_args.
|
||||
let parsed_args =
|
||||
CommandLineArgs::try_parse_from(args(&["brush", "-c", "--", "echo", "--", "more"]))?;
|
||||
assert_eq!(parsed_args.command, Some("echo".to_string()));
|
||||
assert_eq!(parsed_args.script_args, ["--", "more"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_pending_c_flag_edge_cases() {
|
||||
// Direct tests for the detection function.
|
||||
assert!(CommandLineArgs::has_pending_c_flag("-c"));
|
||||
assert!(CommandLineArgs::has_pending_c_flag("-ec"));
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-C")); // uppercase, different flag
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-oc")); // -o takes a value
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("--c")); // long-option-like
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("-")); // bare dash
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("c")); // no leading dash
|
||||
assert!(!CommandLineArgs::has_pending_c_flag("")); // empty
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct Formatter {
|
||||
pub use_color: bool,
|
||||
}
|
||||
|
||||
impl brush_core::extensions::ErrorFormatter for Formatter {
|
||||
fn format_error(
|
||||
&self,
|
||||
err: &brush_core::error::Error,
|
||||
_shell: &brush_core::Shell<impl brush_core::ShellExtensions>,
|
||||
) -> String {
|
||||
let prefix = if self.use_color {
|
||||
color_print::cstr!("<red>error:</red> ")
|
||||
} else {
|
||||
"error: "
|
||||
};
|
||||
|
||||
std::format!("{prefix}{err:#}\n")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Facilities for configuring event tracing in the brush shell.
|
||||
|
||||
use std::{collections::HashSet, fmt::Display};
|
||||
|
||||
use brush_core::Error;
|
||||
use tracing_subscriber::{
|
||||
Layer, Registry, filter::Targets, layer::SubscriberExt, reload::Handle, util::SubscriberInitExt,
|
||||
};
|
||||
|
||||
/// Type of event to trace.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
|
||||
pub enum TraceEvent {
|
||||
/// Traces parsing and evaluation of arithmetic expressions.
|
||||
#[clap(name = "arithmetic")]
|
||||
Arithmetic,
|
||||
/// Traces command execution.
|
||||
#[clap(name = "commands")]
|
||||
Commands,
|
||||
/// Traces command completion generation.
|
||||
#[clap(name = "complete")]
|
||||
Complete,
|
||||
/// Traces word expansion.
|
||||
#[clap(name = "expand")]
|
||||
Expand,
|
||||
/// Traces functions.
|
||||
#[clap(name = "functions")]
|
||||
Functions,
|
||||
/// Traces input controls.
|
||||
#[clap(name = "input")]
|
||||
Input,
|
||||
/// Traces job management.
|
||||
#[clap(name = "jobs")]
|
||||
Jobs,
|
||||
/// Traces the process of parsing tokens into an abstract syntax tree.
|
||||
#[clap(name = "parse")]
|
||||
Parse,
|
||||
/// Traces pattern matching.
|
||||
#[clap(name = "pattern")]
|
||||
Pattern,
|
||||
/// Traces the process of tokenizing input text.
|
||||
#[clap(name = "tokenize")]
|
||||
Tokenize,
|
||||
/// Traces usage of unimplemented functionality.
|
||||
#[clap(name = "unimplemented", alias = "unimp")]
|
||||
Unimplemented,
|
||||
}
|
||||
|
||||
impl Display for TraceEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Arithmetic => write!(f, "arithmetic"),
|
||||
Self::Commands => write!(f, "commands"),
|
||||
Self::Complete => write!(f, "complete"),
|
||||
Self::Expand => write!(f, "expand"),
|
||||
Self::Functions => write!(f, "functions"),
|
||||
Self::Input => write!(f, "input"),
|
||||
Self::Jobs => write!(f, "jobs"),
|
||||
Self::Parse => write!(f, "parse"),
|
||||
Self::Pattern => write!(f, "pattern"),
|
||||
Self::Tokenize => write!(f, "tokenize"),
|
||||
Self::Unimplemented => write!(f, "unimplemented"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TraceEventConfig {
|
||||
enabled_debug_events: HashSet<TraceEvent>,
|
||||
disabled_events: HashSet<TraceEvent>,
|
||||
handle: Option<Handle<Targets, Registry>>,
|
||||
}
|
||||
|
||||
impl TraceEventConfig {
|
||||
pub fn init(enabled_debug_events: &[TraceEvent], disabled_events: &[TraceEvent]) -> Self {
|
||||
let enabled_debug_events: HashSet<TraceEvent> =
|
||||
enabled_debug_events.iter().copied().collect();
|
||||
let disabled_events: HashSet<TraceEvent> = disabled_events.iter().copied().collect();
|
||||
|
||||
let mut config = Self {
|
||||
enabled_debug_events,
|
||||
disabled_events,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let filter = config.compose_filter();
|
||||
|
||||
// Make the filter reloadable so that we can change the log level at runtime.
|
||||
let (reload_filter, handle) = tracing_subscriber::reload::Layer::new(filter);
|
||||
|
||||
let layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stderr)
|
||||
.without_time()
|
||||
.with_target(false)
|
||||
.with_filter(reload_filter);
|
||||
|
||||
if tracing_subscriber::registry()
|
||||
.with(layer)
|
||||
.try_init()
|
||||
.is_ok()
|
||||
{
|
||||
config.handle = Some(handle);
|
||||
} else {
|
||||
// Something went wrong; proceed on anyway but complain audibly.
|
||||
eprintln!("warning: failed to initialize tracing.");
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
fn compose_filter(&self) -> tracing_subscriber::filter::Targets {
|
||||
let mut filter = tracing_subscriber::filter::Targets::new()
|
||||
.with_default(tracing_subscriber::filter::LevelFilter::INFO);
|
||||
|
||||
for event in &self.enabled_debug_events {
|
||||
let targets = Self::event_to_tracing_targets(event);
|
||||
filter = filter.with_targets(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| (target, tracing::Level::DEBUG)),
|
||||
);
|
||||
}
|
||||
|
||||
for event in &self.disabled_events {
|
||||
let targets = Self::event_to_tracing_targets(event);
|
||||
filter = filter.with_targets(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| (target, tracing::level_filters::LevelFilter::OFF)),
|
||||
);
|
||||
}
|
||||
|
||||
filter
|
||||
}
|
||||
|
||||
fn event_to_tracing_targets(event: &TraceEvent) -> Vec<&str> {
|
||||
match event {
|
||||
TraceEvent::Arithmetic => vec!["arithmetic"],
|
||||
TraceEvent::Commands => vec!["commands"],
|
||||
TraceEvent::Complete => vec!["completion"],
|
||||
TraceEvent::Expand => vec!["expansion"],
|
||||
TraceEvent::Functions => vec!["functions"],
|
||||
TraceEvent::Input => vec!["input"],
|
||||
TraceEvent::Jobs => vec!["jobs"],
|
||||
TraceEvent::Parse => vec!["parse"],
|
||||
TraceEvent::Pattern => vec!["pattern"],
|
||||
TraceEvent::Tokenize => vec!["tokenize"],
|
||||
TraceEvent::Unimplemented => vec!["unimplemented"],
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get_enabled_events(&self) -> &HashSet<TraceEvent> {
|
||||
&self.enabled_debug_events
|
||||
}
|
||||
|
||||
pub fn enable(&mut self, event: TraceEvent) -> Result<(), Error> {
|
||||
// Don't bother to reload config if nothing has changed.
|
||||
if !self.enabled_debug_events.insert(event) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.reload_filter()
|
||||
}
|
||||
|
||||
pub fn disable(&mut self, event: TraceEvent) -> Result<(), Error> {
|
||||
// Don't bother to reload config if nothing has changed.
|
||||
if !self.enabled_debug_events.remove(&event) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.reload_filter()
|
||||
}
|
||||
|
||||
fn reload_filter(&self) -> Result<(), Error> {
|
||||
if let Some(handle) = &self.handle {
|
||||
if handle.reload(self.compose_filter()).is_ok() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("failed to enable tracing events").into())
|
||||
}
|
||||
} else {
|
||||
Err(brush_core::ErrorKind::Unimplemented("tracing not initialized").into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Create for brush, an executable bash-compatible shell.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod args;
|
||||
mod brushctl;
|
||||
pub mod bundled;
|
||||
pub mod config;
|
||||
pub mod entry;
|
||||
mod error_formatter;
|
||||
pub mod events;
|
||||
mod productinfo;
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Main entry for the `brush` shell.
|
||||
|
||||
fn main() {
|
||||
brush_shell::entry::run();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Information about this shell project.
|
||||
|
||||
// nash: rebranded from "brush" for the Nucleic agent shell fork (docs/NASH.md §2).
|
||||
/// The formal name of this product.
|
||||
pub const PRODUCT_NAME: &str = "nash";
|
||||
|
||||
const PRODUCT_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
|
||||
const PRODUCT_REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||
|
||||
/// The URI to display as the product's homepage.
|
||||
#[allow(clippy::const_is_empty)]
|
||||
pub const PRODUCT_DISPLAY_URI: &str = if !PRODUCT_HOMEPAGE.is_empty() {
|
||||
PRODUCT_HOMEPAGE
|
||||
} else {
|
||||
PRODUCT_REPO
|
||||
};
|
||||
|
||||
/// The version of the product, in string form.
|
||||
pub const PRODUCT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Info regarding the specific version of sources used to build this product.
|
||||
pub const PRODUCT_GIT_VERSION: &str = git_version::git_version!(
|
||||
prefix = "git:",
|
||||
cargo_prefix = "cargo:",
|
||||
fallback = "unknown:",
|
||||
args = ["--always", "--dirty=-modified", "--match", ""]
|
||||
);
|
||||
|
||||
pub(crate) fn get_product_display_str() -> String {
|
||||
// nash: identify as the Nucleic agent shell fork of brush.
|
||||
std::format!(
|
||||
"{PRODUCT_NAME} (Nucleic agent shell, brush fork) {PRODUCT_VERSION} ({PRODUCT_GIT_VERSION}) - {PRODUCT_DISPLAY_URI}"
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user