allow nuscripts to be run again on windows with assoc/ftype (#14318)

# Description

This PR tries to correct the problem of nushell scripts being made
executable on Windows systems. In order to do this, these steps need to
take place.
1. `assoc .nu=nuscript`
2. `ftype nuscript=C:\path\to\nu.exe '%1' %*`
3. modify the env var PATHEXT by appending `;.NU` at the end
 
Once those steps are done and this PR is landed, one should be able to
create a script such as this.
```nushell
❯ open im_exe.nu
def main [arg] {
  print $"Hello ($arg)!"
}
```
Then they should be able to do this to run the nushell script.
```nushell
❯ im_exe Nushell
Hello Nushell!
```

Under-the-hood, nushell is shelling out to cmd.exe in order to run the
nushell script.

# User-Facing Changes
closes #13020

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
Darren Schroeder 2024-11-15 06:39:42 -06:00 committed by GitHub
parent 8c1ab7e0a3
commit f7832c0e82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -51,7 +51,6 @@ impl Command for External {
input: PipelineData, input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let cwd = engine_state.cwd(Some(stack))?; let cwd = engine_state.cwd(Some(stack))?;
let name: Value = call.req(engine_state, stack, 0)?; let name: Value = call.req(engine_state, stack, 0)?;
let name_str: Cow<str> = match &name { let name_str: Cow<str> = match &name {
@ -68,10 +67,36 @@ impl Command for External {
_ => Path::new(&*name_str).to_owned(), _ => Path::new(&*name_str).to_owned(),
}; };
// 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
// 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 scrtipt
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "NU"
} else {
false
}
} else {
false
};
// Find the absolute path to the executable. On Windows, set the // Find the absolute path to the executable. On Windows, set the
// executable to "cmd.exe" if it's a CMD internal command. If the // executable to "cmd.exe" if it's a CMD internal command. If the
// command is not found, display a helpful error message. // command is not found, display a helpful error message.
let executable = if cfg!(windows) && is_cmd_internal_command(&name_str) { let executable = if cfg!(windows)
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
{
PathBuf::from("cmd.exe") PathBuf::from("cmd.exe")
} else { } else {
// Determine the PATH to be used and then use `which` to find it - though this has no // Determine the PATH to be used and then use `which` to find it - though this has no
@ -97,7 +122,7 @@ impl Command for External {
// Configure args. // Configure args.
let args = eval_arguments_from_call(engine_state, stack, call)?; let args = eval_arguments_from_call(engine_state, stack, call)?;
#[cfg(windows)] #[cfg(windows)]
if is_cmd_internal_command(&name_str) { if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
// The /D flag disables execution of AutoRun commands from registry. // The /D flag disables execution of AutoRun commands from registry.