Skip to content

Commit 62bd2e7

Browse files
committed
chore: rebase + adapt previous changes
Adjusts previous changes to address earlier PR feedback; adds tests.
1 parent 2756df8 commit 62bd2e7

File tree

7 files changed

+149
-69
lines changed

7 files changed

+149
-69
lines changed

brush-core/src/builtins/command.rs

Lines changed: 19 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
use clap::Parser;
22
use std::{fmt::Display, io::Write, path::Path};
33

4-
use crate::{builtins, commands, error, shell, sys::fs::PathExt, ExecutionResult};
5-
6-
/// The value for PATH when invoking `command -p`. This is only used when
7-
/// the Posix.2 `confstr()` returns nothing
8-
/// The value of this variable is taken from the BASH source code.
9-
const STANDARD_UTILS_PATH: &[&str] = &["/bin", "/usr/bin", "/sbin", "/usr/sbin", "/etc:/usr/etc"];
4+
use crate::{builtins, commands, error, shell, sys, sys::fs::PathExt, ExecutionResult};
105

116
/// Directly invokes an external command, without going through typical search order.
127
#[derive(Parser)]
@@ -67,7 +62,8 @@ impl builtins::Command for CommandCommand {
6762
Ok(builtins::ExitCode::Custom(1))
6863
}
6964
} else {
70-
self.execute_command(context, command_name).await
65+
self.execute_command(context, command_name, self.use_default_path)
66+
.await
7167
}
7268
} else {
7369
Ok(builtins::ExitCode::Success)
@@ -118,34 +114,34 @@ impl CommandCommand {
118114
}
119115

120116
if use_default_path {
121-
let path = confstr_path();
122-
// Without an allocation if possible.
123-
let path = path.as_ref().map(|p| String::from_utf8_lossy(p));
124-
let path = path.as_ref().map_or(
125-
itertools::Either::Right(STANDARD_UTILS_PATH.iter().copied()),
126-
|p| itertools::Either::Left(p.split(':')),
127-
);
128-
129-
return shell
130-
.find_executables_in(path, command_name)
117+
let dirs = sys::fs::get_default_standard_utils_paths();
118+
shell
119+
.find_executables_in(dirs.iter(), command_name)
131120
.first()
132-
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()));
121+
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()))
122+
} else {
123+
shell
124+
.find_first_executable_in_path_using_cache(command_name)
125+
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()))
133126
}
134-
135-
shell
136-
.find_first_executable_in_path_using_cache(command_name)
137-
.map(|path| FoundCommand::External(path.to_string_lossy().to_string()))
138127
}
139128
}
140129

141130
async fn execute_command(
142131
&self,
143132
mut context: commands::ExecutionContext<'_>,
144133
command_name: &str,
134+
use_default_path: bool,
145135
) -> Result<builtins::ExitCode, error::Error> {
146136
command_name.clone_into(&mut context.command_name);
147137
let command_and_args = self.command_and_args.iter().map(|arg| arg.into()).collect();
148138

139+
let path_dirs = if use_default_path {
140+
Some(sys::fs::get_default_standard_utils_paths())
141+
} else {
142+
None
143+
};
144+
149145
// We do not have an existing process group to place this into.
150146
let mut pgid = None;
151147

@@ -156,6 +152,7 @@ impl CommandCommand {
156152
&mut pgid,
157153
command_and_args,
158154
false, /* use functions? */
155+
path_dirs,
159156
)
160157
.await?
161158
{
@@ -177,38 +174,3 @@ impl CommandCommand {
177174
}
178175
}
179176
}
180-
181-
/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
182-
/// indicates where all the POSIX.2 standard utilities can be found.
183-
fn confstr_path() -> Option<Vec<u8>> {
184-
#[cfg(unix)]
185-
{
186-
let required_size =
187-
unsafe { nix::libc::confstr(nix::libc::_CS_PATH, std::ptr::null_mut(), 0) };
188-
if required_size == 0 {
189-
return None;
190-
}
191-
// NOTE: Writing `c_char` (i8 or u8 depending on the platform) into `Vec<u8>` is fine,
192-
// as i8 and u8 have compatible representations,
193-
// and Rust does not support platforms where `c_char` is not 8-bit wide.
194-
let mut buffer = Vec::<u8>::with_capacity(required_size);
195-
let final_size = unsafe {
196-
nix::libc::confstr(
197-
nix::libc::_CS_PATH,
198-
buffer.as_mut_ptr().cast(),
199-
required_size,
200-
)
201-
};
202-
if final_size == 0 {
203-
return None;
204-
}
205-
// ERANGE
206-
if final_size > required_size {
207-
return None;
208-
}
209-
unsafe { buffer.set_len(final_size - 1) }; // The last byte is a null terminator.
210-
return Some(buffer);
211-
}
212-
#[allow(unreachable_code)]
213-
None
214-
}

