From: DongHun Kwak Date: Thu, 16 Mar 2023 05:52:04 +0000 (+0900) Subject: Import which 4.4.0 X-Git-Tag: upstream/4.4.0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=refs%2Fheads%2Fupstream;p=platform%2Fupstream%2Frust-which.git Import which 4.4.0 --- 9a8794b9bf07d38504a97548939d334b3f696382 diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json new file mode 100644 index 0000000..0dcc2ff --- /dev/null +++ b/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "13722854453a50002d72faa1d0960f70b68ceea4" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..aa13ec5 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,93 @@ +name: Main workflow +on: + push: + pull_request: + +jobs: + # Run the `rustfmt` code formatter + rustfmt: + name: Rustfmt [Formatter] + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout + uses: actions/checkout@v2 + + - name: Setup | Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + components: rustfmt + + - name: Build | Format + run: cargo fmt --all -- --check + + # Run the `clippy` linting tool + clippy: + name: Clippy [Linter] + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Setup | Checkout + uses: actions/checkout@v2 + + - name: Setup | Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + components: clippy + + - name: Build | Lint + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets --all-features -- -Dwarnings + + # Ensure that the project could be successfully compiled + cargo_check: + name: Compile + runs-on: ubuntu-latest + steps: + - name: Setup | Checkout + uses: actions/checkout@v2 + + - name: Setup | Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Build | Check + run: cargo check --workspace + + # Run tests on Linux, macOS, and Windows + # On both Rust stable and Rust nightly + test: + name: Test Suite + runs-on: ${{ matrix.os }} + needs: cargo_check # First check then run expansive tests + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + rust: [stable, nightly] + steps: + - name: Setup | Checkout + uses: actions/checkout@v2 + + - name: Setup | Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + profile: minimal + override: true + + # Run the ignored tests that expect the above setup + - name: Build | Test + run: cargo test --workspace --all-features -- -Z unstable-options --include-ignored diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..865d4a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +.vscode/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ff6894a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "which" +version = "4.4.0" +authors = ["Harry Fei "] +description = "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." +documentation = "https://docs.rs/which/" +readme = "README.md" +keywords = [ + "which", + "which-rs", + "unix", + "command", +] +categories = [ + "os", + "filesystem", +] +license = "MIT" +repository = "https://github.com/harryfei/which-rs.git" + +[package.metadata.docs.rs] +all-features = true + +[dependencies.either] +version = "1.6.1" + +[dependencies.libc] +version = "0.2.121" + +[dependencies.regex] +version = "1.5.5" +optional = true + +[dev-dependencies.tempfile] +version = "3.3.0" + +[target."cfg(windows)".dependencies.once_cell] +version = "1" diff --git a/Cargo.toml.orig b/Cargo.toml.orig new file mode 100644 index 0000000..e05adb3 --- /dev/null +++ b/Cargo.toml.orig @@ -0,0 +1,26 @@ +[package] +name = "which" +version = "4.4.0" +edition = "2018" +authors = ["Harry Fei "] +repository = "https://github.com/harryfei/which-rs.git" +documentation = "https://docs.rs/which/" +license = "MIT" +description = "A Rust equivalent of Unix command \"which\". Locate installed executable in cross platforms." +readme = "README.md" +categories = ["os", "filesystem"] +keywords = ["which", "which-rs", "unix", "command"] + +[dependencies] +either = "1.6.1" +libc = "0.2.121" +regex = { version = "1.5.5", optional = true } + +[target.'cfg(windows)'.dependencies] +once_cell = "1" + +[dev-dependencies] +tempfile = "3.3.0" + +[package.metadata.docs.rs] +all-features = true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..369139b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 fangyuanziti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5615c36 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +[![Build Status](https://github.com/harryfei/which-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/harryfei/which-rs/actions/workflows/rust.yml) + +# which + +A Rust equivalent of Unix command "which". Locate installed executable in cross platforms. + +## Support platforms + +* Linux +* Windows +* macOS + +## Examples + +1) To find which rustc executable binary is using. + + ``` rust + use which::which; + + let result = which("rustc").unwrap(); + assert_eq!(result, PathBuf::from("/usr/bin/rustc")); + ``` + +2. After enabling the `regex` feature, find all cargo subcommand executables on the path: + + ``` rust + use which::which_re; + + which_re(Regex::new("^cargo-.*").unwrap()).unwrap() + .for_each(|pth| println!("{}", pth.to_string_lossy())); + ``` + +## Documentation + +The documentation is [available online](https://docs.rs/which/). diff --git a/src/checker.rs b/src/checker.rs new file mode 100644 index 0000000..0e92c6a --- /dev/null +++ b/src/checker.rs @@ -0,0 +1,79 @@ +use crate::finder::Checker; +#[cfg(unix)] +use std::ffi::CString; +use std::fs; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +pub struct ExecutableChecker; + +impl ExecutableChecker { + pub fn new() -> ExecutableChecker { + ExecutableChecker + } +} + +impl Checker for ExecutableChecker { + #[cfg(unix)] + fn is_valid(&self, path: &Path) -> bool { + CString::new(path.as_os_str().as_bytes()) + .map(|c| unsafe { libc::access(c.as_ptr(), libc::X_OK) == 0 }) + .unwrap_or(false) + } + + #[cfg(windows)] + fn is_valid(&self, _path: &Path) -> bool { + true + } +} + +pub struct ExistedChecker; + +impl ExistedChecker { + pub fn new() -> ExistedChecker { + ExistedChecker + } +} + +impl Checker for ExistedChecker { + #[cfg(target_os = "windows")] + fn is_valid(&self, path: &Path) -> bool { + fs::symlink_metadata(path) + .map(|metadata| { + let file_type = metadata.file_type(); + file_type.is_file() || file_type.is_symlink() + }) + .unwrap_or(false) + } + + #[cfg(not(target_os = "windows"))] + fn is_valid(&self, path: &Path) -> bool { + fs::metadata(path) + .map(|metadata| metadata.is_file()) + .unwrap_or(false) + } +} + +pub struct CompositeChecker { + checkers: Vec>, +} + +impl CompositeChecker { + pub fn new() -> CompositeChecker { + CompositeChecker { + checkers: Vec::new(), + } + } + + pub fn add_checker(mut self, checker: Box) -> CompositeChecker { + self.checkers.push(checker); + self + } +} + +impl Checker for CompositeChecker { + fn is_valid(&self, path: &Path) -> bool { + self.checkers.iter().all(|checker| checker.is_valid(path)) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..6d800a6 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,26 @@ +use std::fmt; + +pub type Result = std::result::Result; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Error { + BadAbsolutePath, + BadRelativePath, + CannotFindBinaryPath, + CannotGetCurrentDir, + CannotCanonicalize, +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::BadAbsolutePath => write!(f, "bad absolute path"), + Error::BadRelativePath => write!(f, "bad relative path"), + Error::CannotFindBinaryPath => write!(f, "cannot find binary path"), + Error::CannotGetCurrentDir => write!(f, "cannot get current directory"), + Error::CannotCanonicalize => write!(f, "cannot canonicalize path"), + } + } +} diff --git a/src/finder.rs b/src/finder.rs new file mode 100644 index 0000000..858a224 --- /dev/null +++ b/src/finder.rs @@ -0,0 +1,232 @@ +use crate::checker::CompositeChecker; +use crate::error::*; +#[cfg(windows)] +use crate::helper::has_executable_extension; +use either::Either; +#[cfg(feature = "regex")] +use regex::Regex; +#[cfg(feature = "regex")] +use std::borrow::Borrow; +use std::env; +use std::ffi::OsStr; +#[cfg(any(feature = "regex", target_os = "windows"))] +use std::fs; +use std::iter; +use std::path::{Path, PathBuf}; + +pub trait Checker { + fn is_valid(&self, path: &Path) -> bool; +} + +trait PathExt { + fn has_separator(&self) -> bool; + + fn to_absolute

