Fix bug with multiple input objects to an external command.

Previously, we would build a command that looked something like this:

  <ex_cmd> "$it" "&&" "<ex_cmd>" "$it"

So that the "&&" and "<ex_cmd>" would also be arguments to the command,
instead of a chained command. This commit builds up a command string
that can be passed to an external shell.
This commit is contained in:
Jason Gedge 2019-10-14 16:30:23 -04:00
parent d38b8cf851
commit ee8cd671cb
3 changed files with 110 additions and 40 deletions

View file

@ -225,7 +225,6 @@ impl ExternalCommand {
) -> Result<ClassifiedInputStream, ShellError> { ) -> Result<ClassifiedInputStream, ShellError> {
let stdin = input.stdin; let stdin = input.stdin;
let inputs: Vec<Tagged<Value>> = input.objects.into_vec().await; let inputs: Vec<Tagged<Value>> = input.objects.into_vec().await;
let name_tag = self.name_tag.clone();
trace!(target: "nu::run::external", "-> {}", self.name); trace!(target: "nu::run::external", "-> {}", self.name);
trace!(target: "nu::run::external", "inputs = {:?}", inputs); trace!(target: "nu::run::external", "inputs = {:?}", inputs);
@ -235,53 +234,47 @@ impl ExternalCommand {
arg_string.push_str(&arg); arg_string.push_str(&arg);
} }
trace!(target: "nu::run::external", "command = {:?}", self.name);
let mut process; let mut process;
process = Exec::cmd(&self.name);
trace!(target: "nu::run::external", "command = {:?}", process);
if arg_string.contains("$it") { if arg_string.contains("$it") {
let mut first = true; let input_strings = inputs
.iter()
for i in &inputs { .map(|i| {
if i.as_string().is_err() { i.as_string().map_err(|_| {
let mut tag = None; let arg = self.args.iter().find(|arg| arg.item.contains("$it"));
for arg in &self.args { if let Some(arg) = arg {
if arg.item.contains("$it") { ShellError::labeled_error(
tag = Some(arg.tag()); "External $it needs string data",
"given row instead of string data",
arg.tag(),
)
} else {
ShellError::labeled_error(
"Error: $it needs string data",
"given something else",
self.name_tag.clone(),
)
} }
} })
if let Some(tag) = tag { })
return Err(ShellError::labeled_error( .collect::<Result<Vec<String>, ShellError>>()?;
"External $it needs string data",
"given row instead of string data",
tag,
));
} else {
return Err(ShellError::labeled_error(
"Error: $it needs string data",
"given something else",
name_tag,
));
}
}
if !first {
process = process.arg("&&");
process = process.arg(&self.name);
} else {
first = false;
}
for arg in &self.args { let commands = input_strings.iter().map(|i| {
let args = self.args.iter().filter_map(|arg| {
if arg.chars().all(|c| c.is_whitespace()) { if arg.chars().all(|c| c.is_whitespace()) {
continue; None
} else {
Some(arg.replace("$it", &i))
} }
});
process = process.arg(&arg.replace("$it", &i.as_string()?)); format!("{} {}", self.name, itertools::join(args, " "))
} });
}
process = Exec::shell(itertools::join(commands, " && "))
} else { } else {
process = Exec::cmd(&self.name);
for arg in &self.args { for arg in &self.args {
let arg_chars: Vec<_> = arg.chars().collect(); let arg_chars: Vec<_> = arg.chars().collect();
if arg_chars.len() > 1 if arg_chars.len() > 1
@ -321,6 +314,7 @@ impl ExternalCommand {
trace!(target: "nu::run::external", "next = {:?}", stream_next); trace!(target: "nu::run::external", "next = {:?}", stream_next);
let name_tag = self.name_tag.clone();
if let Ok(mut popen) = popen { if let Ok(mut popen) = popen {
match stream_next { match stream_next {
StreamNext::Last => { StreamNext::Last => {

View file

@ -164,6 +164,28 @@ fn save_figures_out_intelligently_where_to_write_out_with_metadata() {
}) })
} }
#[test]
fn it_arg_works_with_many_inputs_to_external_command() {
Playground::setup("it_arg_works_with_many_inputs", |dirs, sandbox| {
sandbox.with_files(vec![
FileWithContent("file1", "text"),
FileWithContent("file2", " and more text"),
]);
let (stdout, stderr) = nu_combined!(
cwd: dirs.test(), h::pipeline(
r#"
echo file1 file2
| split-row " "
| cat $it
"#
));
assert_eq!("text and more text", stdout);
assert!(!stderr.contains("No such file or directory"));
})
}
#[test] #[test]
fn save_can_write_out_csv() { fn save_can_write_out_csv() {
Playground::setup("save_test_2", |dirs, _| { Playground::setup("save_test_2", |dirs, _| {

View file

@ -155,6 +155,60 @@ macro_rules! nu_error {
}}; }};
} }
#[macro_export]
macro_rules! nu_combined {
(cwd: $cwd:expr, $path:expr, $($part:expr),*) => {{
use $crate::helpers::DisplayPath;
let path = format!($path, $(
$part.display_path()
),*);
nu_combined!($cwd, &path)
}};
(cwd: $cwd:expr, $path:expr) => {{
nu_combined!($cwd, $path)
}};
($cwd:expr, $path:expr) => {{
pub use std::error::Error;
pub use std::io::prelude::*;
pub use std::process::{Command, Stdio};
let commands = &*format!(
"
cd {}
{}
exit",
$crate::helpers::in_directory($cwd),
$crate::helpers::DisplayPath::display_path(&$path)
);
let mut process = Command::new(helpers::executable_path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("couldn't run test");
let stdin = process.stdin.as_mut().expect("couldn't open stdin");
stdin
.write_all(commands.as_bytes())
.expect("couldn't write to stdin");
let output = process
.wait_with_output()
.expect("couldn't read from stdout/stderr");
let err = String::from_utf8_lossy(&output.stderr).into_owned();
let out = String::from_utf8_lossy(&output.stdout).into_owned();
let out = out.replace("\r\n", "");
let out = out.replace("\n", "");
(out, err)
}};
}
pub enum Stub<'a> { pub enum Stub<'a> {
FileWithContent(&'a str, &'a str), FileWithContent(&'a str, &'a str),
FileWithContentToBeTrimmed(&'a str, &'a str), FileWithContentToBeTrimmed(&'a str, &'a str),