From ccd5f593148f9aa241f6615ef0b7b6fe4ba1d116 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Fri, 3 Dec 2021 09:55:16 +1300 Subject: [PATCH] Update external spawn (#406) * Simplify external spawn, improve arg cleaning * Fix tests * Fix windows test --- crates/nu-command/src/system/run_external.rs | 77 ++++++++++++++++---- src/tests.rs | 4 +- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 3d81f31f08..3994e326a8 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -10,7 +10,10 @@ use nu_protocol::engine::{EngineState, Stack}; use nu_protocol::{ast::Call, engine::Command, ShellError, Signature, SyntaxShape, Value}; use nu_protocol::{Category, Config, IntoInterruptiblePipelineData, PipelineData, Span, Spanned}; +use itertools::Itertools; + use nu_engine::CallExt; +use regex::Regex; const OUTPUT_BUFFER_SIZE: usize = 8192; @@ -206,22 +209,68 @@ impl ExternalCommand { //TODO. This should be modifiable from the config file. // We could give the option to call from powershell // for minimal builds cwd is unused - let mut process = CommandSys::new("cmd"); - process.arg("/c"); - process.arg(&self.name.item); - for arg in &self.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); + if self.name.item.ends_with(".cmd") || self.name.item.ends_with(".bat") { + self.spawn_cmd_command() + } else { + self.spawn_simple_command() } - process + } else if self.name.item.ends_with(".sh") { + self.spawn_sh_command() } else { - let cmd_with_args = vec![self.name.item.clone(), self.args.join(" ")].join(" "); - let mut process = CommandSys::new("sh"); - process.arg("-c").arg(cmd_with_args); - process + self.spawn_simple_command() + } + } + + /// Spawn a command without shelling out to an external shell + fn spawn_simple_command(&self) -> std::process::Command { + let mut process = std::process::Command::new(&self.name.item); + + for arg in &self.args { + process.arg(&arg); + } + + process + } + + /// Spawn a cmd command with `cmd /c args...` + fn spawn_cmd_command(&self) -> std::process::Command { + let mut process = std::process::Command::new("cmd"); + process.arg("/c"); + process.arg(&self.name.item); + for arg in &self.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(&self) -> std::process::Command { + let joined_and_escaped_arguments = + self.args.iter().map(|arg| shell_arg_escape(arg)).join(" "); + let cmd_with_args = vec![self.name.item.clone(), joined_and_escaped_arguments].join(" "); + let mut process = std::process::Command::new("sh"); + process.arg("-c").arg(cmd_with_args); + process + } +} + +fn has_unsafe_shell_characters(arg: &str) -> bool { + let re: Regex = Regex::new(r"[^\w@%+=:,./-]").expect("regex to be valid"); + + re.is_match(arg) +} + +fn shell_arg_escape(arg: &str) -> String { + match arg { + "" => String::from("''"), + s if !has_unsafe_shell_characters(s) => String::from(s), + _ => { + let single_quotes_escaped = arg.split('\'').join("'\"'\"'"); + format!("'{}'", single_quotes_escaped) } } } diff --git a/src/tests.rs b/src/tests.rs index aacf23f3ff..5b8a359a57 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -56,9 +56,9 @@ fn fail_test(input: &str, expected: &str) -> TestResult { fn not_found_msg() -> &'static str { if cfg!(windows) { - "not recognized" + "cannot find" } else { - "not found" + "No such" } }