From 52cb50b93722e98d23d3be5d6b412e3167afceac Mon Sep 17 00:00:00 2001 From: Antonio Natilla Date: Tue, 2 Nov 2021 18:13:06 +0100 Subject: [PATCH 1/3] Base Command implementation for Format Note that run is not implemented yet --- crates/nu-command/src/default_context.rs | 1 + .../nu-command/src/strings/format/format.rs | 53 +++++++++++++++++++ crates/nu-command/src/strings/format/mod.rs | 3 ++ crates/nu-command/src/strings/mod.rs | 2 + 4 files changed, 59 insertions(+) create mode 100644 crates/nu-command/src/strings/format/format.rs create mode 100644 crates/nu-command/src/strings/format/mod.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 3d41f273..754693c2 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -42,6 +42,7 @@ pub fn create_default_context() -> EngineState { External, First, For, + Format, From, FromJson, Get, diff --git a/crates/nu-command/src/strings/format/format.rs b/crates/nu-command/src/strings/format/format.rs new file mode 100644 index 00000000..5f64661a --- /dev/null +++ b/crates/nu-command/src/strings/format/format.rs @@ -0,0 +1,53 @@ +//use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, SyntaxShape}; + +#[derive(Clone)] +pub struct Format; + +impl Command for Format { + fn name(&self) -> &str { + "format" + } + + fn signature(&self) -> Signature { + Signature::build("format").required( + "pattern", + SyntaxShape::String, + "the pattern to output. e.g.) \"{foo}: {bar}\"", + ) + } + + fn usage(&self) -> &str { + "Format columns into a string using a simple pattern." + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + todo!() + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Print filenames with their sizes", + example: "ls | format '{name}: {size}'", + result: None, + }] + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Format; + use crate::test_examples; + test_examples(Format {}) + } +} diff --git a/crates/nu-command/src/strings/format/mod.rs b/crates/nu-command/src/strings/format/mod.rs new file mode 100644 index 00000000..c20d4163 --- /dev/null +++ b/crates/nu-command/src/strings/format/mod.rs @@ -0,0 +1,3 @@ +pub mod format; + +pub use format::Format; diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index bbb78a24..a23eba5f 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -1,7 +1,9 @@ mod build_string; +mod format; mod size; mod split; pub use build_string::BuildString; +pub use format::*; pub use size::Size; pub use split::*; From 806cd4851fe926d0f97dae53065f02b33d350e54 Mon Sep 17 00:00:00 2001 From: Antonio Natilla Date: Wed, 3 Nov 2021 19:46:33 +0100 Subject: [PATCH 2/3] Format implementation, fix on Echo Now, Echo converts multiple values in a ValueStream, but it simply forwards a single Value; if no PipelineData is detected as an input, an empty string is returned as a single Value. --- crates/nu-command/src/core_commands/echo.rs | 26 ++- crates/nu-command/src/example_test.rs | 2 + .../nu-command/src/strings/format/command.rs | 193 ++++++++++++++++++ .../nu-command/src/strings/format/format.rs | 53 ----- crates/nu-command/src/strings/format/mod.rs | 4 +- 5 files changed, 215 insertions(+), 63 deletions(-) create mode 100644 crates/nu-command/src/strings/format/command.rs delete mode 100644 crates/nu-command/src/strings/format/format.rs diff --git a/crates/nu-command/src/core_commands/echo.rs b/crates/nu-command/src/core_commands/echo.rs index af0c9948..064d52bd 100644 --- a/crates/nu-command/src/core_commands/echo.rs +++ b/crates/nu-command/src/core_commands/echo.rs @@ -29,10 +29,23 @@ impl Command for Echo { _input: PipelineData, ) -> Result { call.rest(engine_state, stack, 0).map(|to_be_echoed| { - PipelineData::Stream(ValueStream::from_stream( - to_be_echoed.into_iter(), - engine_state.ctrlc.clone(), - )) + let n = to_be_echoed.len(); + match n.cmp(&1usize) { + // More than one value is converted in a stream of values + std::cmp::Ordering::Greater => PipelineData::Stream(ValueStream::from_stream( + to_be_echoed.into_iter(), + engine_state.ctrlc.clone(), + )), + + // But a single value can be forwarded as it is + std::cmp::Ordering::Equal => PipelineData::Value(to_be_echoed[0].clone()), + + // When there are no elements, we echo the empty string + std::cmp::Ordering::Less => PipelineData::Value(Value::String { + val: "".to_string(), + span: Span::unknown(), + }), + } }) } @@ -41,10 +54,7 @@ impl Command for Echo { Example { description: "Put a hello message in the pipeline", example: "echo 'hello'", - result: Some(Value::List { - vals: vec![Value::test_string("hello")], - span: Span::new(0, 0), - }), + result: Some(Value::test_string("hello")), }, Example { description: "Print the value of the special '$nu' variable", diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index bc3c819a..635a5ea0 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -24,6 +24,8 @@ pub fn test_examples(cmd: impl Command + 'static) { working_set.add_decl(Box::new(Math)); working_set.add_decl(Box::new(Date)); + use super::Echo; + working_set.add_decl(Box::new(Echo)); // Adding the command that is being tested to the working set working_set.add_decl(Box::new(cmd)); diff --git a/crates/nu-command/src/strings/format/command.rs b/crates/nu-command/src/strings/format/command.rs new file mode 100644 index 00000000..81e3334e --- /dev/null +++ b/crates/nu-command/src/strings/format/command.rs @@ -0,0 +1,193 @@ +use nu_engine::CallExt; +use nu_protocol::ast::{Call, PathMember}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, ValueStream, +}; + +#[derive(Clone)] +pub struct Format; + +impl Command for Format { + fn name(&self) -> &str { + "format" + } + + fn signature(&self) -> Signature { + Signature::build("format").required( + "pattern", + SyntaxShape::String, + "the pattern to output. e.g.) \"{foo}: {bar}\"", + ) + } + + fn usage(&self) -> &str { + "Format columns into a string using a simple pattern." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let specified_pattern: Result = call.req(engine_state, stack, 0); + match specified_pattern { + Err(e) => Err(e), + Ok(pattern) => { + // Handle the pattern + let string_pattern = pattern.as_string().unwrap(); + let ops = extract_formatting_operations(string_pattern); + format(input, &ops) + } + } + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Print filenames with their sizes", + example: "ls | format '{name}: {size}'", + result: None, + }, + Example { + description: "Print elements from some columns of a table", + example: "echo [[col1, col2]; [v1, v2] [v3, v4]] | format '{col2}'", + result: Some(Value::List { + vals: vec![Value::test_string("v2"), Value::test_string("v4")], + span: Span::new(0, 0), + }), + }, + ] + } +} + +#[derive(Debug)] +enum FormatOperation { + FixedText(String), + ValueFromColumn(String), +} + +fn extract_formatting_operations(input: String) -> Vec { + let mut output = vec![]; + + let mut characters = input.chars(); + 'outer: loop { + let mut before_bracket = String::new(); + + for ch in &mut characters { + if ch == '{' { + break; + } + before_bracket.push(ch); + } + + if !before_bracket.is_empty() { + output.push(FormatOperation::FixedText(before_bracket.to_string())); + } + + let mut column_name = String::new(); + + for ch in &mut characters { + if ch == '}' { + break; + } + column_name.push(ch); + } + + if !column_name.is_empty() { + output.push(FormatOperation::ValueFromColumn(column_name.clone())); + } + + if before_bracket.is_empty() && column_name.is_empty() { + break 'outer; + } + } + output +} + +fn format( + input_data: PipelineData, + format_operations: &[FormatOperation], +) -> Result { + let data_as_value = input_data.into_value(); + eprintln!("{:#?}", data_as_value); + match data_as_value { + Value::Record { .. } => match format_record(format_operations, &data_as_value) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, Span::unknown()))), + Err(value) => Err(value), + }, + + Value::List { vals, .. } => { + let mut list = vec![]; + for val in vals.iter() { + match val { + Value::Record { .. } => match format_record(format_operations, val) { + Ok(value) => { + list.push(Value::string(value, Span::unknown())); + } + Err(value) => { + return Err(value); + } + }, + + _ => { + return Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + Span::unknown(), + )) + } + } + } + + Ok(PipelineData::Stream(ValueStream::from_stream( + list.into_iter(), + None, + ))) + } + _ => Err(ShellError::UnsupportedInput( + "Input data is not supported by this command.".to_string(), + Span::unknown(), + )), + } +} + +fn format_record( + format_operations: &[FormatOperation], + data_as_value: &Value, +) -> Result { + let mut output = String::new(); + for op in format_operations { + match op { + FormatOperation::FixedText(s) => output.push_str(s.as_str()), + + // The referenced code suggest to use the correct Span's + // See: https://github.com/nushell/nushell/blob/c4af5df828135159633d4bc3070ce800518a42a2/crates/nu-command/src/commands/strings/format/command.rs#L61 + FormatOperation::ValueFromColumn(col_name) => { + match data_as_value + .clone() + .follow_cell_path(&[PathMember::String { + val: col_name.clone(), + span: Span::unknown(), + }]) { + Ok(value_at_column) => { + output.push_str(value_at_column.as_string().unwrap().as_str()) + } + Err(se) => return Err(se), + } + } + } + } + Ok(output) +} + +#[cfg(test)] +mod test { + #[test] + fn test_examples() { + use super::Format; + use crate::test_examples; + test_examples(Format {}) + } +} diff --git a/crates/nu-command/src/strings/format/format.rs b/crates/nu-command/src/strings/format/format.rs deleted file mode 100644 index 5f64661a..00000000 --- a/crates/nu-command/src/strings/format/format.rs +++ /dev/null @@ -1,53 +0,0 @@ -//use nu_engine::CallExt; -use nu_protocol::ast::Call; -use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::{Example, PipelineData, ShellError, Signature, SyntaxShape}; - -#[derive(Clone)] -pub struct Format; - -impl Command for Format { - fn name(&self) -> &str { - "format" - } - - fn signature(&self) -> Signature { - Signature::build("format").required( - "pattern", - SyntaxShape::String, - "the pattern to output. e.g.) \"{foo}: {bar}\"", - ) - } - - fn usage(&self) -> &str { - "Format columns into a string using a simple pattern." - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - todo!() - } - - fn examples(&self) -> Vec { - vec![Example { - description: "Print filenames with their sizes", - example: "ls | format '{name}: {size}'", - result: None, - }] - } -} - -#[cfg(test)] -mod test { - #[test] - fn test_examples() { - use super::Format; - use crate::test_examples; - test_examples(Format {}) - } -} diff --git a/crates/nu-command/src/strings/format/mod.rs b/crates/nu-command/src/strings/format/mod.rs index c20d4163..71be06ce 100644 --- a/crates/nu-command/src/strings/format/mod.rs +++ b/crates/nu-command/src/strings/format/mod.rs @@ -1,3 +1,3 @@ -pub mod format; +pub mod command; -pub use format::Format; +pub use command::Format; From bfae75ca2e8e1baa14f0ce5aed2604dd335b31c2 Mon Sep 17 00:00:00 2001 From: Antonio Natilla Date: Wed, 3 Nov 2021 20:05:24 +0100 Subject: [PATCH 3/3] Clean-up and adding comments --- crates/nu-command/src/strings/format/command.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/nu-command/src/strings/format/command.rs b/crates/nu-command/src/strings/format/command.rs index 81e3334e..4c78e5b4 100644 --- a/crates/nu-command/src/strings/format/command.rs +++ b/crates/nu-command/src/strings/format/command.rs @@ -36,7 +36,6 @@ impl Command for Format { match specified_pattern { Err(e) => Err(e), Ok(pattern) => { - // Handle the pattern let string_pattern = pattern.as_string().unwrap(); let ops = extract_formatting_operations(string_pattern); format(input, &ops) @@ -69,6 +68,12 @@ enum FormatOperation { ValueFromColumn(String), } +/// Given a pattern that is fed into the Format command, we can process it and subdivide it +/// in two kind of operations. +/// FormatOperation::FixedText contains a portion of the patter that has to be placed +/// there without any further processing. +/// FormatOperation::ValueFromColumn contains the name of a column whose values will be +/// formatted according to the input pattern. fn extract_formatting_operations(input: String) -> Vec { let mut output = vec![]; @@ -107,12 +112,14 @@ fn extract_formatting_operations(input: String) -> Vec { output } +/// Format the incoming PipelineData according to the pattern fn format( input_data: PipelineData, format_operations: &[FormatOperation], ) -> Result { let data_as_value = input_data.into_value(); - eprintln!("{:#?}", data_as_value); + + // We can only handle a Record or a List of Record's match data_as_value { Value::Record { .. } => match format_record(format_operations, &data_as_value) { Ok(value) => Ok(PipelineData::Value(Value::string(value, Span::unknown()))),