Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 55 additions & 4 deletions brush-core/src/builtins/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -32,10 +37,6 @@ impl builtins::Command for CommandCommand {
&self,
context: commands::ExecutionContext<'_>,
) -> Result<builtins::ExitCode, error::Error> {
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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<Vec<u8>> {
#[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<u8>` 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::<u8>::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
}
22 changes: 20 additions & 2 deletions brush-core/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf> {
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<T: AsRef<str>>(
&self,
paths: impl Iterator<Item = T>,
required_glob_pattern: &str,
) -> Vec<PathBuf> {
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(
Expand Down