Support running batch files without typing their extension (#6278)

* Support running batch files without typing their extension

* suppress warning
This commit is contained in:
Reilly Wood 2022-08-09 16:24:08 -07:00 committed by GitHub
parent fc8512be39
commit dcab255d59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 122 additions and 17 deletions

View file

@ -101,6 +101,7 @@ impl Command for External {
}
}
#[derive(Clone)]
pub struct ExternalCommand {
pub name: Spanned<String>,
pub args: Vec<Spanned<String>>,
@ -121,28 +122,75 @@ impl ExternalCommand {
let ctrlc = engine_state.ctrlc.clone();
let mut process = self.create_process(&input, false, head)?;
let child;
// mut is used in the windows branch only, suppress warning on other platforms
#[allow(unused_mut)]
let mut child;
#[cfg(windows)]
{
// Some common Windows commands are actually built in to cmd.exe, not executables in their own right.
// To support those commands, we "shell out" to cmd.exe.
// Running external commands on Windows has 2 points of complication:
// 1. Some common Windows commands are actually built in to cmd.exe, not executables in their own right.
// 2. We need to let users run batch scripts etc. (.bat, .cmd) without typing their extension
// This has the full list of cmd.exe "internal" commands: https://ss64.com/nt/syntax-internal.html
// I (Reilly) went through the full list and whittled it down to ones that are potentially useful:
const CMD_INTERNAL_COMMANDS: [&str; 8] = [
"ASSOC", "DIR", "ECHO", "FTYPE", "MKLINK", "START", "VER", "VOL",
];
let command_name_upper = self.name.item.to_uppercase();
let use_cmd = CMD_INTERNAL_COMMANDS
.iter()
.any(|&cmd| command_name_upper == cmd);
// To support these situations, we have a fallback path that gets run if a command
// fails to be run as a normal executable:
// 1. "shell out" to cmd.exe if the command is a known cmd.exe internal command
// 2. Otherwise, use `which-rs` to look for batch files etc. then run those in cmd.exe
match process.spawn() {
Err(_) => {
let mut fg_process = self.create_process(&input, use_cmd, head)?;
child = fg_process.spawn();
Err(err) => {
// set the default value, maybe we'll override it later
child = Err(err);
// This has the full list of cmd.exe "internal" commands: https://ss64.com/nt/syntax-internal.html
// I (Reilly) went through the full list and whittled it down to ones that are potentially useful:
const CMD_INTERNAL_COMMANDS: [&str; 8] = [
"ASSOC", "DIR", "ECHO", "FTYPE", "MKLINK", "START", "VER", "VOL",
];
let command_name_upper = self.name.item.to_uppercase();
let looks_like_cmd_internal = CMD_INTERNAL_COMMANDS
.iter()
.any(|&cmd| command_name_upper == cmd);
if looks_like_cmd_internal {
let mut cmd_process = self.create_process(&input, true, head)?;
child = cmd_process.spawn();
} else {
#[cfg(feature = "which-support")]
{
// maybe it's a batch file (foo.cmd) and the user typed `foo`. Try to find it with `which-rs`
// TODO: clean this up with an if-let chain once those are stable
if let Ok(path) =
nu_engine::env::path_str(engine_state, stack, self.name.span)
{
if let Some(cwd) = self.env_vars.get("PWD") {
// append cwd to PATH so `which-rs` looks in the cwd too.
// this approximates what cmd.exe does.
let path_with_cwd = format!("{};{}", cwd, path);
if let Ok(which_path) =
which::which_in(&self.name.item, Some(path_with_cwd), cwd)
{
if let Some(file_name) = which_path.file_name() {
let file_name_upper =
file_name.to_string_lossy().to_uppercase();
if file_name_upper != command_name_upper {
// which-rs found an executable file with a slightly different name
// than the one the user tried. Let's try running it
let mut new_command = self.clone();
new_command.name = Spanned {
item: file_name.to_string_lossy().to_string(),
span: self.name.span,
};
let mut cmd_process = new_command
.create_process(&input, true, head)?;
child = cmd_process.spawn();
}
}
}
}
}
}
}
}
Ok(process) => {
child = Ok(process);

View file

@ -1,4 +1,4 @@
use nu_test_support::fs::Stub::EmptyFile;
use nu_test_support::fs::Stub::{EmptyFile, FileWithContent};
use nu_test_support::playground::Playground;
use nu_test_support::{nu, pipeline};
@ -259,3 +259,60 @@ fn single_quote_does_not_expand_path_glob_windows() {
assert!(actual.out.contains("D&D_volume_2.txt"));
});
}
#[cfg(windows)]
#[test]
fn can_run_batch_files() {
Playground::setup("run a Windows batch file", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
"foo.cmd",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.cmd"));
assert!(actual.out.contains("Hello World"));
});
}
#[cfg(windows)]
#[test]
fn can_run_batch_files_without_cmd_extension() {
Playground::setup(
"run a Windows batch file without specifying the extension",
|dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
"foo.cmd",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo"));
assert!(actual.out.contains("Hello World"));
},
);
}
#[cfg(windows)]
#[test]
fn can_run_batch_files_without_bat_extension() {
Playground::setup(
"run a Windows batch file without specifying the extension",
|dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
"foo.bat",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo"));
assert!(actual.out.contains("Hello World"));
},
);
}