diff --git a/crates/nu-parser/src/commands/classified/external.rs b/crates/nu-parser/src/commands/classified/external.rs index d38531623..705569f0a 100644 --- a/crates/nu-parser/src/commands/classified/external.rs +++ b/crates/nu-parser/src/commands/classified/external.rs @@ -14,6 +14,18 @@ impl ExternalArg { pub fn is_it(&self) -> bool { self.has("$it") } + + pub fn is_nu(&self) -> bool { + self.has("$nu") + } + + pub fn looks_like_it(&self) -> bool { + self.arg.starts_with("$it") && (self.arg.starts_with("$it.") || self.is_it()) + } + + pub fn looks_like_nu(&self) -> bool { + self.arg.starts_with("$nu") && (self.arg.starts_with("$nu.") || self.is_nu()) + } } impl std::ops::Deref for ExternalArg { @@ -54,7 +66,11 @@ pub struct ExternalCommand { impl ExternalCommand { pub fn has_it_argument(&self) -> bool { - self.args.iter().any(|arg| arg.has("$it")) + self.args.iter().any(|arg| arg.looks_like_it()) + } + + pub fn has_nu_argument(&self) -> bool { + self.args.iter().any(|arg| arg.looks_like_nu()) } } diff --git a/src/commands/classified/external.rs b/src/commands/classified/external.rs index da61f5fec..33c275d7f 100644 --- a/src/commands/classified/external.rs +++ b/src/commands/classified/external.rs @@ -3,8 +3,11 @@ use futures::stream::StreamExt; use futures_codec::{FramedRead, LinesCodec}; use log::trace; use nu_errors::ShellError; +use nu_parser::commands::classified::external::ExternalArg; use nu_parser::ExternalCommand; -use nu_protocol::{Primitive, ShellTypeName, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::as_column_path; use std::io::Write; use std::ops::Deref; use std::process::{Command, Stdio}; @@ -16,7 +19,7 @@ pub fn nu_value_to_string(command: &ExternalCommand, from: &Value) -> Result Ok(s.clone()), UntaggedValue::Primitive(Primitive::Path(p)) => Ok(p.to_string_lossy().to_string()), unsupported => Err(ShellError::labeled_error( - format!("$it needs string data (given: {})", unsupported.type_name()), + format!("needs string data (given: {})", unsupported.type_name()), "expected a string", &command.name_tag, )), @@ -58,13 +61,59 @@ pub(crate) async fn run_external_command( )); } - if command.has_it_argument() { + if command.has_it_argument() || command.has_nu_argument() { run_with_iterator_arg(command, context, input, is_last).await } else { run_with_stdin(command, context, input, is_last).await } } +fn prepare_column_path_for_fetching_it_variable( + argument: &ExternalArg, +) -> Result, ShellError> { + // We have "$it.[contents of interest]" + // and start slicing from "$it.[member+]" + // ^ here. + let key = nu_source::Text::from(argument.deref()).slice(4..argument.len()); + + to_column_path(&key, &argument.tag) +} + +fn prepare_column_path_for_fetching_nu_variable( + argument: &ExternalArg, +) -> Result, ShellError> { + // We have "$nu.[contents of interest]" + // and start slicing from "$nu.[member+]" + // ^ here. + let key = nu_source::Text::from(argument.deref()).slice(4..argument.len()); + + to_column_path(&key, &argument.tag) +} + +fn to_column_path( + path_members: &str, + tag: impl Into, +) -> Result, ShellError> { + let tag = tag.into(); + + as_column_path( + &UntaggedValue::Table( + path_members + .split('.') + .map(|x| { + let member = match x.parse::() { + Ok(v) => UntaggedValue::int(v), + Err(_) => UntaggedValue::string(x), + }; + + member.into_value(&tag) + }) + .collect(), + ) + .into_value(&tag), + ) +} + async fn run_with_iterator_arg( command: ExternalCommand, context: &mut Context, @@ -87,14 +136,136 @@ async fn run_with_iterator_arg( let path = &path; let args = command.args.clone(); - let it_replacement = match nu_value_to_string(&command, &value) { - Ok(value) => value, - Err(reason) => { - yield Ok(Value { - value: UntaggedValue::Error(reason), - tag: name_tag - }); - return; + let it_replacement = { + if command.has_it_argument() { + let empty_arg = ExternalArg { + arg: "".to_string(), + tag: name_tag.clone() + }; + + let key = args.iter() + .find(|arg| arg.looks_like_it()) + .unwrap_or_else(|| &empty_arg); + + if args.iter().all(|arg| !arg.is_it()) { + let key = match prepare_column_path_for_fetching_it_variable(&key) { + Ok(keypath) => keypath, + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + } + }; + + match crate::commands::get::get_column_path(&key, &value) { + Ok(field) => { + match nu_value_to_string(&command, &field) { + Ok(val) => Some(val), + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + }, + } + }, + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + } + } + } else { + match nu_value_to_string(&command, &value) { + Ok(val) => Some(val), + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + }, + } + } + } else { + None + } + }; + + let nu_replacement = { + if command.has_nu_argument() { + let empty_arg = ExternalArg { + arg: "".to_string(), + tag: name_tag.clone() + }; + + let key = args.iter() + .find(|arg| arg.looks_like_nu()) + .unwrap_or_else(|| &empty_arg); + + let nu_var = match crate::evaluate::variables::nu(&name_tag) { + Ok(variables) => variables, + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + } + }; + + if args.iter().all(|arg| !arg.is_nu()) { + let key = match prepare_column_path_for_fetching_nu_variable(&key) { + Ok(keypath) => keypath, + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + } + }; + + match crate::commands::get::get_column_path(&key, &nu_var) { + Ok(field) => { + match nu_value_to_string(&command, &field) { + Ok(val) => Some(val), + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + }, + } + }, + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + } + } + } else { + match nu_value_to_string(&command, &nu_var) { + Ok(val) => Some(val), + Err(reason) => { + yield Ok(Value { + value: UntaggedValue::Error(reason), + tag: name_tag + }); + return; + }, + } + } + } else { + None } }; @@ -102,25 +273,44 @@ async fn run_with_iterator_arg( if arg.chars().all(|c| c.is_whitespace()) { None } else { - let arg = if arg.is_it() { - let value = it_replacement.to_owned(); - let mut value = expand_tilde(&value, || home_dir.as_ref()).as_ref().to_string(); - #[cfg(not(windows))] - { - value = { - if argument_contains_whitespace(&value) && !argument_is_quoted(&value) { - add_quotes(&value) - } else { - value - } - }; + let arg = if arg.looks_like_it() { + if let Some(mut value) = it_replacement.to_owned() { + let mut value = expand_tilde(&value, || home_dir.as_ref()).as_ref().to_string(); + #[cfg(not(windows))] + { + value = { + if argument_contains_whitespace(&value) && !argument_is_quoted(&value) { + add_quotes(&value) + } else { + value + } + }; + } + Some(value) + } else { + None + } + } else if arg.looks_like_nu() { + if let Some(mut value) = nu_replacement.to_owned() { + #[cfg(not(windows))] + { + value = { + if argument_contains_whitespace(&value) && !argument_is_quoted(&value) { + add_quotes(&value) + } else { + value + } + }; + } + Some(value) + } else { + None } - value } else { - arg.to_string() + Some(arg.to_string()) }; - Some(arg) + arg } }).collect::>(); diff --git a/src/evaluate/evaluator.rs b/src/evaluate/evaluator.rs index 186127b88..d649e8f6e 100644 --- a/src/evaluate/evaluator.rs +++ b/src/evaluate/evaluator.rs @@ -6,8 +6,8 @@ use log::trace; use nu_errors::{ArgumentError, ShellError}; use nu_parser::hir::{self, Expression, SpannedExpression}; use nu_protocol::{ - ColumnPath, Evaluate, Primitive, RangeInclusion, Scope, TaggedDictBuilder, UnspannedPathMember, - UntaggedValue, Value, + ColumnPath, Evaluate, Primitive, RangeInclusion, Scope, UnspannedPathMember, UntaggedValue, + Value, }; use nu_source::Text; @@ -158,31 +158,7 @@ fn evaluate_reference( match name { hir::Variable::It(_) => Ok(scope.it.value.clone().into_value(tag)), hir::Variable::Other(inner) => match inner.slice(source) { - x if x == "nu" => { - let mut nu_dict = TaggedDictBuilder::new(&tag); - - let mut dict = TaggedDictBuilder::new(&tag); - for v in std::env::vars() { - if v.0 != "PATH" && v.0 != "Path" { - dict.insert_untagged(v.0, UntaggedValue::string(v.1)); - } - } - nu_dict.insert_value("env", dict.into_value()); - - let config = crate::data::config::read(&tag, &None)?; - nu_dict.insert_value("config", UntaggedValue::row(config).into_value(&tag)); - - let mut table = vec![]; - let path = std::env::var_os("PATH"); - if let Some(paths) = path { - for path in std::env::split_paths(&paths) { - table.push(UntaggedValue::path(path).into_value(&tag)); - } - } - nu_dict.insert_value("path", UntaggedValue::table(&table).into_value(&tag)); - - Ok(nu_dict.into_value()) - } + x if x == "nu" => crate::evaluate::variables::nu(tag), x => Ok(scope .vars .get(x) diff --git a/src/evaluate/mod.rs b/src/evaluate/mod.rs index a94d84372..c72bc473f 100644 --- a/src/evaluate/mod.rs +++ b/src/evaluate/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod evaluate_args; pub(crate) mod evaluator; pub(crate) mod operator; +pub(crate) mod variables; pub(crate) use evaluator::evaluate_baseline_expr; diff --git a/src/evaluate/variables.rs b/src/evaluate/variables.rs new file mode 100644 index 000000000..d3d4ddc05 --- /dev/null +++ b/src/evaluate/variables.rs @@ -0,0 +1,31 @@ +use nu_errors::ShellError; +use nu_protocol::{TaggedDictBuilder, UntaggedValue, Value}; +use nu_source::Tag; + +pub fn nu(tag: impl Into) -> Result { + let tag = tag.into(); + + let mut nu_dict = TaggedDictBuilder::new(&tag); + + let mut dict = TaggedDictBuilder::new(&tag); + for v in std::env::vars() { + if v.0 != "PATH" && v.0 != "Path" { + dict.insert_untagged(v.0, UntaggedValue::string(v.1)); + } + } + nu_dict.insert_value("env", dict.into_value()); + + let config = crate::data::config::read(&tag, &None)?; + nu_dict.insert_value("config", UntaggedValue::row(config).into_value(&tag)); + + let mut table = vec![]; + let path = std::env::var_os("PATH"); + if let Some(paths) = path { + for path in std::env::split_paths(&paths) { + table.push(UntaggedValue::path(path).into_value(&tag)); + } + } + nu_dict.insert_value("path", UntaggedValue::table(&table).into_value(&tag)); + + Ok(nu_dict.into_value()) +} diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs index 05cbdbc66..ebfa324b5 100644 --- a/tests/shell/pipeline/commands/external.rs +++ b/tests/shell/pipeline/commands/external.rs @@ -22,7 +22,7 @@ fn shows_error_for_command_not_found() { mod it_evaluation { use super::nu; - use nu_test_support::fs::Stub::{EmptyFile, FileWithContentToBeTrimmed}; + use nu_test_support::fs::Stub::{EmptyFile, FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::{pipeline, playground::Playground}; #[test] @@ -76,6 +76,29 @@ mod it_evaluation { assert_eq!(actual, "AndrásWithKitKat"); }) } + + #[test] + fn supports_fetching_given_a_column_path_to_it() { + Playground::setup("it_argument_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + nu_party_venue = "zion" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | cococo $it.nu_party_venue + | echo $it + "# + )); + + assert_eq!(actual, "zion"); + }) + } } mod stdin_evaluation {