mirror of
https://github.com/nushell/nushell.git
synced 2025-05-05 15:32:56 +00:00
Run scripts of any file extension in PATHEXT on Windows (#15611)
# Description On Windows, I would like to be able to call a script directly in nushell and have that script be found in the PATH and run based on filetype associations and PATHEXT. There have been previous discussions related to this feature, see https://github.com/nushell/nushell/issues/6440 and https://github.com/nushell/nushell/issues/15476. The latter issue is only a few weeks old, and after taking a look at it and the resultant PR I found that currently nushell is hardcoded to support only running nushell (.nu) scripts in this way. This PR seeks to make this functionality more generic. Instead of checking that the file extension is explicitly `NU`, it instead checks that it **is not** one of `COM`, `EXE`, `BAT`, `CMD`, or `PS1`. The first four of these are extensions that Windows can figure out how to run on its own. This is implied by the output of `ftype` for any of these extensions, which shows that files are just run without a calling command anyway. ``` >ftype batfile batfile="%1" %* ``` PS1 files are ignored because they are handled as a special in later logic. In implementing this I initially tried to fetch the value of PATHEXT and confirm that the file extension was indeed in PATHEXT. But I determined that because `which()` respects PATHEXT, this would be redundant; any executable that is found by `which` is already going to have an extension in PATHEXT. It is thus only necessary to check that it isn't one of the few extensions that should be called directly, without the use of `cmd.exe`. There are some small formatting changes to `run_external.rs` in the PR as a result of running `cargo fmt` that are not entirely related to the code I modified. I can back out those changes if that is desired. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Behavior for `.nu` scripts will not change. Users will still need to ensure they have PATHEXT and filetype associations set correctly for them to work, but this will now also apply to scripts of other types.
This commit is contained in:
parent
f41b1460aa
commit
b33f4b7f55
@ -79,23 +79,30 @@ impl Command for External {
|
||||
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
|
||||
// On Windows, the user could have run the cmd.exe built-in "assoc" command
|
||||
// Example: "assoc .nu=nuscript" and then run the cmd.exe built-in "ftype" command
|
||||
// Example: "ftype nuscript=C:\path\to\nu.exe '%1' %*" and then added the nushell
|
||||
// script extension ".NU" to the PATHEXT environment variable. In this case, we use
|
||||
// the which command, which will find the executable with or without the extension.
|
||||
// If it "which" returns true, that means that we've found the nushell script and we
|
||||
// believe the user wants to use the windows association to run the script. The only
|
||||
// On Windows, the user could have run the cmd.exe built-in commands "assoc"
|
||||
// and "ftype" to create a file association for an arbitrary file extension.
|
||||
// They then could have added that extension to the PATHEXT environment variable.
|
||||
// For example, a nushell script with extension ".nu" can be set up with
|
||||
// "assoc .nu=nuscript" and "ftype nuscript=C:\path\to\nu.exe '%1' %*",
|
||||
// and then by adding ".NU" to PATHEXT. In this case we use the which command,
|
||||
// which will find the executable with or without the extension. If "which"
|
||||
// returns true, that means that we've found the script and we believe the
|
||||
// user wants to use the windows association to run the script. The only
|
||||
// easy way to do this is to run cmd.exe with the script as an argument.
|
||||
let potential_nuscript_in_windows = if cfg!(windows) {
|
||||
// let's make sure it's a .nu script
|
||||
// File extensions of .COM, .EXE, .BAT, and .CMD are ignored because Windows
|
||||
// can run those files directly. PS1 files are also ignored and that
|
||||
// extension is handled in a separate block below.
|
||||
let pathext_script_in_windows = if cfg!(windows) {
|
||||
if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
|
||||
let ext = executable
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_uppercase();
|
||||
ext == "NU"
|
||||
|
||||
!["COM", "EXE", "BAT", "CMD", "PS1"]
|
||||
.iter()
|
||||
.any(|c| *c == ext)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@ -122,29 +129,28 @@ impl Command for External {
|
||||
// Find the absolute path to the executable. On Windows, set the
|
||||
// executable to "cmd.exe" if it's a CMD internal command. If the
|
||||
// command is not found, display a helpful error message.
|
||||
let executable = if cfg!(windows)
|
||||
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
|
||||
{
|
||||
PathBuf::from("cmd.exe")
|
||||
} else if cfg!(windows) && potential_powershell_script {
|
||||
// If we're on Windows and we're trying to run a PowerShell script, we'll use
|
||||
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
|
||||
// it's automatically installed on all modern windows systems.
|
||||
PathBuf::from("powershell.exe")
|
||||
} else {
|
||||
// Determine the PATH to be used and then use `which` to find it - though this has no
|
||||
// effect if it's an absolute path already
|
||||
let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
|
||||
return Err(command_not_found(
|
||||
&name_str,
|
||||
call.head,
|
||||
engine_state,
|
||||
stack,
|
||||
&cwd,
|
||||
));
|
||||
let executable =
|
||||
if cfg!(windows) && (is_cmd_internal_command(&name_str) || pathext_script_in_windows) {
|
||||
PathBuf::from("cmd.exe")
|
||||
} else if cfg!(windows) && potential_powershell_script {
|
||||
// If we're on Windows and we're trying to run a PowerShell script, we'll use
|
||||
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
|
||||
// it's automatically installed on all modern windows systems.
|
||||
PathBuf::from("powershell.exe")
|
||||
} else {
|
||||
// Determine the PATH to be used and then use `which` to find it - though this has no
|
||||
// effect if it's an absolute path already
|
||||
let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
|
||||
return Err(command_not_found(
|
||||
&name_str,
|
||||
call.head,
|
||||
engine_state,
|
||||
stack,
|
||||
&cwd,
|
||||
));
|
||||
};
|
||||
executable
|
||||
};
|
||||
executable
|
||||
};
|
||||
|
||||
// Create the command.
|
||||
let mut command = std::process::Command::new(&executable);
|
||||
@ -160,7 +166,7 @@ impl Command for External {
|
||||
// Configure args.
|
||||
let args = eval_external_arguments(engine_state, stack, call_args.to_vec())?;
|
||||
#[cfg(windows)]
|
||||
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
|
||||
if is_cmd_internal_command(&name_str) || pathext_script_in_windows {
|
||||
// The /D flag disables execution of AutoRun commands from registry.
|
||||
// The /C flag followed by a command name instructs CMD to execute
|
||||
// that command and quit.
|
||||
|
Loading…
x
Reference in New Issue
Block a user