Merge nucleic/sleek-thistle-egret-fyej into dev

This commit is contained in:
2026-07-18 05:19:31 -07:00
commit b6be87b72d
677 changed files with 102939 additions and 0 deletions
+304
View File
@@ -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());
}
}
+242
View File
@@ -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())
}
}
}
}
}
+289
View File
@@ -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>());
}
}
+444
View File
@@ -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);
}
}
+819
View File
@@ -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
}
}
+20
View File
@@ -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")
}
}
+184
View File
@@ -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())
}
}
}
+12
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
//! Main entry for the `brush` shell.
fn main() {
brush_shell::entry::run();
}
+34
View File
@@ -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}"
)
}