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;
pub const DISPATCH_FLAG: &str = "--invoke-bundled";
pub type BundledFn = fn(args: Vec<OsString>) -> i32;
static REGISTRY: OnceLock<HashMap<String, BundledFn>> = OnceLock::new();
static SELF_EXE: OnceLock<Option<PathBuf>> = OnceLock::new();
#[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);
}
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);
}
#[must_use]
pub fn registry() -> Option<&'static HashMap<String, BundledFn>> {
REGISTRY.get()
}
#[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;
}
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));
};
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()
}
fn self_exe() -> Option<&'static PathBuf> {
SELF_EXE
.get_or_init(|| std::env::current_exe().ok())
.as_ref()
}
#[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"
)),
ContentType::ShortUsage | ContentType::ManPage => Ok(String::new()),
}
}
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());
};
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())); 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;
cmd.argv0 = Some(bundled_name);
let spawn_result = cmd.execute().await?;
let wait_result = spawn_result.wait().await?;
Ok(wait_result.into())
})
}
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,
}
}
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>());
}
}