Skip to content

Commit

Permalink
Retry failed command as .cmd or .bat
Browse files Browse the repository at this point in the history
  • Loading branch information
vsrs committed Jul 23, 2024
1 parent 79faeaf commit b8ce021
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 37 deletions.
83 changes: 53 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,23 +1000,56 @@ impl<'a> Cmd<'a> {
Ok(stream)
}

fn spawn_command_as(
&self,
read_stdout: bool,
read_stderr: bool,
program: Option<&OsStr>,
) -> std::io::Result<std::process::Child> {
let mut command = self.to_command_impl(program);

if !self.data.ignore_stdout {
command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
}
if !self.data.ignore_stderr {
command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
}

command.stdin(match &self.data.stdin_contents {
Some(_) => Stdio::piped(),
None => Stdio::null(),
});

command.spawn()
}

fn output_impl(&self, read_stdout: bool, read_stderr: bool) -> Result<Output> {
let mut child = {
let mut command = self.to_command();

if !self.data.ignore_stdout {
command.stdout(if read_stdout { Stdio::piped() } else { Stdio::inherit() });
}
if !self.data.ignore_stderr {
command.stderr(if read_stderr { Stdio::piped() } else { Stdio::inherit() });
let mut child = self.spawn_command_as(read_stdout, read_stderr, None);

// rust stdlib has all necessary code to run `.cmd` and `.bat` files on Windows via 'cmd /c' invocation,
// but requires the explicit extension. If the command does not have one, add 'cmd' or 'bat' and retry.
//
// Ref: https://github.com/rust-lang/rust/blob/051478957371ee0084a7c0913941d2a8c4757bb9/library/std/src/sys/pal/windows/process.rs#L274
#[cfg(windows)]
if matches!(&child, Err(err) if err.kind() == io::ErrorKind::NotFound)
&& self.data.prog.extension().is_none()
{
for ext in ["cmd", "bat"] {
let possible_executable = self.data.prog.with_extension(ext);
let retry_child = self.spawn_command_as(
read_stdout,
read_stderr,
Some(possible_executable.as_os_str()),
);
if retry_child.is_ok() {
child = retry_child;
break;
}
}
}

command.stdin(match &self.data.stdin_contents {
Some(_) => Stdio::piped(),
None => Stdio::null(),
});

command.spawn().map_err(|err| {
child.map_err(|err| {
// Try to determine whether the command failed because the current
// directory does not exist. Return an appropriate error in such a
// case.
Expand Down Expand Up @@ -1049,23 +1082,13 @@ impl<'a> Cmd<'a> {
}

fn to_command(&self) -> Command {
let mut res = if cfg!(windows) {
// On windows have to use "cmd /c" workaround to allow batch (command) files
let mut res = Command::new("cmd");
res.current_dir(self.shell.current_dir());
res.args(
[OsStr::new("/c"), self.data.prog.as_os_str()]
.iter()
.map(|it| *it)
.chain(self.data.args.iter().map(|it| it.as_os_str())),
);
res
} else {
let mut res = Command::new(&self.data.prog);
res.current_dir(self.shell.current_dir());
res.args(&self.data.args);
res
};
self.to_command_impl(None)
}

fn to_command_impl(&self, program: Option<&OsStr>) -> Command {
let mut res = Command::new(program.unwrap_or(self.data.prog.as_os_str()));
res.args(&self.data.args);
res.current_dir(self.shell.current_dir());

for (key, val) in &*self.shell.env.borrow() {
res.env(key, val);
Expand Down
19 changes: 12 additions & 7 deletions tests/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
use xshell::{cmd, Shell};

#[test]
fn echo() {
fn npm() {
let sh = Shell::new().unwrap();

let res = cmd!(sh, "echo test").read().unwrap();
assert_eq!(res, "test");
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 npm() {
fn explicit_extension() {
let sh = Shell::new().unwrap();

if cmd!(sh, "where npm.cmd").read().is_ok() {
let script_shell = cmd!(sh, "npm get shell").read().unwrap();
assert!(script_shell.ends_with(".exe"))
if cmd!(sh, "where npm").read().is_ok() {
// should not try to find npm with explicit extension
assert!(cmd!(sh, "npm.exe get shell").run().is_err());
}
}

0 comments on commit b8ce021

Please sign in to comment.