diff --git a/crates/nu-cli/src/completion/engine.rs b/crates/nu-cli/src/completion/engine.rs index e2f5cee8d1..169efc3b59 100644 --- a/crates/nu-cli/src/completion/engine.rs +++ b/crates/nu-cli/src/completion/engine.rs @@ -27,6 +27,15 @@ impl<'s> Flatten<'s> { Expression::Block(block) => self.completion_locations(block), Expression::Invocation(block) => self.completion_locations(block), Expression::List(exprs) => exprs.iter().flat_map(|v| self.expression(v)).collect(), + Expression::Table(headers, cells) => headers + .iter() + .flat_map(|v| self.expression(v)) + .chain( + cells + .iter() + .flat_map(|v| v.iter().flat_map(|v| self.expression(v))), + ) + .collect(), Expression::Command => vec![LocationType::Command.spanned(e.span)], Expression::Path(path) => self.expression(&path.head), Expression::Variable(_) => vec![LocationType::Variable.spanned(e.span)], diff --git a/crates/nu-cli/src/evaluate/evaluator.rs b/crates/nu-cli/src/evaluate/evaluator.rs index 480578d0e2..78cf21a4fd 100644 --- a/crates/nu-cli/src/evaluate/evaluator.rs +++ b/crates/nu-cli/src/evaluate/evaluator.rs @@ -80,6 +80,46 @@ pub(crate) async fn evaluate_baseline_expr( Ok(UntaggedValue::range(left, right).into_value(tag)) } + Expression::Table(headers, cells) => { + let mut output_headers = vec![]; + + for expr in headers { + let val = evaluate_baseline_expr(&expr, registry, it, vars, env).await?; + + let header = val.as_string()?; + output_headers.push(header); + } + + let mut output_table = vec![]; + + for row in cells { + if row.len() != headers.len() { + match (row.first(), row.last()) { + (Some(first), Some(last)) => { + return Err(ShellError::labeled_error( + "Cell count doesn't match header count", + format!("expected {} columns", headers.len()), + Span::new(first.span.start(), last.span.end()), + )); + } + _ => { + return Err(ShellError::untagged_runtime_error( + "Cell count doesn't match header count", + )); + } + } + } + + let mut row_output = IndexMap::new(); + for cell in output_headers.iter().zip(row.iter()) { + let val = evaluate_baseline_expr(&cell.1, registry, it, vars, env).await?; + row_output.insert(cell.0.clone(), val); + } + output_table.push(UntaggedValue::row(row_output).into_value(tag.clone())); + } + + Ok(UntaggedValue::Table(output_table).into_value(tag)) + } Expression::List(list) => { let mut exprs = vec![]; diff --git a/crates/nu-parser/src/parse.rs b/crates/nu-parser/src/parse.rs index b9004e5552..dfff19fc9f 100644 --- a/crates/nu-parser/src/parse.rs +++ b/crates/nu-parser/src/parse.rs @@ -537,6 +537,129 @@ fn parse_external_arg( } } +fn parse_list( + lite_block: &LiteBlock, + registry: &dyn SignatureRegistry, +) -> (Vec, Option) { + let mut error = None; + + if lite_block.block.is_empty() { + return (vec![], None); + } + let lite_pipeline = &lite_block.block[0]; + let mut output = vec![]; + for lite_inner in &lite_pipeline.commands { + let (arg, err) = parse_arg(SyntaxShape::Any, registry, &lite_inner.name); + + output.push(arg); + if error.is_none() { + error = err; + } + + for arg in &lite_inner.args { + let (arg, err) = parse_arg(SyntaxShape::Any, registry, &arg); + output.push(arg); + + if error.is_none() { + error = err; + } + } + } + + (output, error) +} + +fn verify_and_strip( + contents: &Spanned, + left: char, + right: char, +) -> (String, Option) { + let mut chars = contents.item.chars(); + + match (chars.next(), chars.next_back()) { + (Some(l), Some(r)) if l == left && r == right => { + let output: String = chars.collect(); + (output, None) + } + _ => ( + String::new(), + Some(ParseError::mismatch( + format!("value in {} {}", left, right), + contents.clone(), + )), + ), + } +} + +fn parse_table( + lite_block: &LiteBlock, + registry: &dyn SignatureRegistry, + span: Span, +) -> (SpannedExpression, Option) { + let mut error = None; + let mut output = vec![]; + + // Header + let lite_pipeline = &lite_block.block[0]; + let lite_inner = &lite_pipeline.commands[0]; + + let (string, err) = verify_and_strip(&lite_inner.name, '[', ']'); + if error.is_none() { + error = err; + } + + let lite_header = match lite_parse(&string, lite_inner.name.span.start() + 1) { + Ok(lb) => lb, + Err(e) => return (garbage(lite_inner.name.span), Some(e.cause)), + }; + + let (headers, err) = parse_list(&lite_header, registry); + if error.is_none() { + error = err; + } + + // Cells + let lite_rows = &lite_block.block[1]; + let lite_cells = &lite_rows.commands[0]; + + let (string, err) = verify_and_strip(&lite_cells.name, '[', ']'); + if error.is_none() { + error = err; + } + + let lite_cell = match lite_parse(&string, lite_cells.name.span.start() + 1) { + Ok(lb) => lb, + Err(e) => return (garbage(lite_cells.name.span), Some(e.cause)), + }; + + let (inner_cell, err) = parse_list(&lite_cell, registry); + if error.is_none() { + error = err; + } + output.push(inner_cell); + + for arg in &lite_cells.args { + let (string, err) = verify_and_strip(&arg, '[', ']'); + if error.is_none() { + error = err; + } + let lite_cell = match lite_parse(&string, arg.span.start() + 1) { + Ok(lb) => lb, + Err(e) => return (garbage(arg.span), Some(e.cause)), + }; + let (inner_cell, err) = parse_list(&lite_cell, registry); + if error.is_none() { + error = err; + } + output.push(inner_cell); + } + + ( + SpannedExpression::new(Expression::Table(headers, output), span), + error, + ) +} + /// Parses the given argument using the shape as a guide for how to correctly parse the argument fn parse_arg( expected_type: SyntaxShape, @@ -644,7 +767,6 @@ fn parse_arg( (Some('['), Some(']')) => { // We have a literal row let string: String = chars.collect(); - let mut error = None; // We haven't done much with the inner string, so let's go ahead and work with it let lite_block = match lite_parse(&string, lite_arg.span.start() + 1) { @@ -655,40 +777,26 @@ fn parse_arg( if lite_block.block.is_empty() { return ( SpannedExpression::new(Expression::List(vec![]), lite_arg.span), - error, + None, ); } - if lite_block.block.len() > 1 { - return ( + if lite_block.block.len() == 1 { + let (items, err) = parse_list(&lite_block, registry); + ( + SpannedExpression::new(Expression::List(items), lite_arg.span), + err, + ) + } else if lite_block.block.len() == 2 { + parse_table(&lite_block, registry, lite_arg.span) + } else { + ( garbage(lite_arg.span), - Some(ParseError::mismatch("table", lite_arg.clone())), - ); + Some(ParseError::mismatch( + "list or table", + "unknown".to_string().spanned(lite_arg.span), + )), + ) } - - let lite_pipeline = lite_block.block[0].clone(); - let mut output = vec![]; - for lite_inner in &lite_pipeline.commands { - let (arg, err) = parse_arg(SyntaxShape::Any, registry, &lite_inner.name); - - output.push(arg); - if error.is_none() { - error = err; - } - - for arg in &lite_inner.args { - let (arg, err) = parse_arg(SyntaxShape::Any, registry, &arg); - output.push(arg); - - if error.is_none() { - error = err; - } - } - } - - ( - SpannedExpression::new(Expression::List(output), lite_arg.span), - error, - ) } _ => ( garbage(lite_arg.span), diff --git a/crates/nu-parser/src/shapes.rs b/crates/nu-parser/src/shapes.rs index cdd1031f2c..d54916f0f8 100644 --- a/crates/nu-parser/src/shapes.rs +++ b/crates/nu-parser/src/shapes.rs @@ -16,6 +16,18 @@ pub fn expression_to_flat_shape(e: &SpannedExpression) -> Vec } output } + Expression::Table(headers, cells) => { + let mut output = vec![]; + for header in headers.iter() { + output.append(&mut expression_to_flat_shape(header)); + } + for row in cells { + for cell in row { + output.append(&mut expression_to_flat_shape(&cell)); + } + } + output + } Expression::Path(exprs) => { let mut output = vec![]; output.append(&mut expression_to_flat_shape(&exprs.head)); diff --git a/crates/nu-protocol/src/hir.rs b/crates/nu-protocol/src/hir.rs index 68ba9635ef..69055e7f74 100644 --- a/crates/nu-protocol/src/hir.rs +++ b/crates/nu-protocol/src/hir.rs @@ -716,6 +716,20 @@ impl PrettyDebugWithSource for SpannedExpression { ), "]", ), + Expression::Table(_headers, cells) => b::delimit( + "[", + b::intersperse( + cells + .iter() + .map(|row| { + row.iter() + .map(|item| item.refined_pretty_debug(refine, source)) + }) + .flatten(), + b::space(), + ), + "]", + ), Expression::Path(path) => path.pretty_debug(source), Expression::FilePath(path) => b::typed("path", b::primitive(path.display())), Expression::ExternalCommand(external) => { @@ -756,6 +770,17 @@ impl PrettyDebugWithSource for SpannedExpression { ), "]", ), + Expression::Table(_headers, cells) => b::delimit( + "[", + b::intersperse( + cells + .iter() + .map(|row| row.iter().map(|item| item.pretty_debug(source))) + .flatten(), + b::space(), + ), + "]", + ), Expression::Path(path) => path.pretty_debug(source), Expression::FilePath(path) => b::typed("path", b::primitive(path.display())), Expression::ExternalCommand(external) => b::typed( @@ -960,6 +985,7 @@ pub enum Expression { Range(Box), Block(hir::Block), List(Vec), + Table(Vec, Vec>), Path(Box), FilePath(PathBuf), @@ -985,6 +1011,7 @@ impl ShellTypeName for Expression { Expression::FilePath(..) => "file path", Expression::Variable(..) => "variable", Expression::List(..) => "list", + Expression::Table(..) => "table", Expression::Binary(..) => "binary", Expression::Range(..) => "range", Expression::Block(..) => "block", diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index ce1c0c0178..1c669d4466 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -418,6 +418,30 @@ fn echoing_ranges() { assert_eq!(actual.out, "6"); } +#[test] +fn table_literals1() { + let actual = nu!( + cwd: ".", + r#" + echo [[name age]; [foo 13]] | get age + "# + ); + + assert_eq!(actual.out, "13"); +} + +#[test] +fn table_literals2() { + let actual = nu!( + cwd: ".", + r#" + echo [[name age] ; [bob 13] [sally 20]] | get age | math sum + "# + ); + + assert_eq!(actual.out, "33"); +} + mod parse { use nu_test_support::nu;