mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01:00
String interpolation (#1849)
* Add string interpolation * fix coloring * A few more fixups + tests * merge master again
This commit is contained in:
parent
ae87582cb6
commit
ed80933806
@ -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),
|
||||
|
@ -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;
|
||||
|
65
crates/nu-cli/src/commands/build_string.rs
Normal file
65
crates/nu-cli/src/commands/build_string.rs
Normal 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(®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())
|
||||
}
|
@ -69,7 +69,7 @@ fn bare(src: &mut Input, span_offset: usize) -> Result<Spanned<String>, 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<LiteBlock, ParseError
|
||||
break;
|
||||
}
|
||||
}
|
||||
// '"' | '\'' => {
|
||||
// let c = *c;
|
||||
// // quoted string
|
||||
// let arg = quoted(src, c, span_offset)?;
|
||||
// cmd.args.push(arg);
|
||||
// }
|
||||
_ => {
|
||||
// basic argument
|
||||
let arg = bare(src, span_offset)?;
|
||||
|
@ -29,7 +29,7 @@ fn parse_simple_column_path(lite_arg: &Spanned<String>) -> (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<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
|
||||
fn parse_arg(
|
||||
expected_type: SyntaxShape,
|
||||
@ -395,12 +555,20 @@ fn parse_arg(
|
||||
}
|
||||
}
|
||||
SyntaxShape::String => {
|
||||
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);
|
||||
let expanded = expand_path(&trimmed).to_string();
|
||||
|
@ -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!(
|
||||
|
Loading…
Reference in New Issue
Block a user