From 42d2adc3e037d2ceb3d3708407b148efa7896c96 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Wed, 20 Nov 2024 07:55:26 -0600 Subject: [PATCH] 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 --- crates/nu-command/src/system/run_external.rs | 45 +++++++++++++++++-- .../nu-command/tests/commands/run_external.rs | 41 +++++++++++++++-- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 604278c676..ffb34bf077 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe use nu_system::ForegroundChild; use nu_utils::IgnoreCaseExt; use pathdiff::diff_paths; +#[cfg(windows)] +use std::os::windows::process::CommandExt; use std::{ borrow::Cow, ffi::{OsStr, OsString}, @@ -91,6 +93,22 @@ impl Command for External { 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 // executable to "cmd.exe" if it's a CMD internal command. If the // 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) { 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 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)); }; executable @@ -123,15 +146,29 @@ impl Command for External { let args = eval_arguments_from_call(engine_state, stack, call)?; #[cfg(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 /C flag followed by a command name instructs CMD to execute // that command and quit. - command.args(["/D", "/C", &name_str]); + command.args(["/D", "/C", &expanded_name.to_string_lossy()]); for arg in &args { 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 { command.args(args.into_iter().map(|s| s.item)); } diff --git a/crates/nu-command/tests/commands/run_external.rs b/crates/nu-command/tests/commands/run_external.rs index 17667c9bb3..8a797300f1 100644 --- a/crates/nu-command/tests/commands/run_external.rs +++ b/crates/nu-command/tests/commands/run_external.rs @@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() { #[cfg(windows)] #[test] -fn can_run_batch_files() { +fn can_run_cmd_files() { 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( "foo.cmd", 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)] #[test] fn can_run_batch_files_without_cmd_extension() { use nu_test_support::fs::Stub::FileWithContent; Playground::setup( - "run a Windows batch file without specifying the extension", + "run a Windows cmd file without specifying the extension", |dirs, sandbox| { sandbox.with_files(&[FileWithContent( "foo.cmd", @@ -440,3 +458,20 @@ fn redirect_combine() { 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")); + }); +}