diff --git a/crates/nu-command/src/core_commands/echo.rs b/crates/nu-command/src/core_commands/echo.rs index af0c994893..064d52bdd2 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 bc3c819a75..635a5ea005 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 0000000000..81e3334e1f --- /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 5f64661a3a..0000000000 --- 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 c20d416331..71be06ceb0 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;