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/default_context.rs b/crates/nu-command/src/default_context.rs index 95635f093c..22b0b8e2ff 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/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..4c78e5b46f --- /dev/null +++ b/crates/nu-command/src/strings/format/command.rs @@ -0,0 +1,200 @@ +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) => { + 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), +} + +/// 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![]; + + 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 +} + +/// 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(); + + // 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()))), + 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/mod.rs b/crates/nu-command/src/strings/format/mod.rs new file mode 100644 index 0000000000..71be06ceb0 --- /dev/null +++ b/crates/nu-command/src/strings/format/mod.rs @@ -0,0 +1,3 @@ +pub mod command; + +pub use command::Format; diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index bbb78a24d7..a23eba5ff0 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::*;