From 959e80ebc51c5b2acc5ca5f70a2da08a84eeef40 Mon Sep 17 00:00:00 2001 From: caomengxuan666 <2507560089@qq.com> Date: Thu, 4 Jun 2026 20:30:22 +0800 Subject: [PATCH] Add Windows-native which command --- Cargo.lock | 9 ++ Cargo.toml | 1 + deps/ntwhich/Cargo.toml | 21 +++ deps/ntwhich/src/main.rs | 3 + deps/ntwhich/src/which.rs | 278 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 312 insertions(+) create mode 100644 deps/ntwhich/Cargo.toml create mode 100644 deps/ntwhich/src/main.rs create mode 100644 deps/ntwhich/src/which.rs diff --git a/Cargo.lock b/Cargo.lock index 79e71f5..fea33e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,7 @@ dependencies = [ "uu_unlink", "uu_uptime", "uu_wc", + "uu_which", "uu_yes", "uucore", "windows-sys 0.59.0", @@ -3444,6 +3445,14 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_which" +version = "2026.5.29" +dependencies = [ + "clap", + "uucore", +] + [[package]] name = "uu_yes" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 5e97da5..c4932f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,7 @@ uptime = { package = "uu_uptime", path = "deps/coreutils/src/uu/uptime" } findutils = { package = "findutils", path = "deps/findutils" } grep = { package = "uu_grep", path = "deps/grep" } ntfind = { package = "find", path = "deps/ntfind" } +ntwhich = { package = "uu_which", path = "deps/ntwhich" } # For registry access in main.rs [dependencies.windows-sys] diff --git a/deps/ntwhich/Cargo.toml b/deps/ntwhich/Cargo.toml new file mode 100644 index 0000000..7a55c14 --- /dev/null +++ b/deps/ntwhich/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "uu_which" +description = "which ~ locate a command in PATH" +repository = "https://github.com/microsoft/coreutils" +version = "2026.5.29" +license = "MIT" +edition = "2024" +rust-version = "1.88.0" +publish = false + +[lib] +path = "src/which.rs" +doctest = false + +[dependencies] +clap = { version = "4.5", features = ["wrap_help", "cargo"] } +uucore = { path = "../coreutils/src/uucore" } + +[[bin]] +name = "which" +path = "src/main.rs" diff --git a/deps/ntwhich/src/main.rs b/deps/ntwhich/src/main.rs new file mode 100644 index 0000000..499d16f --- /dev/null +++ b/deps/ntwhich/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + std::process::exit(uu_which::uumain(uucore::args_os())); +} diff --git a/deps/ntwhich/src/which.rs b/deps/ntwhich/src/which.rs new file mode 100644 index 0000000..a96be76 --- /dev/null +++ b/deps/ntwhich/src/which.rs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::io::{self, Write as _}; +use std::path::{Path, PathBuf}; + +use clap::{Arg, ArgAction, Command}; +use uucore::Args; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub fn uumain(args: impl Args) -> i32 { + match uumain_impl(args) { + Ok(code) => code, + Err(err) => { + let _ = writeln!(io::stderr(), "which: {err}"); + 1 + } + } +} + +fn uumain_impl(args: impl Args) -> Result { + let matches = match uu_app().try_get_matches_from(args) { + Ok(matches) => matches, + Err(err) => { + let _ = err.print(); + return Ok(if err.use_stderr() { 1 } else { 0 }); + } + }; + + let all = matches.get_flag("all"); + let Some(commands) = matches.get_many::("commands") else { + return Err("missing command operand".to_string()); + }; + + let mut all_found = true; + for command in commands { + let hits = find_command(command, all); + if hits.is_empty() { + all_found = false; + continue; + } + + for hit in hits { + println!("{}", hit.display()); + } + } + + Ok(if all_found { 0 } else { 1 }) +} + +pub fn uu_app() -> Command { + Command::new("which") + .version(VERSION) + .about("Locate a command in PATH.") + .override_usage("which [OPTION]... COMMAND...") + .arg( + Arg::new("all") + .short('a') + .long("all") + .help("print all matching pathnames of each command") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("commands") + .value_name("COMMAND") + .num_args(1..) + .value_parser(clap::value_parser!(OsString)), + ) +} + +fn find_command(command: &OsStr, all: bool) -> Vec { + let mut hits = Vec::new(); + let mut seen = HashSet::new(); + let pathext = pathext(); + + let mut push_hit = |path: PathBuf| { + let key = normalize_seen_key(&path); + if seen.insert(key) { + hits.push(path); + } + }; + + if has_path_separator(Path::new(command)) { + for candidate in candidates(PathBuf::from(command), &pathext) { + if is_regular_file(&candidate) { + push_hit(candidate); + if !all { + return hits; + } + } + } + return hits; + } + + let Some(path_var) = env::var_os("PATH") else { + return hits; + }; + + for dir in env::split_paths(&path_var) { + if dir.as_os_str().is_empty() { + continue; + } + + let base = dir.join(command); + for candidate in candidates(base, &pathext) { + if is_regular_file(&candidate) { + push_hit(candidate); + if !all { + return hits; + } + } + } + } + + hits +} + +fn candidates(base: PathBuf, pathext: &[OsString]) -> Vec { + let mut out = vec![base.clone()]; + if base.extension().is_some() { + return out; + } + + for ext in pathext { + let mut candidate = base.clone(); + candidate.as_mut_os_string().push(ext); + out.push(candidate); + } + + out +} + +fn pathext() -> Vec { + let Some(value) = env::var_os("PATHEXT") else { + return default_pathext(); + }; + + let mut out = Vec::new(); + for ext in value.to_string_lossy().split(';') { + if ext.is_empty() { + continue; + } + + let ext = if ext.starts_with('.') { + ext.to_string() + } else { + format!(".{ext}") + }; + out.push(OsString::from(ext)); + } + + if out.is_empty() { + default_pathext() + } else { + out + } +} + +fn default_pathext() -> Vec { + [".COM", ".EXE", ".BAT", ".CMD"] + .into_iter() + .map(OsString::from) + .collect() +} + +fn has_path_separator(path: &Path) -> bool { + let path = path.as_os_str().to_string_lossy(); + path.contains('/') || path.contains('\\') +} + +fn is_regular_file(path: &Path) -> bool { + path.metadata().is_ok_and(|metadata| metadata.is_file()) +} + +fn normalize_seen_key(path: &Path) -> String { + path.as_os_str().to_string_lossy().to_lowercase() +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::sync::Mutex; + use std::time::{SystemTime, UNIX_EPOCH}; + + use super::*; + + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + struct EnvGuard { + key: &'static str, + old: Option, + } + + impl EnvGuard { + fn set(key: &'static str, value: impl Into) -> Self { + let old = env::var_os(key); + unsafe { + env::set_var(key, value.into()); + } + Self { key, old } + } + } + + impl Drop for EnvGuard { + fn drop(&mut self) { + unsafe { + if let Some(old) = &self.old { + env::set_var(self.key, old); + } else { + env::remove_var(self.key); + } + } + } + } + + fn temp_dir() -> PathBuf { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = env::temp_dir().join(format!("ntwhich-test-{unique}")); + fs::create_dir(&path).unwrap(); + path + } + + #[test] + fn finds_first_match_with_pathext() { + let _lock = TEST_LOCK.lock().unwrap(); + let dir = temp_dir(); + fs::write(dir.join("tool.EXE"), []).unwrap(); + let _path = EnvGuard::set("PATH", dir.as_os_str()); + let _pathext = EnvGuard::set("PATHEXT", ".EXE"); + + let hits = find_command(OsStr::new("tool"), false); + + assert_eq!(hits, vec![dir.join("tool.EXE")]); + fs::remove_dir_all(dir).unwrap(); + } + + #[test] + fn all_returns_all_matches() { + let _lock = TEST_LOCK.lock().unwrap(); + let dir1 = temp_dir(); + let dir2 = temp_dir(); + fs::write(dir1.join("tool.CMD"), []).unwrap(); + fs::write(dir2.join("tool.CMD"), []).unwrap(); + let joined = env::join_paths([dir1.as_os_str(), dir2.as_os_str()]).unwrap(); + let _path = EnvGuard::set("PATH", joined); + let _pathext = EnvGuard::set("PATHEXT", ".CMD"); + + let hits = find_command(OsStr::new("tool"), true); + + assert_eq!(hits, vec![dir1.join("tool.CMD"), dir2.join("tool.CMD")]); + fs::remove_dir_all(dir1).unwrap(); + fs::remove_dir_all(dir2).unwrap(); + } + + #[test] + fn searches_explicit_relative_path() { + let _lock = TEST_LOCK.lock().unwrap(); + let dir = temp_dir(); + let old_dir = env::current_dir().unwrap(); + fs::create_dir(dir.join("bin")).unwrap(); + fs::write(dir.join("bin").join("tool.EXE"), []).unwrap(); + let _pathext = EnvGuard::set("PATHEXT", ".EXE"); + env::set_current_dir(&dir).unwrap(); + + let hits = find_command(OsStr::new("bin/tool"), false); + + env::set_current_dir(old_dir).unwrap(); + assert_eq!(hits, vec![PathBuf::from("bin/tool.EXE")]); + fs::remove_dir_all(dir).unwrap(); + } +}