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 parking_lot::Mutex;
#[allow(unused)]
use std::env;
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
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_source::Tag;
#[cfg(feature = "which")]
use which::which_in;
pub(crate) fn run_external_command(
command: ExternalCommand,
context: &mut EvaluationContext,
@ -141,6 +147,79 @@ fn run_with_stdin(
)
}
/// Spawn a direct exe
#[allow(unused)]
fn spawn_exe(full_path: PathBuf, args: &[String]) -> Command {
let mut process = Command::new(full_path);
for arg in args {
process.arg(&arg);
}
process
}
/// Spawn a cmd command with `cmd /c args...`
fn spawn_cmd_command(command: &ExternalCommand, args: &[String]) -> Command {
let mut process = Command::new("cmd");
process.arg("/c");
process.arg(&command.name);
for arg in args {
// Clean the args before we use them:
// https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe
// cmd.exe needs to have a caret to escape a pipe
let arg = arg.replace("|", "^|");
process.arg(&arg);
}
process
}
/// 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 mut process = Command::new("sh");
process.arg("-c").arg(cmd_with_args);
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,
@ -151,31 +230,7 @@ fn spawn(
) -> Result<InputStream, ShellError> {
let command = command.clone();
let mut process = {
#[cfg(windows)]
{
let mut process = Command::new("cmd");
process.arg("/c");
process.arg(&command.name);
for arg in args {
// Clean the args before we use them:
// https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe
// cmd.exe needs to have a caret to escape a pipe
let arg = arg.replace("|", "^|");
process.arg(&arg);
}
process
}
#[cfg(not(windows))]
{
let cmd_with_args = vec![command.name.clone(), args.join(" ")].join(" ");
let mut process = Command::new("sh");
process.arg("-c").arg(cmd_with_args);
process
}
};
let mut process = spawn_any(&command, args, path);
process.current_dir(path);
trace!(target: "nu::run::external", "cwd = {:?}", &path);