brush-core/src/commands.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ pub(crate) async fn execute(
273273
process_group_id: &mut Option<i32>,
274274
args: Vec<CommandArg>,
275275
use_functions: bool,
276+
path_dirs: Option<Vec<String>>,
276277
) -> Result<CommandSpawnResult, error::Error> {
277278
if !cmd_context.command_name.contains(std::path::MAIN_SEPARATOR) {
278279
let builtin = cmd_context
@@ -307,10 +308,19 @@ pub(crate) async fn execute(
307308
}
308309
}
309310

310-
if let Some(path) = cmd_context
311-
.shell
312-
.find_first_executable_in_path_using_cache(&cmd_context.command_name)
313-
{
311+
let path = if let Some(path_dirs) = path_dirs {
312+
cmd_context
313+
.shell
314+
.find_executables_in(path_dirs.iter(), &cmd_context.command_name)
315+
.first()
316+
.cloned()
317+
} else {
318+
cmd_context
319+
.shell
320+
.find_first_executable_in_path_using_cache(&cmd_context.command_name)
321+
};
322+
323+
if let Some(path) = path {
314324
let resolved_path = path.to_string_lossy();
315325
execute_external_command(
316326
cmd_context,

brush-core/src/interp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
11161116
&mut context.process_group_id,
11171117
args,
11181118
true, /* use functions? */
1119+
None,
11191120
)
11201121
.await;
11211122

brush-core/src/shell.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -540,12 +540,9 @@ impl Shell {
540540
// PATH (if not already set)
541541
#[cfg(unix)]
542542
if !self.env.is_set("PATH") {
543-
self.env.set_global(
544-
"PATH",
545-
ShellVariable::new(
546-
"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".into(),
547-
),
548-
)?;
543+
let default_path_str = sys::fs::get_default_executable_search_paths().join(":");
544+
self.env
545+
.set_global("PATH", ShellVariable::new(default_path_str.into()))?;
549546
}
550547

551548
// PIPESTATUS
@@ -1316,10 +1313,8 @@ impl Shell {
13161313
let is_executable = |path: &Path| path.is_file() && path.executable();
13171314

13181315
let mut executables = vec![];
1319-
13201316
for dir_str in paths {
13211317
let dir_str = dir_str.as_ref();
1322-
13231318
let pattern =
13241319
patterns::Pattern::from(std::format!("{dir_str}/{required_glob_pattern}"))
13251320
.set_extended_globbing(self.options.extended_globbing)

brush-core/src/sys/stubs/fs.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ pub(crate) trait StubMetadataExt {
5252
}
5353

5454
impl StubMetadataExt for std::fs::Metadata {}
55+
56+
pub(crate) fn get_default_executable_search_paths() -> Vec<String> {
57+
vec![]
58+
}
59+
60+
pub(crate) fn get_default_standard_utils_paths() -> Vec<String> {
61+
vec![]
62+
}

brush-core/src/sys/unix/fs.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
1+
use std::os::unix::ffi::OsStringExt;
12
use std::os::unix::fs::{FileTypeExt, MetadataExt};
23
use std::path::Path;
34

5+
const DEFAULT_EXECUTABLE_SEARCH_PATHS: &[&str] = &[
6+
"/usr/local/sbin",
7+
"/usr/local/bin",
8+
"/usr/sbin",
9+
"/usr/bin",
10+
"/sbin",
11+
"/bin",
12+
];
13+
14+
const DEFAULT_STANDARD_UTILS_PATHS: &[&str] =
15+
&["/bin", "/usr/bin", "/sbin", "/usr/sbin", "/etc", "/usr/etc"];
16+
417
impl crate::sys::fs::PathExt for Path {
518
fn readable(&self) -> bool {
619
nix::unistd::access(self, nix::unistd::AccessFlags::R_OK).is_ok()
@@ -56,3 +69,78 @@ fn try_get_file_type(path: &Path) -> Option<std::fs::FileType> {
5669
fn try_get_file_mode(path: &Path) -> Option<u32> {
5770
path.metadata().map(|metadata| metadata.mode()).ok()
5871
}
72+
73+
pub(crate) fn get_default_executable_search_paths() -> Vec<String> {
74+
DEFAULT_EXECUTABLE_SEARCH_PATHS
75+
.iter()
76+
.map(|s| (*s).to_owned())
77+
.collect()
78+
}
79+
80+
/// Retrieves the platform-specific set of paths that should contain standard system
81+
/// utilities. Used by `command -p`, for example.
82+
pub(crate) fn get_default_standard_utils_paths() -> Vec<String> {
83+
//
84+
// Try to call confstr(_CS_PATH). If that fails, can't find a string value, or
85+
// finds an empty string, then we'll fall back to hard-coded defaults.
86+
//
87+
88+
if let Ok(Some(cs_path)) = confstr_cs_path() {
89+
if !cs_path.is_empty() {
90+
return cs_path.split(':').map(|s| s.to_string()).collect();
91+
}
92+
}
93+
94+
DEFAULT_STANDARD_UTILS_PATHS
95+
.iter()
96+
.map(|s| (*s).to_owned())
97+
.collect()
98+
}
99+
100+
fn confstr_cs_path() -> Result<Option<String>, std::io::Error> {
101+
let value = confstr(nix::libc::_CS_PATH)?;
102+
103+
if let Some(value) = value {
104+
let value_str = value
105+
.into_string()
106+
.map_err(|_err| std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid data"))?;
107+
Ok(Some(value_str))
108+
} else {
109+
Ok(None)
110+
}
111+
}
112+
113+
/// A wrapper for [`nix::libc::confstr`]. Returns a value for the default PATH variable which
114+
/// indicates where all the POSIX.2 standard utilities can be found.
115+
///
116+
/// N.B. We would strongly prefer to use a safe API exposed (in an idiomatic way) by nix
117+
/// or similar. Until that exists, we accept the need to make the unsafe call directly.
118+
fn confstr(name: nix::libc::c_int) -> Result<Option<std::ffi::OsString>, std::io::Error> {
119+
let required_size = unsafe { nix::libc::confstr(name, std::ptr::null_mut(), 0) };
120+
121+
// When confstr returns 0, it either means there's no value associated with _CS_PATH, or
122+
// _CS_PATH is considered invalid (and not present) on this platform. In both cases, we
123+
// treat it as a non-existent value and return None.
124+
if required_size == 0 {
125+
return Ok(None);
126+
}
127+
128+
let mut buffer = Vec::<u8>::with_capacity(required_size);
129+
130+
// NOTE: Writing `c_char` (i8 or u8 depending on the platform) into `Vec<u8>` is fine,
131+
// as i8 and u8 have compatible representations,
132+
// and Rust does not support platforms where `c_char` is not 8-bit wide.
133+
let final_size =
134+
unsafe { nix::libc::confstr(name, buffer.as_mut_ptr().cast(), buffer.capacity()) };
135+
136+
if final_size == 0 {
137+
return Err(std::io::Error::last_os_error());
138+
}
139+
140+
unsafe { buffer.set_len(final_size) };
141+
142+
// The last byte is a null terminator.
143+
buffer.pop();
144+
145+
Ok(Some(std::ffi::OsString::from_vec(buffer)))
146+
}

brush-shell/tests/cases/builtins/command.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ cases:
3131
echo "[/usr/bin/non-existent]"
3232
command -v /usr/bin/non-existent || echo "2. Not found"
3333
34+
- name: "command -v -p"
35+
stdin: |
36+
unset PATH
37+
38+
echo "[no -p]"
39+
command -v cat
40+
41+
echo "[-p]"
42+
command -v -p cat
43+
3444
- name: "command -v with full paths"
3545
skip: true # TODO: investigate why this fails on arch linux
3646
stdin: |
@@ -57,3 +67,9 @@ cases:
5767
- name: "command with --help"
5868
stdin: |
5969
command ls --help
70+
71+
- name: "command with -p"
72+
stdin: |
73+
unset PATH
74+
75+
command -p -- ls

0 commit comments

Comments
 (0)