diff --git a/src/lib.rs b/src/lib.rs index 9a7ccda..9084a29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1048,10 +1048,88 @@ impl<'a> Cmd<'a> { Ok(output) } + #[cfg(not(windows))] + fn resolve_program(&self) -> OsString { + self.data.prog.as_os_str().into() + } + + #[cfg(windows)] + fn resolve_program(&self) -> OsString { + if self.data.prog.extension().is_some() { + // fast path for explicit extension + return self.data.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 + .data + .env_changes + .iter() + .filter_map(|change| match change { + EnvChange::Set(name, value) if name.eq_ignore_ascii_case(ENV_PATH) => Some(value), + _ => 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(paths.as_ref()) { + 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.shell.var_os("SystemRoot"); + if let Some(program_path) = self.find_in_paths(paths.as_ref()) { + return program_path; + } + + // 5. Parent paths + let paths = self.shell.var_os(ENV_PATH); + if let Some(program_path) = self.find_in_paths(paths.as_ref()) { + return program_path; + } + + return self.data.prog.as_os_str().into(); + } + + #[cfg(windows)] + fn find_in_paths(&self, paths: Option<&OsString>) -> 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.data.prog.with_extension(ext)); + if std::fs::metadata(&path).is_ok() { + return Some(path.into_os_string()); + } + } + } + + None + }) + } + fn to_command(&self) -> Command { - let mut res = Command::new(&self.data.prog); - res.current_dir(self.shell.current_dir()); + let program = self.resolve_program(); + let mut res = Command::new(program); + res.args(&self.data.args); + res.current_dir(self.shell.current_dir()); for (key, val) in &*self.shell.env.borrow() { res.env(key, val); diff --git a/tests/windows.rs b/tests/windows.rs new file mode 100644 index 0000000..f1283ea --- /dev/null +++ b/tests/windows.rs @@ -0,0 +1,38 @@ +#![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_child_path() { + let sh = Shell::new().unwrap(); + + if cmd!(sh, "where npm").read().is_ok() { + // should succeed as sh contains its own `PATH` + assert!(cmd!(sh, "npm get shell").env("PATH", ".").run().is_ok()); + } +} + +#[test] +fn overridden_path() { + let sh = Shell::new().unwrap(); + + let _enc = sh.push_env("PATH", "."); + + if cmd!(sh, "where npm").read().is_ok() { + // should fail as `PATH` is completely overridden + assert!(cmd!(sh, "npm get shell").run().is_err()); + } +}