mirror of
https://github.com/nushell/nushell.git
synced 2024-11-26 02:13:47 +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(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),
|
||||||
|
@ -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;
|
||||||
|
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 {
|
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)?;
|
||||||
|
@ -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,12 +555,20 @@ fn parse_arg(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyntaxShape::String => {
|
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);
|
let trimmed = trim_quotes(&lite_arg.item);
|
||||||
(
|
(
|
||||||
SpannedExpression::new(Expression::string(trimmed), lite_arg.span),
|
SpannedExpression::new(Expression::string(trimmed), lite_arg.span),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
SyntaxShape::Pattern => {
|
SyntaxShape::Pattern => {
|
||||||
let trimmed = trim_quotes(&lite_arg.item);
|
let trimmed = trim_quotes(&lite_arg.item);
|
||||||
let expanded = expand_path(&trimmed).to_string();
|
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]
|
#[test]
|
||||||
fn argument_invocation_reports_errors() {
|
fn argument_invocation_reports_errors() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
|
Loading…
Reference in New Issue
Block a user