From d487956a3a7ee0e06abbaf8a8678ec04677409ba Mon Sep 17 00:00:00 2001 From: Vit Date: Fri, 19 Jul 2024 21:52:28 +0300 Subject: [PATCH] Windows command files support Find .cmd or .bat in paths. --- src/lib.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++- tests/windows.rs | 37 +++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/windows.rs diff --git a/src/lib.rs b/src/lib.rs index b96f4bd..f08456d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1041,12 +1041,96 @@ impl Cmd { /// Other builder methods have no effect on the command returned since they control how the /// command is run, but this method does not yet execute the command. pub fn to_command(&self) -> Command { - let mut result = Command::new(&self.prog); + let program = self.resolve_program(); + let mut result = Command::new(&program); result.current_dir(&self.sh.cwd); result.args(&self.args); result.envs(&*self.sh.env); result } + + #[cfg(not(windows))] + fn resolve_program(&self) -> OsString { + self.prog.as_os_str().into() + } + + #[cfg(windows)] + fn resolve_program(&self) -> OsString { + if self.prog.extension().is_some() { + // fast path for explicit extension + return self.prog.as_os_str().into(); + } + + // mimics `search_paths` behavior: + // https://github.com/rust-lang/rust/blob/051478957371ee0084a7c0913941d2a8c4757bb9/library/std/src/sys/pal/windows/process.rs#L482 + + const ENV_PATH: &str = "PATH"; + + // 1. Child paths + let paths = self + .sh + .env + .iter() + .filter_map(|(name, value)| { + if name.eq_ignore_ascii_case(ENV_PATH) { + Some(value.as_ref()) + } else { + None + } + }) + .last(); + + if let Some(program_path) = self.find_in_paths(paths) { + return program_path; + } + + // 2. Application path + let paths = env::current_exe().ok().map(|mut path| { + path.pop(); + OsString::from(path) + }); + + if let Some(program_path) = self.find_in_paths(map_os_str(&paths)) { + return program_path; + } + + // 3 & 4. System paths + // Sort of compromise: use %SystemRoot% to avoid adding an additional dependency on the `windows` crate. + // Usually %SystemRoot% expands to 'C:\WINDOWS' and 'C:\WINDOWS\SYSTEM32' exists in `PATH`, + // so the compromise covers both `GetSystemDirectoryW` and `GetWindowsDirectoryW` cases. + let paths = self.sh.var_os("SystemRoot"); + if let Some(program_path) = self.find_in_paths(map_os_str(&paths)) { + return program_path; + } + + // 5. Parent paths + let paths = self.sh.var_os(ENV_PATH); + if let Some(program_path) = self.find_in_paths(map_os_str(&paths)) { + return program_path; + } + + return self.prog.as_os_str().into(); + + fn map_os_str(value: &Option) -> Option<&OsStr> { + value.as_ref().map(|p| p.as_os_str()) + } + } + + #[cfg(windows)] + fn find_in_paths(&self, paths: Option<&OsStr>) -> Option { + paths.and_then(|paths| { + for folder in env::split_paths(&paths).filter(|p| !p.as_os_str().is_empty()) { + for ext in ["cmd", "bat"] { + let path = folder.join(self.prog.with_extension(ext)); + if std::fs::metadata(&path).is_ok() { + return Some(path.into_os_string()); + } + } + } + + None + }) + } } /// A temporary directory. diff --git a/tests/windows.rs b/tests/windows.rs new file mode 100644 index 0000000..f8113a8 --- /dev/null +++ b/tests/windows.rs @@ -0,0 +1,37 @@ +#![cfg(windows)] + +use xshell::{cmd, Shell}; + +#[test] +fn npm() { + let sh = Shell::new().unwrap(); + + if cmd!(sh, "where npm").read().is_ok() { + let script_shell = cmd!(sh, "npm get shell").read().unwrap(); + assert!(script_shell.ends_with(".exe")); + + let script_shell_explicit = cmd!(sh, "npm.cmd get shell").read().unwrap(); + assert_eq!(script_shell, script_shell_explicit); + } +} + +#[test] +fn overridden_cmd_path() { + let sh = Shell::new().unwrap(); + + if cmd!(sh, "where npm").read().is_ok() { + // should fail as `PATH` is overridden via Cmd + assert!(cmd!(sh, "npm get shell").env("PATH", ".").run().is_err()); + } +} + +#[test] +fn overridden_shell_path() { + let mut sh = Shell::new().unwrap(); + sh.set_var("PATH", "."); + + if cmd!(sh, "where npm").read().is_ok() { + // should fail as `PATH` is overridden via Shell + assert!(cmd!(sh, "npm get shell").env("PATH", ".").run().is_err()); + } +}