allow ps1 files to be executed without pwsh/powershell -c file.ps1 (#14379)

# Description

This PR allows nushell to run powershell scripts easier. You can already
do `powershell -c script.ps1` but this PR takes it a step further by
doing the `powershell -c` part for you. So, if you have script.ps1 you
can execute it by running it in the command position of the repl.

![image](https://github.com/user-attachments/assets/0661a746-27d9-4d21-b576-c244ff7fab2b)

or once it's in json, just consume it with nushell.

![image](https://github.com/user-attachments/assets/38f5c5d8-3659-41f0-872b-91a14909760b)

# User-Facing Changes
Easier to run powershell scripts. It should work on Windows with
powershell.exe.

# Tests + Formatting
Added 1 test

# After Submitting


---------

Co-authored-by: Wind <WindSoilder@outlook.com>
This commit is contained in:
Darren Schroeder 2024-11-20 07:55:26 -06:00 committed by GitHub
parent 5d1eb031eb
commit 42d2adc3e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 7 deletions

View file

@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe
use nu_system::ForegroundChild; use nu_system::ForegroundChild;
use nu_utils::IgnoreCaseExt; use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths; use pathdiff::diff_paths;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
@ -91,6 +93,22 @@ impl Command for External {
false false
}; };
// let's make sure it's a .ps1 script, but only on Windows
let potential_powershell_script = if cfg!(windows) {
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "PS1"
} 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.
@ -98,11 +116,16 @@ impl Command for External {
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows) && (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
{ {
PathBuf::from("cmd.exe") 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 { } 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
// effect if it's an absolute path already // effect if it's an absolute path already
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else { let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
return Err(command_not_found(&name_str, call.head, engine_state, stack)); return Err(command_not_found(&name_str, call.head, engine_state, stack));
}; };
executable executable
@ -123,15 +146,29 @@ impl Command for External {
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) || potential_nuscript_in_windows { if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
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.
// The /C flag followed by a command name instructs CMD to execute // The /C flag followed by a command name instructs CMD to execute
// that command and quit. // that command and quit.
command.args(["/D", "/C", &name_str]); command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
for arg in &args { for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?); command.raw_arg(escape_cmd_argument(arg)?);
} }
} else if potential_powershell_script {
use nu_path::canonicalize_with;
// canonicalize the path to the script so that tests pass
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
canonicalize_with(&expanded_name, cwd)?
} else {
// If we can't get the current working directory, just provide the expanded name
expanded_name
};
// The -Command flag followed by a script name instructs PowerShell to
// execute that script and quit.
command.args(["-Command", &canon_path.to_string_lossy()]);
for arg in &args {
command.raw_arg(arg.item.clone());
}
} else { } else {
command.args(args.into_iter().map(|s| s.item)); command.args(args.into_iter().map(|s| s.item));
} }

View file

@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() {
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files() { fn can_run_cmd_files() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| { Playground::setup("run a Windows cmd file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
r#" r#"
@ -371,12 +371,30 @@ fn can_run_batch_files() {
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_batch_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run a Windows batch file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.bat",
r#"
@echo off
echo Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.bat"));
assert!(actual.out.contains("Hello World"));
});
}
#[cfg(windows)] #[cfg(windows)]
#[test] #[test]
fn can_run_batch_files_without_cmd_extension() { fn can_run_batch_files_without_cmd_extension() {
use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContent;
Playground::setup( Playground::setup(
"run a Windows batch file without specifying the extension", "run a Windows cmd file without specifying the extension",
|dirs, sandbox| { |dirs, sandbox| {
sandbox.with_files(&[FileWithContent( sandbox.with_files(&[FileWithContent(
"foo.cmd", "foo.cmd",
@ -440,3 +458,20 @@ fn redirect_combine() {
assert_eq!(actual.out, "FooBar"); assert_eq!(actual.out, "FooBar");
}); });
} }
#[cfg(windows)]
#[test]
fn can_run_ps1_files() {
use nu_test_support::fs::Stub::FileWithContent;
Playground::setup("run_a_windows_ps_file", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"foo.ps1",
r#"
Write-Host Hello World
"#,
)]);
let actual = nu!(cwd: dirs.test(), pipeline("foo.ps1"));
assert!(actual.out.contains("Hello World"));
});
}