From b16b3c0b7f771627adcaa069b3d0bad6776a2e81 Mon Sep 17 00:00:00 2001 From: Leon Date: Sat, 24 Dec 2022 04:49:19 +1000 Subject: [PATCH] Add `values` command (see #7166) (#7583) --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/example_test.rs | 7 +- crates/nu-command/src/filters/columns.rs | 6 +- crates/nu-command/src/filters/each.rs | 19 +- crates/nu-command/src/filters/mod.rs | 2 + crates/nu-command/src/filters/transpose.rs | 4 +- crates/nu-command/src/filters/values.rs | 213 +++++++++++++++++++ crates/nu-command/src/platform/ansi/ansi_.rs | 2 +- crates/nu-command/src/viewers/table.rs | 2 +- crates/nu-engine/src/column.rs | 2 +- src/tests/test_engine.rs | 2 +- 11 files changed, 246 insertions(+), 14 deletions(-) create mode 100644 crates/nu-command/src/filters/values.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ec41319fe0..39f5c79ca9 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -138,6 +138,7 @@ pub fn create_default_context() -> EngineState { Upsert, Update, UpdateCells, + Values, Where, Window, Wrap, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 24dd831ab9..7e2e5612d8 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -9,8 +9,9 @@ pub fn test_examples(cmd: impl Command + 'static) { #[cfg(test)] mod test_examples { use super::super::{ - Ansi, Date, Echo, From, If, Into, Let, LetEnv, Math, MathEuler, MathPi, MathRound, Path, - Random, Split, SplitColumn, SplitRow, Str, StrJoin, StrLength, StrReplace, Url, Wrap, + Ansi, Date, Echo, From, If, Into, IntoString, Let, LetEnv, Math, MathEuler, MathPi, + MathRound, Path, Random, Split, SplitColumn, SplitRow, Str, StrJoin, StrLength, StrReplace, + Url, Values, Wrap, }; use crate::{Break, Mut, To}; use itertools::Itertools; @@ -69,6 +70,7 @@ mod test_examples { working_set.add_decl(Box::new(If)); working_set.add_decl(Box::new(To)); working_set.add_decl(Box::new(Into)); + working_set.add_decl(Box::new(IntoString)); working_set.add_decl(Box::new(Random)); working_set.add_decl(Box::new(Split)); working_set.add_decl(Box::new(SplitColumn)); @@ -77,6 +79,7 @@ mod test_examples { working_set.add_decl(Box::new(Path)); working_set.add_decl(Box::new(Date)); working_set.add_decl(Box::new(Url)); + working_set.add_decl(Box::new(Values)); working_set.add_decl(Box::new(Ansi)); working_set.add_decl(Box::new(Wrap)); working_set.add_decl(Box::new(LetEnv)); diff --git a/crates/nu-command/src/filters/columns.rs b/crates/nu-command/src/filters/columns.rs index e26df20d0a..2349ab9683 100644 --- a/crates/nu-command/src/filters/columns.rs +++ b/crates/nu-command/src/filters/columns.rs @@ -24,7 +24,11 @@ impl Command for Columns { } fn usage(&self) -> &str { - "Show the columns in the input." + "Given a record or table, produce a list of its columns' names." + } + + fn extra_usage(&self) -> &str { + "This is a counterpart to `values`, which produces a list of columns' values." } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index 5b123a6256..8f46fe2cd5 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -73,20 +73,29 @@ with 'transpose' first."# vec![ Example { example: "[1 2 3] | each {|e| 2 * $e }", - description: "Multiplies elements in list", + description: "Multiplies elements in the list", result: Some(Value::List { vals: stream_test_1, span: Span::test_data(), }), }, + Example { + example: "{major:2, minor:1, patch:4} | values | each { into string }", + description: "Produce a list of values in the record, converted to string", + result: Some(Value::List { + vals: vec![ + Value::test_string("2"), + Value::test_string("1"), + Value::test_string("4"), + ], + span: Span::test_data(), + }), + }, Example { example: r#"[1 2 3 2] | each {|e| if $e == 2 { "two" } }"#, description: "Produce a list that has \"two\" for each 2 in the input", result: Some(Value::List { - vals: vec![ - Value::string("two", Span::test_data()), - Value::string("two", Span::test_data()), - ], + vals: vec![Value::test_string("two"), Value::test_string("two")], span: Span::test_data(), }), }, diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 177c3bc8b4..9a0f268e8b 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -47,6 +47,7 @@ mod update; mod update_cells; mod upsert; mod utils; +mod values; mod where_; mod window; mod wrap; @@ -100,6 +101,7 @@ pub use uniq_by::UniqBy; pub use update::Update; pub use update_cells::UpdateCells; pub use upsert::Upsert; +pub use values::Values; pub use where_::Where; pub use window::Window; pub use wrap::Wrap; diff --git a/crates/nu-command/src/filters/transpose.rs b/crates/nu-command/src/filters/transpose.rs index 2ec7166fa6..bd7104c7ea 100644 --- a/crates/nu-command/src/filters/transpose.rs +++ b/crates/nu-command/src/filters/transpose.rs @@ -274,7 +274,7 @@ pub fn transpose( } v => Value::List { vals: vec![v.clone(), x.clone()], - span: v.span().expect("this should be a valid span"), + span: v.expect_span(), }, }; cols.remove(index); @@ -313,7 +313,7 @@ pub fn transpose( } v => Value::List { vals: vec![v.clone(), Value::nothing(name)], - span: v.span().expect("this should be a valid span"), + span: v.expect_span(), }, }; cols.remove(index); diff --git a/crates/nu-command/src/filters/values.rs b/crates/nu-command/src/filters/values.rs new file mode 100644 index 0000000000..70984d56fe --- /dev/null +++ b/crates/nu-command/src/filters/values.rs @@ -0,0 +1,213 @@ +use indexmap::IndexMap; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + Type, Value, +}; + +#[derive(Clone)] +pub struct Values; + +impl Command for Values { + fn name(&self) -> &str { + "values" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![ + (Type::Record(vec![]), Type::List(Box::new(Type::Any))), + (Type::Table(vec![]), Type::List(Box::new(Type::Any))), + ]) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Given a record or table, produce a list of its columns' values." + } + + fn extra_usage(&self) -> &str { + "This is a counterpart to `columns`, which produces a list of columns' names." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "{ mode:normal userid:31415 } | values", + description: "Get the values from the record (produce a list)", + result: Some(Value::List { + vals: vec![Value::test_string("normal"), Value::test_int(31415)], + span: Span::test_data(), + }), + }, + Example { + example: "{ f:250 g:191 c:128 d:1024 e:2000 a:16 b:32 } | values", + description: "Values are ordered by the column order of the record", + result: Some(Value::List { + vals: vec![ + Value::test_int(250), + Value::test_int(191), + Value::test_int(128), + Value::test_int(1024), + Value::test_int(2000), + Value::test_int(16), + Value::test_int(32), + ], + span: Span::test_data(), + }), + }, + Example { + example: "[[name meaning]; [ls list] [mv move] [cd 'change directory']] | values", + description: "Get the values from the table (produce a list of lists)", + result: Some(Value::List { + vals: vec![ + Value::List { + vals: vec![ + Value::test_string("ls"), + Value::test_string("mv"), + Value::test_string("cd"), + ], + span: Span::test_data(), + }, + Value::List { + vals: vec![ + Value::test_string("list"), + Value::test_string("move"), + Value::test_string("change directory"), + ], + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + values(engine_state, span, input) + } +} + +// The semantics of `values` are as follows: +// For each column, get the values for that column, in row order. +// Holes are not preserved, i.e. position in the resulting list +// does not necessarily equal row number. +pub fn get_values<'a>( + input: impl IntoIterator, + head: Span, + input_span: Span, +) -> Result, ShellError> { + let mut output: IndexMap> = IndexMap::new(); + + for item in input { + match item { + Value::Record { cols, vals, .. } => { + for (k, v) in cols.iter().zip(vals.iter()) { + if let Some(vec) = output.get_mut(k) { + vec.push(v.clone()); + } else { + output.insert(k.clone(), vec![v.clone()]); + } + } + } + Value::Error { error } => return Err(error.clone()), + _ => { + return Err(ShellError::OnlySupportsThisInputType( + "record or table".into(), + item.get_type().to_string(), + head, + input_span, + )) + } + } + } + + Ok(output.into_values().map(|v| Value::list(v, head)).collect()) +} + +fn values( + engine_state: &EngineState, + head: Span, + input: PipelineData, +) -> Result { + let ctrlc = engine_state.ctrlc.clone(); + let metadata = input.metadata(); + match input { + PipelineData::Empty => Ok(PipelineData::Empty), + PipelineData::Value(Value::List { vals, span }, ..) => { + match get_values(&vals, head, span) { + Ok(cols) => Ok(cols + .into_iter() + .into_pipeline_data(ctrlc) + .set_metadata(metadata)), + Err(err) => Err(err), + } + } + PipelineData::Value(Value::CustomValue { val, span }, ..) => { + let input_as_base_value = val.to_base_value(span)?; + match get_values(&[input_as_base_value], head, span) { + Ok(cols) => Ok(cols + .into_iter() + .into_pipeline_data(ctrlc) + .set_metadata(metadata)), + Err(err) => Err(err), + } + } + PipelineData::ListStream(stream, ..) => { + let vals: Vec<_> = stream.into_iter().collect(); + match get_values(&vals, head, head) { + Ok(cols) => Ok(cols + .into_iter() + .into_pipeline_data(ctrlc) + .set_metadata(metadata)), + Err(err) => Err(err), + } + } + PipelineData::Value(Value::Record { vals, .. }, ..) => { + Ok(vals.into_pipeline_data(ctrlc).set_metadata(metadata)) + } + // Propagate errors + PipelineData::Value(Value::Error { error: _ }, ..) => Ok(input), + PipelineData::Value(other, ..) => { + Err(ShellError::OnlySupportsThisInputType( + "record or table".into(), + other.get_type().to_string(), + head, + // This line requires the Value::Error match above. + other.expect_span(), + )) + } + PipelineData::ExternalStream { .. } => { + Err(ShellError::OnlySupportsThisInputType( + "record or table".into(), + "raw data".into(), + head, + // This line requires the PipelineData::Empty and PipelineData::ListStream matches above. + input + .span() + .expect("PipelineData::ExternalStream had no span"), + )) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Values {}) + } +} diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index dddea11398..796ef26da3 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -723,7 +723,7 @@ Format: # _ => { return Err(ShellError::IncompatibleParametersSingle( format!("problem with key: {}", k), - code.span().expect("error with span"), + code.expect_span(), )) } } diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index 853e0554e4..e1c7b8d30d 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -654,7 +654,7 @@ fn handle_row_stream( ctrlc, ) } - // Next, `into html -l` sources: + // Next, `to html -l` sources: Some(PipelineMetadata { data_source: DataSource::HtmlThemes, }) => { diff --git a/crates/nu-engine/src/column.rs b/crates/nu-engine/src/column.rs index c77043a1d8..4f4336fc14 100644 --- a/crates/nu-engine/src/column.rs +++ b/crates/nu-engine/src/column.rs @@ -5,7 +5,7 @@ pub fn get_columns<'a>(input: impl IntoIterator) -> Vec TestResult { #[test] fn help_works_with_missing_requirements() -> TestResult { - run_test(r#"each --help | lines | length"#, "40") + run_test(r#"each --help | lines | length"#, "43") } #[test]