(self, cwd: P) -> PathBuf + where + P: AsRef; +} + +impl PathExt for PathBuf { + fn has_separator(&self) -> bool { + self.components().count() > 1 + } + + fn to_absolute

(self, cwd: P) -> PathBuf + where + P: AsRef, + { + if self.is_absolute() { + self + } else { + let mut new_path = PathBuf::from(cwd.as_ref()); + new_path.push(self); + new_path + } + } +} + +pub struct Finder; + +impl Finder { + pub fn new() -> Finder { + Finder + } + + pub fn find( + &self, + binary_name: T, + paths: Option, + cwd: Option, + binary_checker: CompositeChecker, + ) -> Result> + where + T: AsRef, + U: AsRef, + V: AsRef, + { + let path = PathBuf::from(&binary_name); + + let binary_path_candidates = match cwd { + Some(cwd) if path.has_separator() => { + // Search binary in cwd if the path have a path separator. + Either::Left(Self::cwd_search_candidates(path, cwd).into_iter()) + } + _ => { + // Search binary in PATHs(defined in environment variable). + let p = paths.ok_or(Error::CannotFindBinaryPath)?; + let paths: Vec<_> = env::split_paths(&p).collect(); + + Either::Right(Self::path_search_candidates(path, paths).into_iter()) + } + }; + + Ok(binary_path_candidates + .filter(move |p| binary_checker.is_valid(p)) + .map(correct_casing)) + } + + #[cfg(feature = "regex")] + pub fn find_re( + &self, + binary_regex: impl Borrow, + paths: Option, + binary_checker: CompositeChecker, + ) -> Result> + where + T: AsRef, + { + let p = paths.ok_or(Error::CannotFindBinaryPath)?; + // Collect needs to happen in order to not have to + // change the API to borrow on `paths`. + #[allow(clippy::needless_collect)] + let paths: Vec<_> = env::split_paths(&p).collect(); + + let matching_re = paths + .into_iter() + .flat_map(fs::read_dir) + .flatten() + .flatten() + .map(|e| e.path()) + .filter(move |p| { + if let Some(unicode_file_name) = p.file_name().unwrap().to_str() { + binary_regex.borrow().is_match(unicode_file_name) + } else { + false + } + }) + .filter(move |p| binary_checker.is_valid(p)); + + Ok(matching_re) + } + + fn cwd_search_candidates(binary_name: PathBuf, cwd: C) -> impl IntoIterator + where + C: AsRef, + { + let path = binary_name.to_absolute(cwd); + + Self::append_extension(iter::once(path)) + } + + fn path_search_candidates

( + binary_name: PathBuf, + paths: P, + ) -> impl IntoIterator + where + P: IntoIterator, + { + let new_paths = paths.into_iter().map(move |p| p.join(binary_name.clone())); + + Self::append_extension(new_paths) + } + + #[cfg(unix)] + fn append_extension

(paths: P) -> impl IntoIterator + where + P: IntoIterator, + { + paths + } + + #[cfg(windows)] + fn append_extension

(paths: P) -> impl IntoIterator + where + P: IntoIterator, + { + use once_cell::sync::Lazy; + + // Sample %PATHEXT%: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC + // PATH_EXTENSIONS is then [".COM", ".EXE", ".BAT", …]. + // (In one use of PATH_EXTENSIONS we skip the dot, but in the other we need it; + // hence its retention.) + static PATH_EXTENSIONS: Lazy> = Lazy::new(|| { + env::var("PATHEXT") + .map(|pathext| { + pathext + .split(';') + .filter_map(|s| { + if s.as_bytes().first() == Some(&b'.') { + Some(s.to_owned()) + } else { + // Invalid segment; just ignore it. + None + } + }) + .collect() + }) + // PATHEXT not being set or not being a proper Unicode string is exceedingly + // improbable and would probably break Windows badly. Still, don't crash: + .unwrap_or_default() + }); + + paths + .into_iter() + .flat_map(move |p| -> Box> { + // Check if path already have executable extension + if has_executable_extension(&p, &PATH_EXTENSIONS) { + Box::new(iter::once(p)) + } else { + let bare_file = p.extension().map(|_| p.clone()); + // Appended paths with windows executable extensions. + // e.g. path `c:/windows/bin[.ext]` will expand to: + // [c:/windows/bin.ext] + // c:/windows/bin[.ext].COM + // c:/windows/bin[.ext].EXE + // c:/windows/bin[.ext].CMD + // ... + Box::new( + bare_file + .into_iter() + .chain(PATH_EXTENSIONS.iter().map(move |e| { + // Append the extension. + let mut p = p.clone().into_os_string(); + p.push(e); + + PathBuf::from(p) + })), + ) + } + }) + } +} + +#[cfg(target_os = "windows")] +fn correct_casing(mut p: PathBuf) -> PathBuf { + if let (Some(parent), Some(file_name)) = (p.parent(), p.file_name()) { + if let Ok(iter) = fs::read_dir(parent) { + for e in iter.filter_map(std::result::Result::ok) { + if e.file_name().eq_ignore_ascii_case(file_name) { + p.pop(); + p.push(e.file_name()); + break; + } + } + } + } + p +} + +#[cfg(not(target_os = "windows"))] +fn correct_casing(p: PathBuf) -> PathBuf { + p +} diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..eb96891 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,40 @@ +use std::path::Path; + +/// Check if given path has extension which in the given vector. +pub fn has_executable_extension, S: AsRef>(path: T, pathext: &[S]) -> bool { + let ext = path.as_ref().extension().and_then(|e| e.to_str()); + match ext { + Some(ext) => pathext + .iter() + .any(|e| ext.eq_ignore_ascii_case(&e.as_ref()[1..])), + _ => false, + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_extension_in_extension_vector() { + // Case insensitive + assert!(has_executable_extension( + PathBuf::from("foo.exe"), + &[".COM", ".EXE", ".CMD"] + )); + + assert!(has_executable_extension( + PathBuf::from("foo.CMD"), + &[".COM", ".EXE", ".CMD"] + )); + } + + #[test] + fn test_extension_not_in_extension_vector() { + assert!(!has_executable_extension( + PathBuf::from("foo.bar"), + &[".COM", ".EXE", ".CMD"] + )); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3e556eb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,631 @@ +//! which +//! +//! A Rust equivalent of Unix command `which(1)`. +//! # Example: +//! +//! To find which rustc executable binary is using: +//! +//! ```no_run +//! use which::which; +//! use std::path::PathBuf; +//! +//! let result = which("rustc").unwrap(); +//! assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +//! +//! ``` + +mod checker; +mod error; +mod finder; +#[cfg(windows)] +mod helper; + +#[cfg(feature = "regex")] +use std::borrow::Borrow; +use std::env; +use std::fmt; +use std::path; + +use std::ffi::{OsStr, OsString}; + +use crate::checker::{CompositeChecker, ExecutableChecker, ExistedChecker}; +pub use crate::error::*; +use crate::finder::Finder; + +/// Find an executable binary's path by name. +/// +/// If given an absolute path, returns it if the file exists and is executable. +/// +/// If given a relative path, returns an absolute path to the file if +/// it exists and is executable. +/// +/// If given a string without path separators, looks for a file named +/// `binary_name` at each directory in `$PATH` and if it finds an executable +/// file there, returns it. +/// +/// # Example +/// +/// ```no_run +/// use which::which; +/// use std::path::PathBuf; +/// +/// let result = which::which("rustc").unwrap(); +/// assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +/// +/// ``` +pub fn which>(binary_name: T) -> Result { + which_all(binary_name).and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) +} + +/// Find an executable binary's path by name, ignoring `cwd`. +/// +/// If given an absolute path, returns it if the file exists and is executable. +/// +/// Does not resolve relative paths. +/// +/// If given a string without path separators, looks for a file named +/// `binary_name` at each directory in `$PATH` and if it finds an executable +/// file there, returns it. +/// +/// # Example +/// +/// ```no_run +/// use which::which; +/// use std::path::PathBuf; +/// +/// let result = which::which_global("rustc").unwrap(); +/// assert_eq!(result, PathBuf::from("/usr/bin/rustc")); +/// +/// ``` +pub fn which_global>(binary_name: T) -> Result { + which_all_global(binary_name).and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) +} + +/// Find all binaries with `binary_name` using `cwd` to resolve relative paths. +pub fn which_all>(binary_name: T) -> Result> { + let cwd = env::current_dir().ok(); + + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + finder.find(binary_name, env::var_os("PATH"), cwd, binary_checker) +} + +/// Find all binaries with `binary_name` ignoring `cwd`. +pub fn which_all_global>( + binary_name: T, +) -> Result> { + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + finder.find( + binary_name, + env::var_os("PATH"), + Option::<&Path>::None, + binary_checker, + ) +} + +/// Find all binaries matching a regular expression in a the system PATH. +/// +/// Only available when feature `regex` is enabled. +/// +/// # Arguments +/// +/// * `regex` - A regular expression to match binaries with +/// +/// # Examples +/// +/// Find Python executables: +/// +/// ```no_run +/// use regex::Regex; +/// use which::which; +/// use std::path::PathBuf; +/// +/// let re = Regex::new(r"python\d$").unwrap(); +/// let binaries: Vec = which::which_re(re).unwrap().collect(); +/// let python_paths = vec![PathBuf::from("/usr/bin/python2"), PathBuf::from("/usr/bin/python3")]; +/// assert_eq!(binaries, python_paths); +/// ``` +/// +/// Find all cargo subcommand executables on the path: +/// +/// ``` +/// use which::which_re; +/// use regex::Regex; +/// +/// which_re(Regex::new("^cargo-.*").unwrap()).unwrap() +/// .for_each(|pth| println!("{}", pth.to_string_lossy())); +/// ``` +#[cfg(feature = "regex")] +pub fn which_re(regex: impl Borrow) -> Result> { + which_re_in(regex, env::var_os("PATH")) +} + +/// Find `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. +pub fn which_in(binary_name: T, paths: Option, cwd: V) -> Result +where + T: AsRef, + U: AsRef, + V: AsRef, +{ + which_in_all(binary_name, paths, cwd) + .and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) +} + +/// Find all binaries matching a regular expression in a list of paths. +/// +/// Only available when feature `regex` is enabled. +/// +/// # Arguments +/// +/// * `regex` - A regular expression to match binaries with +/// * `paths` - A string containing the paths to search +/// (separated in the same way as the PATH environment variable) +/// +/// # Examples +/// +/// ```no_run +/// use regex::Regex; +/// use which::which; +/// use std::path::PathBuf; +/// +/// let re = Regex::new(r"python\d$").unwrap(); +/// let paths = Some("/usr/bin:/usr/local/bin"); +/// let binaries: Vec = which::which_re_in(re, paths).unwrap().collect(); +/// let python_paths = vec![PathBuf::from("/usr/bin/python2"), PathBuf::from("/usr/bin/python3")]; +/// assert_eq!(binaries, python_paths); +/// ``` +#[cfg(feature = "regex")] +pub fn which_re_in( + regex: impl Borrow, + paths: Option, +) -> Result> +where + T: AsRef, +{ + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + finder.find_re(regex, paths, binary_checker) +} + +/// Find all binaries with `binary_name` in the path list `paths`, using `cwd` to resolve relative paths. +pub fn which_in_all( + binary_name: T, + paths: Option, + cwd: V, +) -> Result> +where + T: AsRef, + U: AsRef, + V: AsRef, +{ + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + finder.find(binary_name, paths, Some(cwd), binary_checker) +} + +/// Find all binaries with `binary_name` in the path list `paths`, ignoring `cwd`. +pub fn which_in_global( + binary_name: T, + paths: Option, +) -> Result> +where + T: AsRef, + U: AsRef, +{ + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + finder.find(binary_name, paths, Option::<&Path>::None, binary_checker) +} + +fn build_binary_checker() -> CompositeChecker { + CompositeChecker::new() + .add_checker(Box::new(ExistedChecker::new())) + .add_checker(Box::new(ExecutableChecker::new())) +} + +/// A wrapper containing all functionality in this crate. +pub struct WhichConfig { + cwd: Option>, + custom_path_list: Option, + binary_name: Option, + #[cfg(feature = "regex")] + regex: Option, +} + +impl Default for WhichConfig { + fn default() -> Self { + Self { + cwd: Some(either::Either::Left(true)), + custom_path_list: None, + binary_name: None, + #[cfg(feature = "regex")] + regex: None, + } + } +} + +#[cfg(feature = "regex")] +type Regex = regex::Regex; + +#[cfg(not(feature = "regex"))] +type Regex = (); + +impl WhichConfig { + pub fn new() -> Self { + Self::default() + } + + /// Whether or not to use the current working directory. `true` by default. + /// + /// # Panics + /// + /// If regex was set previously, and you've just passed in `use_cwd: true`, this will panic. + pub fn system_cwd(mut self, use_cwd: bool) -> Self { + #[cfg(feature = "regex")] + if self.regex.is_some() && use_cwd { + panic!("which can't use regex and cwd at the same time!") + } + self.cwd = Some(either::Either::Left(use_cwd)); + self + } + + /// Sets a custom path for resolving relative paths. + /// + /// # Panics + /// + /// If regex was set previously, this will panic. + pub fn custom_cwd(mut self, cwd: path::PathBuf) -> Self { + #[cfg(feature = "regex")] + if self.regex.is_some() { + panic!("which can't use regex and cwd at the same time!") + } + self.cwd = Some(either::Either::Right(cwd)); + self + } + + /// Sets the path name regex to search for. You ***MUST*** call this, or [`Self::binary_name`] prior to searching. + /// + /// When `Regex` is disabled this function takes the unit type as a stand in. The parameter will change when + /// `Regex` is enabled. + /// + /// # Panics + /// + /// If the `regex` feature wasn't turned on for this crate this will always panic. Additionally if a + /// `cwd` (aka current working directory) or `binary_name` was set previously, this will panic, as those options + /// are incompatible with `regex`. + #[allow(unused_variables)] + pub fn regex(mut self, regex: Regex) -> Self { + #[cfg(not(feature = "regex"))] + { + panic!("which's regex feature was not enabled in your Cargo.toml!") + } + #[cfg(feature = "regex")] + { + if self.cwd != Some(either::Either::Left(false)) && self.cwd.is_some() { + panic!("which can't use regex and cwd at the same time!") + } + if self.binary_name.is_some() { + panic!("which can't use `binary_name` and `regex` at the same time!"); + } + self.regex = Some(regex); + self + } + } + + /// Sets the path name to search for. You ***MUST*** call this, or [`Self::regex`] prior to searching. + /// + /// # Panics + /// + /// If a `regex` was set previously this will panic as this is not compatible with `regex`. + pub fn binary_name(mut self, name: OsString) -> Self { + #[cfg(feature = "regex")] + if self.regex.is_some() { + panic!("which can't use `binary_name` and `regex` at the same time!"); + } + self.binary_name = Some(name); + self + } + + /// Uses the given string instead of the `PATH` env variable. + pub fn custom_path_list(mut self, custom_path_list: OsString) -> Self { + self.custom_path_list = Some(custom_path_list); + self + } + + /// Uses the `PATH` env variable. Enabled by default. + pub fn system_path_list(mut self) -> Self { + self.custom_path_list = None; + self + } + + /// Finishes configuring, runs the query and returns the first result. + pub fn first_result(self) -> Result { + self.all_results() + .and_then(|mut i| i.next().ok_or(Error::CannotFindBinaryPath)) + } + + /// Finishes configuring, runs the query and returns all results. + pub fn all_results(self) -> Result> { + let binary_checker = build_binary_checker(); + + let finder = Finder::new(); + + let paths = self.custom_path_list.or_else(|| env::var_os("PATH")); + + #[cfg(feature = "regex")] + if let Some(regex) = self.regex { + return finder + .find_re(regex, paths, binary_checker) + .map(|i| Box::new(i) as Box>); + } + + let cwd = match self.cwd { + Some(either::Either::Left(false)) => None, + Some(either::Either::Right(custom)) => Some(custom), + None | Some(either::Either::Left(true)) => env::current_dir().ok(), + }; + + finder + .find( + self.binary_name.expect( + "binary_name not set! You must set binary_name or regex before searching!", + ), + paths, + cwd, + binary_checker, + ) + .map(|i| Box::new(i) as Box>) + } +} + +/// An owned, immutable wrapper around a `PathBuf` containing the path of an executable. +/// +/// The constructed `PathBuf` is the output of `which` or `which_in`, but `which::Path` has the +/// advantage of being a type distinct from `std::path::Path` and `std::path::PathBuf`. +/// +/// It can be beneficial to use `which::Path` instead of `std::path::Path` when you want the type +/// system to enforce the need for a path that exists and points to a binary that is executable. +/// +/// Since `which::Path` implements `Deref` for `std::path::Path`, all methods on `&std::path::Path` +/// are also available to `&which::Path` values. +#[derive(Clone, PartialEq, Eq)] +pub struct Path { + inner: path::PathBuf, +} + +impl Path { + /// Returns the path of an executable binary by name. + /// + /// This calls `which` and maps the result into a `Path`. + pub fn new>(binary_name: T) -> Result { + which(binary_name).map(|inner| Path { inner }) + } + + /// Returns the paths of all executable binaries by a name. + /// + /// this calls `which_all` and maps the results into `Path`s. + pub fn all>(binary_name: T) -> Result> { + which_all(binary_name).map(|inner| inner.map(|inner| Path { inner })) + } + + /// Returns the path of an executable binary by name in the path list `paths` and using the + /// current working directory `cwd` to resolve relative paths. + /// + /// This calls `which_in` and maps the result into a `Path`. + pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in(binary_name, paths, cwd).map(|inner| Path { inner }) + } + + /// Returns all paths of an executable binary by name in the path list `paths` and using the + /// current working directory `cwd` to resolve relative paths. + /// + /// This calls `which_in_all` and maps the results into a `Path`. + pub fn all_in( + binary_name: T, + paths: Option, + cwd: V, + ) -> Result> + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in_all(binary_name, paths, cwd).map(|inner| inner.map(|inner| Path { inner })) + } + + /// Returns a reference to a `std::path::Path`. + pub fn as_path(&self) -> &path::Path { + self.inner.as_path() + } + + /// Consumes the `which::Path`, yielding its underlying `std::path::PathBuf`. + pub fn into_path_buf(self) -> path::PathBuf { + self.inner + } +} + +impl fmt::Debug for Path { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl std::ops::Deref for Path { + type Target = path::Path; + + fn deref(&self) -> &path::Path { + self.inner.deref() + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl PartialEq for Path { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq for path::PathBuf { + fn eq(&self, other: &Path) -> bool { + *self == other.inner + } +} + +/// An owned, immutable wrapper around a `PathBuf` containing the _canonical_ path of an +/// executable. +/// +/// The constructed `PathBuf` is the result of `which` or `which_in` followed by +/// `Path::canonicalize`, but `CanonicalPath` has the advantage of being a type distinct from +/// `std::path::Path` and `std::path::PathBuf`. +/// +/// It can be beneficial to use `CanonicalPath` instead of `std::path::Path` when you want the type +/// system to enforce the need for a path that exists, points to a binary that is executable, is +/// absolute, has all components normalized, and has all symbolic links resolved +/// +/// Since `CanonicalPath` implements `Deref` for `std::path::Path`, all methods on +/// `&std::path::Path` are also available to `&CanonicalPath` values. +#[derive(Clone, PartialEq, Eq)] +pub struct CanonicalPath { + inner: path::PathBuf, +} + +impl CanonicalPath { + /// Returns the canonical path of an executable binary by name. + /// + /// This calls `which` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + pub fn new>(binary_name: T) -> Result { + which(binary_name) + .and_then(|p| p.canonicalize().map_err(|_| Error::CannotCanonicalize)) + .map(|inner| CanonicalPath { inner }) + } + + /// Returns the canonical paths of an executable binary by name. + /// + /// This calls `which_all` and `Path::canonicalize` and maps the results into `CanonicalPath`s. + pub fn all>( + binary_name: T, + ) -> Result>> { + which_all(binary_name).map(|inner| { + inner.map(|inner| { + inner + .canonicalize() + .map_err(|_| Error::CannotCanonicalize) + .map(|inner| CanonicalPath { inner }) + }) + }) + } + + /// Returns the canonical path of an executable binary by name in the path list `paths` and + /// using the current working directory `cwd` to resolve relative paths. + /// + /// This calls `which_in` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + pub fn new_in(binary_name: T, paths: Option, cwd: V) -> Result + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in(binary_name, paths, cwd) + .and_then(|p| p.canonicalize().map_err(|_| Error::CannotCanonicalize)) + .map(|inner| CanonicalPath { inner }) + } + + /// Returns all of the canonical paths of an executable binary by name in the path list `paths` and + /// using the current working directory `cwd` to resolve relative paths. + /// + /// This calls `which_in_all` and `Path::canonicalize` and maps the result into a `CanonicalPath`. + pub fn all_in( + binary_name: T, + paths: Option, + cwd: V, + ) -> Result>> + where + T: AsRef, + U: AsRef, + V: AsRef, + { + which_in_all(binary_name, paths, cwd).map(|inner| { + inner.map(|inner| { + inner + .canonicalize() + .map_err(|_| Error::CannotCanonicalize) + .map(|inner| CanonicalPath { inner }) + }) + }) + } + + /// Returns a reference to a `std::path::Path`. + pub fn as_path(&self) -> &path::Path { + self.inner.as_path() + } + + /// Consumes the `which::CanonicalPath`, yielding its underlying `std::path::PathBuf`. + pub fn into_path_buf(self) -> path::PathBuf { + self.inner + } +} + +impl fmt::Debug for CanonicalPath { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.inner, f) + } +} + +impl std::ops::Deref for CanonicalPath { + type Target = path::Path; + + fn deref(&self) -> &path::Path { + self.inner.deref() + } +} + +impl AsRef for CanonicalPath { + fn as_ref(&self) -> &path::Path { + self.as_path() + } +} + +impl AsRef for CanonicalPath { + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl PartialEq for CanonicalPath { + fn eq(&self, other: &path::PathBuf) -> bool { + self.inner == *other + } +} + +impl PartialEq for path::PathBuf { + fn eq(&self, other: &CanonicalPath) -> bool { + *self == other.inner + } +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..32a1a28 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,404 @@ +extern crate which; + +#[cfg(all(unix, feature = "regex"))] +use regex::Regex; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::{env, vec}; +use tempfile::TempDir; + +struct TestFixture { + /// Temp directory. + pub tempdir: TempDir, + /// $PATH + pub paths: OsString, + /// Binaries created in $PATH + pub bins: Vec, +} + +const SUBDIRS: &[&str] = &["a", "b", "c"]; +const BIN_NAME: &str = "bin"; + +#[cfg(unix)] +fn mk_bin(dir: &Path, path: &str, extension: &str) -> io::Result { + use std::os::unix::fs::OpenOptionsExt; + let bin = dir.join(path).with_extension(extension); + fs::OpenOptions::new() + .write(true) + .create(true) + .mode(0o666 | (libc::S_IXUSR as u32)) + .open(&bin) + .and_then(|_f| bin.canonicalize()) +} + +fn touch(dir: &Path, path: &str, extension: &str) -> io::Result { + let b = dir.join(path).with_extension(extension); + fs::File::create(&b).and_then(|_f| b.canonicalize()) +} + +#[cfg(windows)] +fn mk_bin(dir: &Path, path: &str, extension: &str) -> io::Result { + touch(dir, path, extension) +} + +impl TestFixture { + // tmp/a/bin + // tmp/a/bin.exe + // tmp/a/bin.cmd + // tmp/b/bin + // tmp/b/bin.exe + // tmp/b/bin.cmd + // tmp/c/bin + // tmp/c/bin.exe + // tmp/c/bin.cmd + pub fn new() -> TestFixture { + let tempdir = tempfile::tempdir().unwrap(); + let mut builder = fs::DirBuilder::new(); + builder.recursive(true); + let mut paths = vec![]; + let mut bins = vec![]; + for d in SUBDIRS.iter() { + let p = tempdir.path().join(d); + builder.create(&p).unwrap(); + bins.push(mk_bin(&p, BIN_NAME, "").unwrap()); + bins.push(mk_bin(&p, BIN_NAME, "exe").unwrap()); + bins.push(mk_bin(&p, BIN_NAME, "cmd").unwrap()); + paths.push(p); + } + let p = tempdir.path().join("win-bin"); + builder.create(&p).unwrap(); + bins.push(mk_bin(&p, "win-bin", "exe").unwrap()); + paths.push(p); + TestFixture { + tempdir, + paths: env::join_paths(paths).unwrap(), + bins, + } + } + + #[allow(dead_code)] + pub fn touch(&self, path: &str, extension: &str) -> io::Result { + touch(self.tempdir.path(), path, extension) + } + + pub fn mk_bin(&self, path: &str, extension: &str) -> io::Result { + mk_bin(self.tempdir.path(), path, extension) + } +} + +fn _which>(f: &TestFixture, path: T) -> which::Result { + which::CanonicalPath::new_in(path, Some(f.paths.clone()), f.tempdir.path()) +} + +fn _which_all<'a, T: AsRef + 'a>( + f: &'a TestFixture, + path: T, +) -> which::Result> + '_> { + which::CanonicalPath::all_in(path, Some(f.paths.clone()), f.tempdir.path()) +} + +#[test] +#[cfg(unix)] +fn it_works() { + use std::process::Command; + let result = which::Path::new("rustc"); + assert!(result.is_ok()); + + let which_result = Command::new("which").arg("rustc").output(); + + assert_eq!( + String::from(result.unwrap().to_str().unwrap()), + String::from_utf8(which_result.unwrap().stdout) + .unwrap() + .trim() + ); +} + +#[test] +#[cfg(unix)] +fn test_which() { + let f = TestFixture::new(); + assert_eq!(_which(&f, &BIN_NAME).unwrap(), f.bins[0]) +} + +#[test] +#[cfg(windows)] +fn test_which() { + let f = TestFixture::new(); + assert_eq!(_which(&f, &BIN_NAME).unwrap(), f.bins[1]) +} + +#[test] +#[cfg(all(unix, feature = "regex"))] +fn test_which_re_in_with_matches() { + let f = TestFixture::new(); + f.mk_bin("a/bin_0", "").unwrap(); + f.mk_bin("b/bin_1", "").unwrap(); + let re = Regex::new(r"bin_\d").unwrap(); + + let result: Vec = which::which_re_in(re, Some(f.paths)) + .unwrap() + .into_iter() + .collect(); + + let temp = f.tempdir; + + assert_eq!( + result, + vec![temp.path().join("a/bin_0"), temp.path().join("b/bin_1")] + ) +} + +#[test] +#[cfg(all(unix, feature = "regex"))] +fn test_which_re_in_without_matches() { + let f = TestFixture::new(); + let re = Regex::new(r"bi[^n]").unwrap(); + + let result: Vec = which::which_re_in(re, Some(f.paths)) + .unwrap() + .into_iter() + .collect(); + + assert_eq!(result, Vec::::new()) +} + +#[test] +#[cfg(all(unix, feature = "regex"))] +fn test_which_re_accepts_owned_and_borrow() { + which::which_re(Regex::new(r".").unwrap()) + .unwrap() + .for_each(drop); + which::which_re(&Regex::new(r".").unwrap()) + .unwrap() + .for_each(drop); + which::which_re_in(Regex::new(r".").unwrap(), Some("pth")) + .unwrap() + .for_each(drop); + which::which_re_in(&Regex::new(r".").unwrap(), Some("pth")) + .unwrap() + .for_each(drop); +} + +#[test] +#[cfg(unix)] +fn test_which_extension() { + let f = TestFixture::new(); + let b = Path::new(&BIN_NAME).with_extension(""); + assert_eq!(_which(&f, &b).unwrap(), f.bins[0]) +} + +#[test] +#[cfg(windows)] +fn test_which_extension() { + let f = TestFixture::new(); + let b = Path::new(&BIN_NAME).with_extension("cmd"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[2]) +} + +#[test] +#[cfg(windows)] +fn test_which_no_extension() { + let f = TestFixture::new(); + let b = Path::new("win-bin"); + let which_result = which::which_in(&b, Some(&f.paths), ".").unwrap(); + // Make sure the extension is the correct case. + assert_eq!(which_result.extension(), f.bins[9].extension()); + assert_eq!(fs::canonicalize(&which_result).unwrap(), f.bins[9]) +} + +#[test] +fn test_which_not_found() { + let f = TestFixture::new(); + assert!(_which(&f, "a").is_err()); +} + +#[test] +fn test_which_second() { + let f = TestFixture::new(); + let b = f.mk_bin("b/another", env::consts::EXE_EXTENSION).unwrap(); + assert_eq!(_which(&f, "another").unwrap(), b); +} + +#[test] +fn test_which_all() { + let f = TestFixture::new(); + let actual = _which_all(&f, BIN_NAME) + .unwrap() + .map(|c| c.unwrap()) + .collect::>(); + let mut expected = f + .bins + .iter() + .map(|p| p.canonicalize().unwrap()) + .collect::>(); + #[cfg(windows)] + { + expected.retain(|p| p.file_stem().unwrap() == BIN_NAME); + expected.retain(|p| p.extension().map(|ext| ext == "exe" || ext == "cmd") == Some(true)); + } + #[cfg(not(windows))] + { + expected.retain(|p| p.file_name().unwrap() == BIN_NAME); + } + assert_eq!(actual, expected); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, &f.bins[3]).unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, &f.bins[4]).unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute_path_case() { + // Test that an absolute path with an uppercase extension + // is accepted. + let f = TestFixture::new(); + let p = &f.bins[4]; + assert_eq!(_which(&f, &p).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute_extension() { + let f = TestFixture::new(); + // Don't append EXE_EXTENSION here. + let b = f.bins[3].parent().unwrap().join(&BIN_NAME); + assert_eq!(_which(&f, &b).unwrap(), f.bins[3].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_absolute_extension() { + let f = TestFixture::new(); + // Don't append EXE_EXTENSION here. + let b = f.bins[4].parent().unwrap().join(&BIN_NAME); + assert_eq!(_which(&f, &b).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "b/bin").unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_relative() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "b/bin").unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_extension() { + // test_which_relative tests a relative path without an extension, + // so test a relative path with an extension here. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension(env::consts::EXE_EXTENSION); + assert_eq!(_which(&f, &b).unwrap(), f.bins[3].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_extension() { + // test_which_relative tests a relative path without an extension, + // so test a relative path with an extension here. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension("cmd"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[5].canonicalize().unwrap()); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_extension_case() { + // Test that a relative path with an uppercase extension + // is accepted. + let f = TestFixture::new(); + let b = Path::new("b/bin").with_extension("EXE"); + assert_eq!(_which(&f, &b).unwrap(), f.bins[4].canonicalize().unwrap()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_leading_dot() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "./b/bin").unwrap(), + f.bins[3].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(windows)] +fn test_which_relative_leading_dot() { + let f = TestFixture::new(); + assert_eq!( + _which(&f, "./b/bin").unwrap(), + f.bins[4].canonicalize().unwrap() + ); +} + +#[test] +#[cfg(unix)] +fn test_which_non_executable() { + // Shouldn't return non-executable files. + let f = TestFixture::new(); + f.touch("b/another", "").unwrap(); + assert!(_which(&f, "another").is_err()); +} + +#[test] +#[cfg(unix)] +fn test_which_absolute_non_executable() { + // Shouldn't return non-executable files, even if given an absolute path. + let f = TestFixture::new(); + let b = f.touch("b/another", "").unwrap(); + assert!(_which(&f, &b).is_err()); +} + +#[test] +#[cfg(unix)] +fn test_which_relative_non_executable() { + // Shouldn't return non-executable files. + let f = TestFixture::new(); + f.touch("b/another", "").unwrap(); + assert!(_which(&f, "b/another").is_err()); +} + +#[test] +fn test_failure() { + let f = TestFixture::new(); + + let run = || -> which::Result { + let p = _which(&f, "./b/bin")?; + Ok(p.into_path_buf()) + }; + + let _ = run(); +}