diff --git a/Cargo.toml b/Cargo.toml index cca4fbce83..323069da8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,6 +137,10 @@ path = "src/plugins/insert.rs" name = "nu_plugin_edit" path = "src/plugins/edit.rs" +[[bin]] +name = "nu_plugin_format" +path = "src/plugins/format.rs" + [[bin]] name = "nu_plugin_parse" path = "src/plugins/parse.rs" diff --git a/README.md b/README.md index ef245135ad..46a4c45ac8 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat | edit column-or-column-path value | Edit an existing column to have a new value | | embed column | Creates a new table of one column with the given name, and places the current table inside of it | | first amount | Show only the first number of rows | +| format pattern | Format table row data as a string following the given pattern | | get column-or-column-path | Open column and get data from the corresponding cells | | group-by column | Creates a new table with the data from the table rows grouped by the column given | | inc (column-or-column-path) | Increment a value or version. Optionally use the column of a table | diff --git a/src/data/base.rs b/src/data/base.rs index 72c98f2c89..d877e4a7cc 100644 --- a/src/data/base.rs +++ b/src/data/base.rs @@ -422,7 +422,7 @@ impl Tagged { } } - pub(crate) fn as_string(&self) -> Result { + pub fn as_string(&self) -> Result { match &self.item { Value::Primitive(Primitive::String(s)) => Ok(s.clone()), Value::Primitive(Primitive::Boolean(x)) => Ok(format!("{}", x)), diff --git a/src/plugins/format.rs b/src/plugins/format.rs new file mode 100644 index 0000000000..7ed33f964c --- /dev/null +++ b/src/plugins/format.rs @@ -0,0 +1,128 @@ +use nu::{ + serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, + SyntaxShape, Tagged, TaggedItem, Value, +}; + +use nom::{ + bytes::complete::{tag, take_while}, + IResult, +}; + +#[derive(Debug)] +enum FormatCommand { + Text(String), + Column(String), +} + +fn format(input: &str) -> IResult<&str, Vec> { + let mut output = vec![]; + + let mut loop_input = input; + loop { + let (input, before) = take_while(|c| c != '{')(loop_input)?; + if before.len() > 0 { + output.push(FormatCommand::Text(before.to_string())); + } + if input != "" { + // Look for column as we're now at one + let (input, _) = tag("{")(input)?; + let (input, column) = take_while(|c| c != '}')(input)?; + let (input, _) = tag("}")(input)?; + + output.push(FormatCommand::Column(column.to_string())); + loop_input = input; + } else { + loop_input = input; + } + if loop_input == "" { + break; + } + } + + Ok((loop_input, output)) +} + +struct Format { + commands: Vec, +} + +impl Format { + fn new() -> Self { + Format { commands: vec![] } + } +} + +impl Plugin for Format { + fn config(&mut self) -> Result { + Ok(Signature::build("format") + .desc("Format columns into a string using a simple pattern") + .required( + "pattern", + SyntaxShape::Any, + "the pattern to match. Eg) \"{foo}: {bar}\"", + ) + .filter()) + } + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + if let Some(args) = call_info.args.positional { + match &args[0] { + Tagged { + item: Value::Primitive(Primitive::String(pattern)), + .. + } => { + let format_pattern = format(&pattern).unwrap(); + self.commands = format_pattern.1 + } + Tagged { tag, .. } => { + return Err(ShellError::labeled_error( + "Unrecognized type in params", + "expected a string", + tag, + )); + } + } + } + Ok(vec![]) + } + + fn filter(&mut self, input: Tagged) -> Result, ShellError> { + match &input { + Tagged { + item: Value::Row(dict), + .. + } => { + let mut output = String::new(); + + for command in &self.commands { + match command { + FormatCommand::Text(s) => { + output.push_str(s); + } + FormatCommand::Column(c) => { + match dict.entries.get(c) { + Some(c) => match c.as_string() { + Ok(v) => output.push_str(&v), + _ => return Ok(vec![]), + }, + None => { + // This row doesn't match, so don't emit anything + return Ok(vec![]); + } + } + } + } + } + + return Ok(vec![ReturnSuccess::value( + Value::string(output).tagged_unknown(), + )]); + } + _ => {} + } + Ok(vec![]) + } +} + +fn main() { + serve_plugin(&mut Format::new()); +} diff --git a/tests/tests.rs b/tests/tests.rs index 490dabefff..8799f5077d 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -57,7 +57,7 @@ fn insert_plugin() { } #[test] -fn read_plugin() { +fn parse_plugin() { let actual = nu!( cwd: "tests/fixtures/formats", h::pipeline( r#" @@ -72,6 +72,21 @@ fn read_plugin() { assert_eq!(actual, "StupidLongName"); } +#[test] +fn format_plugin() { + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open cargo_sample.toml + | get package + | format "{name} has license {license}" + | echo $it + "# + )); + + assert_eq!(actual, "nu has license ISC"); +} + #[test] fn prepend_plugin() { let actual = nu!(