Skip to content

Commit 6e0a5a2

Browse files
committed
Find .cmd or .bat in paths.
1 parent 79faeaf commit 6e0a5a2

File tree

2 files changed

+104
-24
lines changed

2 files changed

+104
-24
lines changed

src/lib.rs

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,24 +1048,87 @@ impl<'a> Cmd<'a> {
10481048
Ok(output)
10491049
}
10501050

1051+
#[cfg(not(windows))]
1052+
fn resolve_program(&self) -> OsString {
1053+
self.data.prog.as_os_str().into()
1054+
}
1055+
1056+
#[cfg(windows)]
1057+
fn resolve_program(&self) -> OsString {
1058+
if self.data.prog.extension().is_some() {
1059+
// fast path for explicit extension
1060+
return self.data.prog.as_os_str().into();
1061+
}
1062+
1063+
// mimics `search_paths` behavior:
1064+
// https://github.com/rust-lang/rust/blob/051478957371ee0084a7c0913941d2a8c4757bb9/library/std/src/sys/pal/windows/process.rs#L482
1065+
1066+
const ENV_PATH: &str = "PATH";
1067+
1068+
// 1. Child paths
1069+
let paths = self
1070+
.data
1071+
.env_changes
1072+
.iter()
1073+
.filter_map(|change| match change {
1074+
EnvChange::Set(name, value) if name.eq_ignore_ascii_case(ENV_PATH) => Some(value),
1075+
_ => None,
1076+
})
1077+
.last();
1078+
1079+
if let Some(program_path) = self.find_in_paths(paths) {
1080+
return program_path;
1081+
}
1082+
1083+
// 2. Application path
1084+
let paths = env::current_exe().ok().map(|mut path| {
1085+
path.pop();
1086+
OsString::from(path)
1087+
});
1088+
1089+
if let Some(program_path) = self.find_in_paths(paths.as_ref()) {
1090+
return program_path;
1091+
}
1092+
1093+
// 3 & 4. System paths
1094+
// Sort of compromise: use %SystemRoot% to avoid adding an additional dependency on the `windows` crate.
1095+
// Usually %SystemRoot% expands to 'C:\WINDOWS' and 'C:\WINDOWS\SYSTEM32' exists in `PATH`,
1096+
// so the compromise covers both `GetSystemDirectoryW` and `GetWindowsDirectoryW` cases.
1097+
let paths = self.shell.var_os("SystemRoot");
1098+
if let Some(program_path) = self.find_in_paths(paths.as_ref()) {
1099+
return program_path;
1100+
}
1101+
1102+
// 5. Parent paths
1103+
let paths = self.shell.var_os(ENV_PATH);
1104+
if let Some(program_path) = self.find_in_paths(paths.as_ref()) {
1105+
return program_path;
1106+
}
1107+
1108+
return self.data.prog.as_os_str().into();
1109+
}
1110+
1111+
fn find_in_paths(&self, paths: Option<&OsString>) -> Option<OsString> {
1112+
paths.and_then(|paths| {
1113+
for folder in env::split_paths(&paths).filter(|p| !p.as_os_str().is_empty()) {
1114+
for ext in ["cmd", "bat"] {
1115+
let path = folder.join(self.data.prog.with_extension(ext));
1116+
if std::fs::metadata(&path).is_ok() {
1117+
return Some(path.into_os_string());
1118+
}
1119+
}
1120+
}
1121+
1122+
None
1123+
})
1124+
}
1125+
10511126
fn to_command(&self) -> Command {
1052-
let mut res = if cfg!(windows) {
1053-
// On windows have to use "cmd /c" workaround to allow batch (command) files
1054-
let mut res = Command::new("cmd");
1055-
res.current_dir(self.shell.current_dir());
1056-
res.args(
1057-
[OsStr::new("/c"), self.data.prog.as_os_str()]
1058-
.iter()
1059-
.map(|it| *it)
1060-
.chain(self.data.args.iter().map(|it| it.as_os_str())),
1061-
);
1062-
res
1063-
} else {
1064-
let mut res = Command::new(&self.data.prog);
1065-
res.current_dir(self.shell.current_dir());
1066-
res.args(&self.data.args);
1067-
res
1068-
};
1127+
let program = self.resolve_program();
1128+
let mut res = Command::new(program);
1129+
1130+
res.args(&self.data.args);
1131+
res.current_dir(self.shell.current_dir());
10691132

10701133
for (key, val) in &*self.shell.env.borrow() {
10711134
res.env(key, val);

tests/windows.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,36 @@
33
use xshell::{cmd, Shell};
44

55
#[test]
6-
fn echo() {
6+
fn npm() {
77
let sh = Shell::new().unwrap();
88

9-
let res = cmd!(sh, "echo test").read().unwrap();
10-
assert_eq!(res, "test");
9+
if cmd!(sh, "where npm").read().is_ok() {
10+
let script_shell = cmd!(sh, "npm get shell").read().unwrap();
11+
assert!(script_shell.ends_with(".exe"));
12+
13+
let script_shell_explicit = cmd!(sh, "npm.cmd get shell").read().unwrap();
14+
assert_eq!(script_shell, script_shell_explicit);
15+
}
1116
}
1217

1318
#[test]
14-
fn npm() {
19+
fn overridden_child_path() {
1520
let sh = Shell::new().unwrap();
1621

17-
if cmd!(sh, "where npm.cmd").read().is_ok() {
18-
let script_shell = cmd!(sh, "npm get shell").read().unwrap();
19-
assert!(script_shell.ends_with(".exe"))
22+
if cmd!(sh, "where npm").read().is_ok() {
23+
// should succeed as sh contains it's own `PATH`
24+
assert!(cmd!(sh, "npm get shell").env("PATH", ".").run().is_ok());
25+
}
26+
}
27+
28+
#[test]
29+
fn overridden_path() {
30+
let sh = Shell::new().unwrap();
31+
32+
let _enc = sh.push_env("PATH", ".");
33+
34+
if cmd!(sh, "where npm").read().is_ok() {
35+
// should fail as `PATH` totally overridden
36+
assert!(cmd!(sh, "npm get shell").run().is_err());
2037
}
2138
}

0 commit comments

Comments
 (0)