feat: spawn the executables directly if possible (#3974)

* feat: spawn the executables directly if possible

This pull request changes nu-command so that it spawns the process directly if:
- They are a `.exe` on Windows
- They are not a `.sh` or `.bash` on not windows.

Benefits:
- As I explained in [this comment](https://github.com/nushell/nushell/issues/3898#issuecomment-894000812), this is another step towards making Nushell a standalone shell, that doesn't need to shell out unless it is running a script for a particular shell (cmd, sh, ps1, etc.).
- Fixes the bug with multiline strings
- Better performance due to direct spawning.

For example, this script shows ~20 ms less latency.
After:
```nu
C:\> benchmark { node -e 'console.log("sssss")' }
───┬──────────────────
 # │    real time
───┼──────────────────
 0 │ 63ms 921us 600ns
───┴──────────────────
```
Before
```nu
C:\> benchmark { node -e 'console.log("sssss")' }
───┬──────────────────
 # │    real time
───┼──────────────────
 0 │ 79ms 136us 800ns
───┴──────────────────
```

Fixes #3898

* fix: make which dependency optional

Also fixes clippy warnings

* refactor: refactor spawn_exe, spawn_cmd, spawn_sh, and spawn_any

* fix: use which feature instead of which-support

* fix: use which_in to use the cwd of nu

* fix: use case insensitive comparison of the extensions

Sometimes the case of the extension is uppercased by the "which_in" function

Also use unix instead of not windows. Some os might not have sh support
This commit is contained in:
Amin Yahyaabadi 2021-09-01 09:38:52 -05:00 committed by GitHub
parent 08014c6a98
commit 260ff99710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -4,7 +4,10 @@ use nu_engine::{MaybeTextCodec, StringOrBinary};
use nu_test_support::NATIVE_PATH_ENV_VAR; use nu_test_support::NATIVE_PATH_ENV_VAR;
use parking_lot::Mutex; use parking_lot::Mutex;
#[allow(unused)]
use std::env;
use std::io::Write; use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::mpsc; use std::sync::mpsc;
use std::{borrow::Cow, io::BufReader}; use std::{borrow::Cow, io::BufReader};
@ -17,6 +20,9 @@ use nu_protocol::hir::{ExternalCommand, ExternalRedirection};
use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value};
use nu_source::Tag; use nu_source::Tag;
#[cfg(feature = "which")]
use which::which_in;
pub(crate) fn run_external_command( pub(crate) fn run_external_command(
command: ExternalCommand, command: ExternalCommand,
context: &mut EvaluationContext, context: &mut EvaluationContext,
@ -141,19 +147,18 @@ fn run_with_stdin(
) )
} }
fn spawn( /// Spawn a direct exe
command: &ExternalCommand, #[allow(unused)]
path: &str, fn spawn_exe(full_path: PathBuf, args: &[String]) -> Command {
args: &[String], let mut process = Command::new(full_path);
input: InputStream, for arg in args {
external_redirection: ExternalRedirection, process.arg(&arg);
scope: &Scope, }
) -> Result<InputStream, ShellError> { process
let command = command.clone(); }
let mut process = { /// Spawn a cmd command with `cmd /c args...`
#[cfg(windows)] fn spawn_cmd_command(command: &ExternalCommand, args: &[String]) -> Command {
{
let mut process = Command::new("cmd"); let mut process = Command::new("cmd");
process.arg("/c"); process.arg("/c");
process.arg(&command.name); process.arg(&command.name);
@ -165,17 +170,67 @@ fn spawn(
process.arg(&arg); process.arg(&arg);
} }
process process
} }
#[cfg(not(windows))] /// Spawn a sh command with `sh -c args...`
{ fn spawn_sh_command(command: &ExternalCommand, args: &[String]) -> Command {
let cmd_with_args = vec![command.name.clone(), args.join(" ")].join(" "); let cmd_with_args = vec![command.name.clone(), args.join(" ")].join(" ");
let mut process = Command::new("sh"); let mut process = Command::new("sh");
process.arg("-c").arg(cmd_with_args); process.arg("-c").arg(cmd_with_args);
process process
} }
};
/// a function to spawn any external command
#[allow(unused)] // for minimal builds cwd is unused
fn spawn_any(command: &ExternalCommand, args: &[String], cwd: &str) -> Command {
// resolve the executable name if it is spawnable directly
#[cfg(feature = "which")]
// TODO add more available paths to `env::var_os("PATH")`?
if let Result::Ok(full_path) = which_in(&command.name, env::var_os("PATH"), cwd) {
if let Some(extension) = full_path.extension() {
#[cfg(windows)]
if extension.eq_ignore_ascii_case("exe") {
// if exe spawn it directly
return spawn_exe(full_path, args);
} else {
// TODO implement special care for various executable types such as .bat, .ps1, .cmd, etc
// https://github.com/mklement0/Native/blob/e0e0b8785cad39a73053e35084d1f60d87fbac58/Native.psm1#L749
// otherwise shell out to cmd
return spawn_cmd_command(command, args);
}
#[cfg(not(windows))]
if !["sh", "bash"]
.iter()
.any(|ext| extension.eq_ignore_ascii_case(ext))
{
// if exe spawn it directly
return spawn_exe(full_path, args);
} else {
// otherwise shell out to sh
return spawn_sh_command(command, args);
}
}
}
// in all the other cases shell out
if cfg!(windows) {
spawn_cmd_command(command, args)
} else {
// TODO what happens if that os doesn't support spawning sh?
spawn_sh_command(command, args)
}
}
fn spawn(
command: &ExternalCommand,
path: &str,
args: &[String],
input: InputStream,
external_redirection: ExternalRedirection,
scope: &Scope,
) -> Result<InputStream, ShellError> {
let command = command.clone();
let mut process = spawn_any(&command, args, path);
process.current_dir(path); process.current_dir(path);
trace!(target: "nu::run::external", "cwd = {:?}", &path); trace!(target: "nu::run::external", "cwd = {:?}", &path);