feat: integrated coreutils as in-process shell builtins

- Integrated 17 new coreutils-based shell builtins including `diff`, `date`, `ln`, `stat`, `seq`, `touch`, and others.
- Refactored vendored utilities to execute as in-process shell builtins by routing I/O, environment access, and path resolution through `pi_uutils_ctx`.
- Disabled process-level modifications (e.g., clock setting, hostname modification) to ensure safety and scope adherence within the shell environment.
- Implemented shell-specific features such as cancellation polling, custom exit code management, and efficient output streaming for all new builtins.
This commit is contained in:
can1357
2026-07-11 20:35:56 +02:00
parent 6c292b97c3
commit 0ae8efd649
62 changed files with 13648 additions and 3 deletions
+17
View File
@@ -54,6 +54,23 @@ uu_sha512sum = { path = "../vendor/uu-sha512sum" }
uu_b2sum = { path = "../vendor/uu-b2sum" }
uu_basename = { path = "../vendor/uu-basename" }
uu_dirname = { path = "../vendor/uu-dirname" }
uu_readlink = { path = "../vendor/uu-readlink" }
uu_realpath = { path = "../vendor/uu-realpath" }
uu_touch = { path = "../vendor/uu-touch" }
uu_stat = { path = "../vendor/uu-stat" }
uu_date = { path = "../vendor/uu-date" }
uu_mktemp = { path = "../vendor/uu-mktemp" }
uu_seq = { path = "../vendor/uu-seq" }
uu_yes = { path = "../vendor/uu-yes" }
uu_printenv = { path = "../vendor/uu-printenv" }
uu_ln = { path = "../vendor/uu-ln" }
uu_truncate = { path = "../vendor/uu-truncate" }
uu_tac = { path = "../vendor/uu-tac" }
uu_nproc = { path = "../vendor/uu-nproc" }
uu_uname = { path = "../vendor/uu-uname" }
uu_whoami = { path = "../vendor/uu-whoami" }
uu_hostname = { path = "../vendor/uu-hostname" }
pi_uu_diff = { path = "../pi-uu-diff" }
uu_cut = { path = "../vendor/uu-cut" }
uu_tee = { path = "../vendor/uu-tee" }
uu_tr = { path = "../vendor/uu-tr" }
+17
View File
@@ -219,6 +219,23 @@ uutil_builtin!(pub fn sha512sum_builtin => uu_sha512sum::run);
uutil_builtin!(pub fn b2sum_builtin => uu_b2sum::run);
uutil_builtin!(pub fn basename_builtin => uu_basename::run);
uutil_builtin!(pub fn dirname_builtin => uu_dirname::run);
uutil_builtin!(pub fn readlink_builtin => uu_readlink::run);
uutil_builtin!(pub fn realpath_builtin => uu_realpath::run);
uutil_builtin!(pub fn touch_builtin => uu_touch::run);
uutil_builtin!(pub fn stat_builtin => uu_stat::run);
uutil_builtin!(pub fn date_builtin => uu_date::run);
uutil_builtin!(pub fn mktemp_builtin => uu_mktemp::run);
uutil_builtin!(pub fn seq_builtin => uu_seq::run);
uutil_builtin!(pub fn yes_builtin => uu_yes::run);
uutil_builtin!(pub fn printenv_builtin => uu_printenv::run);
uutil_builtin!(pub fn ln_builtin => uu_ln::run);
uutil_builtin!(pub fn truncate_builtin => uu_truncate::run);
uutil_builtin!(pub fn tac_builtin => uu_tac::run);
uutil_builtin!(pub fn nproc_builtin => uu_nproc::run);
uutil_builtin!(pub fn uname_builtin => uu_uname::run);
uutil_builtin!(pub fn whoami_builtin => uu_whoami::run);
uutil_builtin!(pub fn hostname_builtin => uu_hostname::run);
uutil_builtin!(pub fn diff_builtin => pi_uu_diff::run);
uutil_builtin!(pub fn cut_builtin => uu_cut::run);
uutil_builtin!(pub fn tee_builtin => uu_tee::run);
uutil_builtin!(pub fn tr_builtin => uu_tr::run);
+1
View File
@@ -4,6 +4,7 @@ mod fd;
pub mod minimizer;
pub mod process;
pub mod shell;
mod which;
#[cfg(windows)]
pub mod windows;
+19
View File
@@ -632,6 +632,23 @@ async fn create_session_for_run(
shell.register_builtin("b2sum", crate::coreutils::b2sum_builtin());
shell.register_builtin("basename", crate::coreutils::basename_builtin());
shell.register_builtin("dirname", crate::coreutils::dirname_builtin());
shell.register_builtin("readlink", crate::coreutils::readlink_builtin());
shell.register_builtin("realpath", crate::coreutils::realpath_builtin());
shell.register_builtin("touch", crate::coreutils::touch_builtin());
shell.register_builtin("stat", crate::coreutils::stat_builtin());
shell.register_builtin("date", crate::coreutils::date_builtin());
shell.register_builtin("mktemp", crate::coreutils::mktemp_builtin());
shell.register_builtin("seq", crate::coreutils::seq_builtin());
shell.register_builtin("yes", crate::coreutils::yes_builtin());
shell.register_builtin("printenv", crate::coreutils::printenv_builtin());
shell.register_builtin("truncate", crate::coreutils::truncate_builtin());
shell.register_builtin("tac", crate::coreutils::tac_builtin());
shell.register_builtin("nproc", crate::coreutils::nproc_builtin());
shell.register_builtin("uname", crate::coreutils::uname_builtin());
shell.register_builtin("whoami", crate::coreutils::whoami_builtin());
shell.register_builtin("hostname", crate::coreutils::hostname_builtin());
shell.register_builtin("which", crate::which::which_builtin());
shell.register_builtin("diff", crate::coreutils::diff_builtin());
shell.register_builtin("cut", crate::coreutils::cut_builtin());
shell.register_builtin("tee", crate::coreutils::tee_builtin());
shell.register_builtin("tr", crate::coreutils::tr_builtin());
@@ -647,6 +664,8 @@ async fn create_session_for_run(
if !uutils_env_disabled(config, "PI_DISABLE_MV_BUILTIN") {
shell.register_builtin("mv", crate::coreutils::mv_builtin());
}
// ln can clobber existing files via -f; gate it with the destructive set.
shell.register_builtin("ln", crate::coreutils::ln_builtin());
}
}
+252
View File
@@ -0,0 +1,252 @@
//! In-process `which` builtin backed by brush's PATH-search helpers.
//!
//! Follows which(1) (GNU/debianutils) semantics: each name operand is looked
//! up in the shell's `PATH`; the first match is printed (all matches with
//! `-a`). Lookup failures are silent; the exit status is 0 when every name
//! was found and 1 when any name was missing.
use std::{
ffi::OsString,
io::{self, Write},
path::{Path, PathBuf},
};
use brush_core::{
Error,
builtins::{BoxFuture, ContentOptions, ContentType, Registration},
commands::{CommandArg, ExecutionContext},
extensions::ShellExtensions,
openfiles::{OpenFile, OpenFiles, null},
pathsearch,
results::ExecutionResult,
sys,
};
use clap::{Parser, error::ErrorKind};
#[derive(Parser, Debug)]
#[command(name = "which", about = "Locate a command's executable in the shell's PATH")]
struct WhichCli {
/// Print all matching executables in PATH, not just the first.
#[arg(short = 'a', long = "all")]
all: bool,
/// Command names to locate.
#[arg(value_name = "name")]
names: Vec<String>,
}
/// Creates the `which` shell builtin registration.
pub fn which_builtin<SE: ShellExtensions>() -> Registration<SE> {
fn execute<SE: ShellExtensions>(
context: ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> BoxFuture<'_, Result<ExecutionResult, Error>> {
Box::pin(std::future::ready(Ok(run_which(context, args))))
}
Registration {
execute_func: execute::<SE>,
content_func: which_content,
disabled: false,
special_builtin: false,
declaration_builtin: false,
transparent_background_wrapper: false,
}
}
fn run_which<SE: ShellExtensions>(
context: ExecutionContext<'_, SE>,
args: Vec<CommandArg>,
) -> ExecutionResult {
let mut stdout = context
.try_fd(OpenFiles::STDOUT_FD)
.unwrap_or_else(null_sink);
let mut stderr = context
.try_fd(OpenFiles::STDERR_FD)
.unwrap_or_else(null_sink);
let cwd = context.shell.working_dir().to_path_buf();
let path_var = context
.shell
.env_str("PATH")
.map(std::borrow::Cow::into_owned)
.unwrap_or_default();
let argv: Vec<OsString> = args
.iter()
.map(|arg| OsString::from(arg.to_string()))
.collect();
let cli = match WhichCli::try_parse_from(argv) {
Ok(cli) => cli,
Err(err) => {
let rendered = err.to_string();
let code = match err.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
let _ = write!(stdout, "{rendered}");
0
},
_ => {
let _ = write!(stderr, "{rendered}");
2
},
};
return ExecutionResult::new(code);
},
};
let mut all_found = true;
for name in &cli.names {
let matches = find_matches(name, &path_var, &cwd, cli.all);
if matches.is_empty() {
// which(1) reports missing names via the exit status only.
all_found = false;
}
for path in matches {
let _ = writeln!(stdout, "{}", path.display());
}
}
ExecutionResult::new(u8::from(!all_found))
}
/// Collects the executable matches for a single `which` name operand.
///
/// A name containing a path separator is checked directly against `cwd`
/// (yielding at most one match); otherwise each `PATH` entry — with relative
/// and empty entries resolved against `cwd` — is probed in `PATH` order.
/// Returns only the first match unless `all` is set. Windows `PATHEXT`
/// resolution is handled by [`brush_core::sys::fs::resolve_executable`].
fn find_matches(name: &str, path_var: &str, cwd: &Path, all: bool) -> Vec<PathBuf> {
if sys::fs::contains_path_separator(name) {
let candidate = cwd.join(name);
if candidate.is_dir() {
return Vec::new();
}
return sys::fs::resolve_executable(candidate).into_iter().collect();
}
let dirs = sys::fs::split_paths(path_var).map(|dir| {
if dir.as_os_str().is_empty() {
// POSIX: an empty PATH entry names the current directory.
cwd.to_path_buf()
} else if dir.is_relative() {
cwd.join(dir)
} else {
dir
}
});
let mut found = pathsearch::search_for_executable(dirs, name);
if all {
found.collect()
} else {
found.next().into_iter().collect()
}
}
fn null_sink() -> OpenFile {
null().unwrap_or_else(|_| OpenFile::from(io::stdout()))
}
#[allow(
clippy::unnecessary_wraps,
reason = "signature must match brush's CommandContentFunc fn pointer"
)]
fn which_content(
_name: &str,
_content_type: ContentType,
_options: &ContentOptions,
) -> Result<String, Error> {
Ok("which: which [-a] name [name ...]\n".to_string())
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use std::{
env, fs,
os::unix::fs::PermissionsExt,
path::PathBuf,
sync::atomic::{AtomicUsize, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
use super::find_matches;
static COUNTER: AtomicUsize = AtomicUsize::new(0);
/// Creates a fresh, canonicalized temp directory (macOS `/var` is a
/// symlink; canonicalizing keeps constructed and probed paths identical).
fn temp_root(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
let root = env::temp_dir().join(format!(
"pi-shell-which-{tag}-{}-{}-{}",
std::process::id(),
nanos,
COUNTER.fetch_add(1, Ordering::Relaxed),
));
fs::create_dir_all(&root).expect("temp dir should be created");
fs::canonicalize(&root).expect("temp dir should canonicalize")
}
fn place_file(dir: &std::path::Path, name: &str, executable: bool) -> PathBuf {
let path = dir.join(name);
fs::write(&path, b"#!/bin/sh\n").expect("file should be written");
let mode = if executable { 0o755 } else { 0o644 };
fs::set_permissions(&path, fs::Permissions::from_mode(mode))
.expect("permissions should be set");
path
}
#[test]
fn finds_only_executable_files() {
let dir = temp_root("exec-only");
let tool = place_file(&dir, "tool", true);
place_file(&dir, "blob", false);
let path_var = dir.display().to_string();
assert_eq!(find_matches("tool", &path_var, &dir, false), vec![tool]);
assert!(find_matches("blob", &path_var, &dir, false).is_empty());
assert!(find_matches("missing", &path_var, &dir, false).is_empty());
}
#[test]
fn all_flag_returns_matches_in_path_order() {
let dir_a = temp_root("all-a");
let dir_b = temp_root("all-b");
let tool_a = place_file(&dir_a, "tool", true);
let tool_b = place_file(&dir_b, "tool", true);
let path_var = format!("{}:{}", dir_a.display(), dir_b.display());
let cwd = temp_root("all-cwd");
assert_eq!(find_matches("tool", &path_var, &cwd, true), vec![tool_a.clone(), tool_b]);
// Without -a only the first PATH entry's match is returned.
assert_eq!(find_matches("tool", &path_var, &cwd, false), vec![tool_a]);
}
#[test]
fn name_with_separator_resolves_against_cwd() {
let cwd = temp_root("slash");
let bin = cwd.join("bin");
fs::create_dir_all(&bin).expect("bin dir should be created");
let tool = place_file(&bin, "tool", true);
place_file(&bin, "blob", false);
// PATH is irrelevant for names containing a separator.
assert_eq!(find_matches("bin/tool", "", &cwd, false), vec![tool]);
assert!(find_matches("bin/blob", "", &cwd, false).is_empty());
// A directory is never a match, even with execute bits set.
assert!(find_matches("./bin", "", &cwd, false).is_empty());
}
#[test]
fn relative_path_entries_resolve_against_cwd() {
let cwd = temp_root("rel-entry");
let bin = cwd.join("bin");
fs::create_dir_all(&bin).expect("bin dir should be created");
let tool = place_file(&bin, "tool", true);
assert_eq!(find_matches("tool", "bin", &cwd, false), vec![tool]);
}
}