Always escape non-literal arguments when running external command (#2697)

This commit is contained in:
Benoît C 2020-10-26 23:33:40 -04:00 committed by GitHub
parent 1b0ed30516
commit c283db373b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -3,6 +3,7 @@ use crate::evaluate::evaluate_baseline_expr;
use crate::futures::ThreadedReceiver; use crate::futures::ThreadedReceiver;
use crate::prelude::*; use crate::prelude::*;
use std::borrow::Cow;
use std::io::Write; use std::io::Write;
use std::ops::Deref; use std::ops::Deref;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
@ -13,6 +14,7 @@ use futures_codec::FramedRead;
use log::trace; use log::trace;
use nu_errors::ShellError; use nu_errors::ShellError;
use nu_protocol::hir::Expression;
use nu_protocol::hir::{ExternalCommand, ExternalRedirection}; use nu_protocol::hir::{ExternalCommand, ExternalRedirection};
use nu_protocol::{Primitive, Scope, ShellTypeName, UntaggedValue, Value}; use nu_protocol::{Primitive, Scope, ShellTypeName, UntaggedValue, Value};
use nu_source::Tag; use nu_source::Tag;
@ -50,6 +52,7 @@ async fn run_with_stdin(
let mut command_args = vec![]; let mut command_args = vec![];
for arg in command.args.iter() { for arg in command.args.iter() {
let is_literal = matches!(arg.expr, Expression::Literal(_));
let value = evaluate_baseline_expr(arg, &context.registry, scope.clone()).await?; let value = evaluate_baseline_expr(arg, &context.registry, scope.clone()).await?;
// Skip any arguments that don't really exist, treating them as optional // Skip any arguments that don't really exist, treating them as optional
@ -65,8 +68,10 @@ async fn run_with_stdin(
for t in table { for t in table {
match &t.value { match &t.value {
UntaggedValue::Primitive(_) => { UntaggedValue::Primitive(_) => {
command_args command_args.push((
.push(t.convert_to_string().trim_end_matches('\n').to_string()); t.convert_to_string().trim_end_matches('\n').to_string(),
is_literal,
));
} }
_ => { _ => {
return Err(ShellError::labeled_error( return Err(ShellError::labeled_error(
@ -80,14 +85,14 @@ async fn run_with_stdin(
} }
_ => { _ => {
let trimmed_value_string = value.as_string()?.trim_end_matches('\n').to_string(); let trimmed_value_string = value.as_string()?.trim_end_matches('\n').to_string();
command_args.push(trimmed_value_string); command_args.push((trimmed_value_string, is_literal));
} }
} }
} }
let process_args = command_args let process_args = command_args
.iter() .iter()
.map(|arg| { .map(|(arg, _is_literal)| {
let home_dir; let home_dir;
#[cfg(feature = "dirs")] #[cfg(feature = "dirs")]
@ -103,8 +108,9 @@ async fn run_with_stdin(
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
if argument_contains_whitespace(&arg) && !argument_is_quoted(&arg) { if !_is_literal {
add_quotes(&arg) let escaped = escape_double_quotes(&arg);
add_double_quotes(&escaped)
} else { } else {
arg.as_ref().to_string() arg.as_ref().to_string()
} }
@ -476,11 +482,6 @@ where
shellexpand::tilde_with_context(input, home_dir) shellexpand::tilde_with_context(input, home_dir)
} }
#[allow(unused)]
pub fn argument_contains_whitespace(argument: &str) -> bool {
argument.chars().any(|c| c.is_whitespace())
}
fn argument_is_quoted(argument: &str) -> bool { fn argument_is_quoted(argument: &str) -> bool {
if argument.len() < 2 { if argument.len() < 2 {
return false; return false;
@ -491,10 +492,20 @@ fn argument_is_quoted(argument: &str) -> bool {
} }
#[allow(unused)] #[allow(unused)]
fn add_quotes(argument: &str) -> String { fn add_double_quotes(argument: &str) -> String {
format!("\"{}\"", argument) format!("\"{}\"", argument)
} }
#[allow(unused)]
fn escape_double_quotes(argument: &str) -> Cow<'_, str> {
// allocate new string only if required
if argument.contains('"') {
Cow::Owned(argument.replace('"', r#"\""#))
} else {
Cow::Borrowed(argument)
}
}
#[allow(unused)] #[allow(unused)]
fn remove_quotes(argument: &str) -> Option<&str> { fn remove_quotes(argument: &str) -> Option<&str> {
if !argument_is_quoted(argument) { if !argument_is_quoted(argument) {
@ -520,7 +531,7 @@ fn shell_os_paths() -> Vec<std::path::PathBuf> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
add_quotes, argument_contains_whitespace, argument_is_quoted, expand_tilde, remove_quotes, add_double_quotes, argument_is_quoted, escape_double_quotes, expand_tilde, remove_quotes,
}; };
#[cfg(feature = "which")] #[cfg(feature = "which")]
use super::{run_external_command, EvaluationContext, InputStream}; use super::{run_external_command, EvaluationContext, InputStream};
@ -600,10 +611,10 @@ mod tests {
} }
#[test] #[test]
fn checks_contains_whitespace_from_argument_to_be_passed_in() { fn checks_escape_double_quotes() {
assert_eq!(argument_contains_whitespace("andrés"), false); assert_eq!(escape_double_quotes("andrés"), "andrés");
assert_eq!(argument_contains_whitespace("and rés"), true); assert_eq!(escape_double_quotes(r#"an"drés"#), r#"an\"drés"#);
assert_eq!(argument_contains_whitespace(r#"and\ rés"#), true); assert_eq!(escape_double_quotes(r#""an"drés""#), r#"\"an\"drés\""#);
} }
#[test] #[test]
@ -631,9 +642,8 @@ mod tests {
} }
#[test] #[test]
fn adds_quotes_to_argument_to_be_passed_in() { fn adds_double_quotes_to_argument_to_be_passed_in() {
assert_eq!(add_quotes("andrés"), "\"andrés\""); assert_eq!(add_double_quotes("andrés"), "\"andrés\"");
//assert_eq!(add_quotes("\"andrés\""), "\"andrés\"");
} }
#[test] #[test]