String interpolation (#1849)

* Add string interpolation

* fix coloring

* A few more fixups + tests

* merge master again
This commit is contained in:
Jonathan Turner 2020-05-19 12:27:26 -07:00 committed by GitHub
parent ae87582cb6
commit ed80933806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 303 additions and 13 deletions

View File

@ -284,6 +284,7 @@ pub fn create_default_context(
whole_stream_command(Lines), whole_stream_command(Lines),
whole_stream_command(Trim), whole_stream_command(Trim),
whole_stream_command(Echo), whole_stream_command(Echo),
whole_stream_command(BuildString),
// Column manipulation // Column manipulation
whole_stream_command(Reject), whole_stream_command(Reject),
whole_stream_command(Select), whole_stream_command(Select),

View File

@ -8,6 +8,7 @@ pub(crate) mod alias;
pub(crate) mod append; pub(crate) mod append;
pub(crate) mod args; pub(crate) mod args;
pub(crate) mod autoview; pub(crate) mod autoview;
pub(crate) mod build_string;
pub(crate) mod cal; pub(crate) mod cal;
pub(crate) mod calc; pub(crate) mod calc;
pub(crate) mod cd; pub(crate) mod cd;
@ -132,6 +133,7 @@ pub(crate) use command::{
pub(crate) use alias::Alias; pub(crate) use alias::Alias;
pub(crate) use append::Append; pub(crate) use append::Append;
pub(crate) use build_string::BuildString;
pub(crate) use cal::Cal; pub(crate) use cal::Cal;
pub(crate) use calc::Calc; pub(crate) use calc::Calc;
pub(crate) use compact::Compact; pub(crate) use compact::Compact;

View File

@ -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<Value>,
}
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<OutputStream, ShellError> {
build_string(args, registry)
}
fn examples(&self) -> Vec<Example> {
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<OutputStream, ShellError> {
let registry = registry.clone();
let tag = args.call_info.name_tag.clone();
let stream = async_stream! {
let (BuildStringArgs { rest }, mut input) = args.process(&registry).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())
}

View File

@ -69,7 +69,7 @@ fn bare(src: &mut Input, span_offset: usize) -> Result<Spanned<String>, ParseErr
if c == delimiter { if c == delimiter {
inside_quote = false; inside_quote = false;
} }
} else if c == '\'' || c == '"' { } else if c == '\'' || c == '"' || c == '`' {
inside_quote = true; inside_quote = true;
delimiter = c; delimiter = c;
} else if c == '[' { } else if c == '[' {
@ -154,12 +154,6 @@ fn pipeline(src: &mut Input, span_offset: usize) -> Result<LiteBlock, ParseError
break; break;
} }
} }
// '"' | '\'' => {
// let c = *c;
// // quoted string
// let arg = quoted(src, c, span_offset)?;
// cmd.args.push(arg);
// }
_ => { _ => {
// basic argument // basic argument
let arg = bare(src, span_offset)?; let arg = bare(src, span_offset)?;

View File

@ -29,7 +29,7 @@ fn parse_simple_column_path(lite_arg: &Spanned<String>) -> (SpannedExpression, O
if c == delimiter { if c == delimiter {
inside_delimiter = false; inside_delimiter = false;
} }
} else if c == '\'' || c == '"' { } else if c == '\'' || c == '"' || c == '`' {
inside_delimiter = true; inside_delimiter = true;
delimiter = c; delimiter = c;
} else if c == '.' { } else if c == '.' {
@ -228,6 +228,7 @@ fn trim_quotes(input: &str) -> String {
match (chars.next(), chars.next_back()) { match (chars.next(), chars.next_back()) {
(Some('\''), Some('\'')) => chars.collect(), (Some('\''), Some('\'')) => chars.collect(),
(Some('"'), Some('"')) => chars.collect(), (Some('"'), Some('"')) => chars.collect(),
(Some('`'), Some('`')) => chars.collect(),
_ => input.to_string(), _ => input.to_string(),
} }
} }
@ -352,6 +353,165 @@ fn parse_unit(lite_arg: &Spanned<String>) -> (SpannedExpression, Option<ParseErr
) )
} }
#[derive(Debug)]
enum FormatCommand {
Text(Spanned<String>),
Column(Spanned<String>),
}
fn format(input: &str, start: usize) -> (Vec<FormatCommand>, Option<ParseError>) {
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<String>,
) -> (SpannedExpression, Option<ParseError>) {
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 /// Parses the given argument using the shape as a guide for how to correctly parse the argument
fn parse_arg( fn parse_arg(
expected_type: SyntaxShape, expected_type: SyntaxShape,
@ -395,11 +555,19 @@ fn parse_arg(
} }
} }
SyntaxShape::String => { SyntaxShape::String => {
let trimmed = trim_quotes(&lite_arg.item); if lite_arg.item.starts_with('`')
( && lite_arg.item.len() > 1
SpannedExpression::new(Expression::string(trimmed), lite_arg.span), && lite_arg.item.ends_with('`')
None, {
) // 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 => { SyntaxShape::Pattern => {
let trimmed = trim_quotes(&lite_arg.item); let trimmed = trim_quotes(&lite_arg.item);

View File

@ -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] #[test]
fn argument_invocation_reports_errors() { fn argument_invocation_reports_errors() {
let actual = nu!( let actual = nu!(