diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 1b586c54be..30389ebc88 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -284,6 +284,7 @@ pub fn create_default_context( whole_stream_command(Lines), whole_stream_command(Trim), whole_stream_command(Echo), + whole_stream_command(BuildString), // Column manipulation whole_stream_command(Reject), whole_stream_command(Select), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 4d37e38b0e..cc67db0854 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -8,6 +8,7 @@ pub(crate) mod alias; pub(crate) mod append; pub(crate) mod args; pub(crate) mod autoview; +pub(crate) mod build_string; pub(crate) mod cal; pub(crate) mod calc; pub(crate) mod cd; @@ -132,6 +133,7 @@ pub(crate) use command::{ pub(crate) use alias::Alias; pub(crate) use append::Append; +pub(crate) use build_string::BuildString; pub(crate) use cal::Cal; pub(crate) use calc::Calc; pub(crate) use compact::Compact; diff --git a/crates/nu-cli/src/commands/build_string.rs b/crates/nu-cli/src/commands/build_string.rs new file mode 100644 index 0000000000..8e42ffbf6f --- /dev/null +++ b/crates/nu-cli/src/commands/build_string.rs @@ -0,0 +1,65 @@ +use crate::prelude::*; +use nu_errors::ShellError; + +use crate::commands::WholeStreamCommand; +use crate::data::value::format_leaf; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; + +#[derive(Deserialize)] +pub struct BuildStringArgs { + rest: Vec, +} + +pub struct BuildString; + +impl WholeStreamCommand for BuildString { + fn name(&self) -> &str { + "build-string" + } + + fn signature(&self) -> Signature { + Signature::build("build-string") + .rest(SyntaxShape::Any, "all values to form into the string") + } + + fn usage(&self) -> &str { + "Builds a string from the arguments" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + build_string(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Builds a string from a string and a number, without spaces between them", + example: "build-string 'foo' 3", + result: None, + }] + } +} + +pub fn build_string( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + let tag = args.call_info.name_tag.clone(); + let stream = async_stream! { + let (BuildStringArgs { rest }, mut input) = args.process(®istry).await?; + + let mut output_string = String::new(); + + for r in rest { + output_string.push_str(&format_leaf(&r).plain_string(100_000)) + } + + yield Ok(ReturnSuccess::Value(UntaggedValue::string(&output_string).into_value(tag))); + }; + + Ok(stream.to_output_stream()) +} diff --git a/crates/nu-parser/src/lite_parse.rs b/crates/nu-parser/src/lite_parse.rs index dbd76c0a67..4dcfae653f 100644 --- a/crates/nu-parser/src/lite_parse.rs +++ b/crates/nu-parser/src/lite_parse.rs @@ -69,7 +69,7 @@ fn bare(src: &mut Input, span_offset: usize) -> Result, ParseErr if c == delimiter { inside_quote = false; } - } else if c == '\'' || c == '"' { + } else if c == '\'' || c == '"' || c == '`' { inside_quote = true; delimiter = c; } else if c == '[' { @@ -154,12 +154,6 @@ fn pipeline(src: &mut Input, span_offset: usize) -> Result { - // let c = *c; - // // quoted string - // let arg = quoted(src, c, span_offset)?; - // cmd.args.push(arg); - // } _ => { // basic argument let arg = bare(src, span_offset)?; diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index aeebe937f9..45719cb703 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -29,7 +29,7 @@ fn parse_simple_column_path(lite_arg: &Spanned) -> (SpannedExpression, O if c == delimiter { inside_delimiter = false; } - } else if c == '\'' || c == '"' { + } else if c == '\'' || c == '"' || c == '`' { inside_delimiter = true; delimiter = c; } else if c == '.' { @@ -228,6 +228,7 @@ fn trim_quotes(input: &str) -> String { match (chars.next(), chars.next_back()) { (Some('\''), Some('\'')) => chars.collect(), (Some('"'), Some('"')) => chars.collect(), + (Some('`'), Some('`')) => chars.collect(), _ => input.to_string(), } } @@ -352,6 +353,165 @@ fn parse_unit(lite_arg: &Spanned) -> (SpannedExpression, Option), + Column(Spanned), +} + +fn format(input: &str, start: usize) -> (Vec, Option) { + let original_start = start; + let mut output = vec![]; + let mut error = None; + + let mut loop_input = input.chars().peekable(); + let mut start = start; + let mut end = start; + loop { + let mut before = String::new(); + + let mut found_start = false; + while let Some(c) = loop_input.next() { + end += 1; + if c == '{' { + if let Some(x) = loop_input.peek() { + if *x == '{' { + found_start = true; + end += 1; + let _ = loop_input.next(); + break; + } + } + } + before.push(c); + } + + if !before.is_empty() { + if found_start { + output.push(FormatCommand::Text( + before.to_string().spanned(Span::new(start, end - 2)), + )); + } else { + output.push(FormatCommand::Text(before.spanned(Span::new(start, end)))); + break; + } + } + // Look for column as we're now at one + let mut column = String::new(); + start = end; + + let mut previous_c = ' '; + let mut found_end = false; + while let Some(c) = loop_input.next() { + end += 1; + if c == '}' && previous_c == '}' { + let _ = column.pop(); + found_end = true; + break; + } + previous_c = c; + column.push(c); + } + + if !column.is_empty() { + if found_end { + output.push(FormatCommand::Column( + column.to_string().spanned(Span::new(start, end - 2)), + )); + } else { + output.push(FormatCommand::Column( + column.to_string().spanned(Span::new(start, end)), + )); + + if error.is_none() { + error = Some(ParseError::argument_error( + input.spanned(Span::new(original_start, end)), + ArgumentError::MissingValueForName("unclosed {{ }}".to_string()), + )); + } + } + } + + if found_start && !found_end { + error = Some(ParseError::argument_error( + input.spanned(Span::new(original_start, end)), + ArgumentError::MissingValueForName("unclosed {{ }}".to_string()), + )); + } + + if before.is_empty() && column.is_empty() { + break; + } + + start = end; + } + + (output, error) +} + +/// Parses an interpolated string, one that has expressions inside of it +fn parse_interpolated_string( + registry: &dyn SignatureRegistry, + lite_arg: &Spanned, +) -> (SpannedExpression, Option) { + let inner_string = trim_quotes(&lite_arg.item); + let mut error = None; + + let (format_result, err) = format(&inner_string, lite_arg.span.start() + 1); + + if error.is_none() { + error = err; + } + + let mut output = vec![]; + + for f in format_result { + match f { + FormatCommand::Text(t) => { + output.push(SpannedExpression { + expr: Expression::Literal(hir::Literal::String(t.item)), + span: t.span, + }); + } + FormatCommand::Column(c) => { + let (o, err) = parse_full_column_path(&c, registry); + if error.is_none() { + error = err; + } + output.push(o); + } + } + } + + let block = vec![Commands { + span: lite_arg.span, + list: vec![ClassifiedCommand::Internal(InternalCommand { + name: "build-string".to_owned(), + name_span: lite_arg.span, + args: hir::Call { + head: Box::new(SpannedExpression { + expr: Expression::Synthetic(hir::Synthetic::String("build-string".to_owned())), + span: lite_arg.span, + }), + is_last: false, + named: None, + positional: Some(output), + span: lite_arg.span, + }, + })], + }]; + + let call = SpannedExpression { + expr: Expression::Invocation(Block { + block, + span: lite_arg.span, + }), + span: lite_arg.span, + }; + + (call, error) +} + /// Parses the given argument using the shape as a guide for how to correctly parse the argument fn parse_arg( expected_type: SyntaxShape, @@ -395,11 +555,19 @@ fn parse_arg( } } SyntaxShape::String => { - let trimmed = trim_quotes(&lite_arg.item); - ( - SpannedExpression::new(Expression::string(trimmed), lite_arg.span), - None, - ) + if lite_arg.item.starts_with('`') + && lite_arg.item.len() > 1 + && lite_arg.item.ends_with('`') + { + // This is an interpolated string + parse_interpolated_string(registry, &lite_arg) + } else { + let trimmed = trim_quotes(&lite_arg.item); + ( + SpannedExpression::new(Expression::string(trimmed), lite_arg.span), + None, + ) + } } SyntaxShape::Pattern => { let trimmed = trim_quotes(&lite_arg.item); diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index 884467cee8..01d576823d 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -100,6 +100,66 @@ fn invocation_handles_dot() { }) } +#[test] +fn string_interpolation_with_it() { + let actual = nu!( + cwd: ".", + r#" + echo "foo" | echo `{{$it}}` + "# + ); + + assert_eq!(actual.out, "foo"); +} + +#[test] +fn string_interpolation_with_column() { + let actual = nu!( + cwd: ".", + r#" + echo '{"name": "bob"}' | from json | echo `{{name}} is cool` + "# + ); + + assert_eq!(actual.out, "bob is cool"); +} + +#[test] +fn string_interpolation_with_column2() { + let actual = nu!( + cwd: ".", + r#" + echo '{"name": "fred"}' | from json | echo `also {{name}} is cool` + "# + ); + + assert_eq!(actual.out, "also fred is cool"); +} + +#[test] +fn string_interpolation_with_column3() { + let actual = nu!( + cwd: ".", + r#" + echo '{"name": "sally"}' | from json | echo `also {{name}}` + "# + ); + + assert_eq!(actual.out, "also sally"); +} + +#[test] +fn string_interpolation_with_it_column_path() { + let actual = nu!( + cwd: ".", + r#" + echo '{"name": "sammie"}' | from json | echo `{{$it.name}}` + "# + ); + + assert_eq!(actual.out, "sammie"); +} + #[test] fn argument_invocation_reports_errors() { let actual = nu!(