diff --git a/brush-core/src/builtins/command.rs b/brush-core/src/builtins/command.rs index bc0c2ea3..ebd3f885 100644 --- a/brush-core/src/builtins/command.rs +++ b/brush-core/src/builtins/command.rs @@ -3,6 +3,11 @@ use std::{fmt::Display, io::Write, path::Path}; use crate::{builtins, commands, error, shell, sys::fs::PathExt, ExecutionResult}; +/// The value for PATH when invoking `command -p`. This is only used when +/// the Posix.2 `confstr()` returns nothing +/// The value of this variable is taken from the BASH source code. +const STANDARD_UTILS_PATH: &[&str] = &["/bin", "/usr/bin", "/sbin", "/usr/sbin", "/etc:/usr/etc"]; + /// Directly invokes an external command, without going through typical search order. #[derive(Parser)] pub(crate) struct CommandCommand { @@ -32,10 +37,6 @@ impl builtins::Command for CommandCommand { &self, context: commands::ExecutionContext<'_>, ) -> Result { - if self.use_default_path { - return error::unimp("command -p"); - } - if self.print_description || self.print_verbose_description { if let Some(found_cmd) = self.try_find_command(context.shell) { if self.print_description { @@ -105,6 +106,21 @@ impl CommandCommand { } } + if self.use_default_path { + let path = confstr_path(); + // Without an allocation if possible. + let path = path.as_ref().map(|p| String::from_utf8_lossy(p)); + let path = path.as_ref().map_or( + itertools::Either::Right(STANDARD_UTILS_PATH.iter().copied()), + |p| itertools::Either::Left(p.split(':')), + ); + + return shell + .find_executables_in(path, self.command_name.as_str()) + .first() + .map(|path| FoundCommand::External(path.to_string_lossy().to_string())); + } + shell .find_executables_in_path(self.command_name.as_str()) .first() @@ -148,3 +164,38 @@ impl CommandCommand { } } } + +/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which +/// indicates where all the POSIX.2 standard utilities can be found. +fn confstr_path() -> Option> { + #[cfg(unix)] + { + let required_size = + unsafe { nix::libc::confstr(nix::libc::_CS_PATH, std::ptr::null_mut(), 0) }; + if required_size == 0 { + return None; + } + // NOTE: Writing `c_char` (i8 or u8 depending on the platform) into `Vec` is fine, + // as i8 and u8 have compatible representations, + // and Rust does not support platforms where `c_char` is not 8-bit wide. + let mut buffer = Vec::::with_capacity(required_size); + let final_size = unsafe { + nix::libc::confstr( + nix::libc::_CS_PATH, + buffer.as_mut_ptr().cast(), + required_size, + ) + }; + if final_size == 0 { + return None; + } + // ERANGE + if final_size > required_size { + return None; + } + unsafe { buffer.set_len(final_size - 1) }; // The last byte is a null terminator. + return Some(buffer); + } + #[allow(unreachable_code)] + None +} diff --git a/brush-core/src/shell.rs b/brush-core/src/shell.rs index 3190bed0..1fea90a1 100644 --- a/brush-core/src/shell.rs +++ b/brush-core/src/shell.rs @@ -879,12 +879,30 @@ impl Shell { /// # Arguments /// /// * `required_glob_pattern` - The glob pattern to match against. - #[allow(clippy::manual_flatten)] pub fn find_executables_in_path(&self, required_glob_pattern: &str) -> Vec { + self.find_executables_in( + self.env.get_str("PATH").unwrap_or_default().split(':'), + required_glob_pattern, + ) + } + + /// Finds executables in the given paths, matching the given glob pattern. + /// + /// # Arguments + /// + /// * `paths` - The paths to search in + /// * `required_glob_pattern` - The glob pattern to match against. + #[allow(clippy::manual_flatten)] + pub fn find_executables_in<'a, T: AsRef>( + &self, + paths: impl Iterator, + required_glob_pattern: &str, + ) -> Vec { let is_executable = |path: &Path| path.executable(); let mut executables = vec![]; - for dir_str in self.env.get_str("PATH").unwrap_or_default().split(':') { + for dir_str in paths { + let dir_str = dir_str.as_ref(); let pattern = std::format!("{dir_str}/{required_glob_pattern}"); // TODO: Pass through quoting. if let Ok(entries) = patterns::Pattern::from(pattern).expand(