From 3820fef801641b13a49c4106a8f5823c1eb5ef4b Mon Sep 17 00:00:00 2001 From: Jonathan Turner Date: Wed, 30 Oct 2019 11:33:36 +1300 Subject: [PATCH] Add a simple read/parse plugin to better handle text data --- Cargo.toml | 6 +- src/plugins/read.rs | 156 +++++++++++++++++++++++++++++++ src/utils.rs | 4 + tests/fixtures/formats/fileA.txt | 3 + tests/tests.rs | 16 ++++ 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/plugins/read.rs create mode 100644 tests/fixtures/formats/fileA.txt diff --git a/Cargo.toml b/Cargo.toml index e81e830e1a..52589cb733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,8 +74,8 @@ natural = "0.3.0" serde_urlencoded = "0.6.1" sublime_fuzzy = "0.5" trash = "1.0.0" +regex = "1" -regex = {version = "1", optional = true } neso = { version = "0.5.0", optional = true } crossterm = { version = "0.10.2", optional = true } syntect = {version = "3.2.0", optional = true } @@ -136,6 +136,10 @@ path = "src/plugins/add.rs" name = "nu_plugin_edit" path = "src/plugins/edit.rs" +[[bin]] +name = "nu_plugin_read" +path = "src/plugins/read.rs" + [[bin]] name = "nu_plugin_str" path = "src/plugins/str.rs" diff --git a/src/plugins/read.rs b/src/plugins/read.rs new file mode 100644 index 0000000000..de88946e91 --- /dev/null +++ b/src/plugins/read.rs @@ -0,0 +1,156 @@ +use nu::{ + serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, + SyntaxShape, Tagged, TaggedDictBuilder, Value, +}; + +use nom::{ + bytes::complete::{tag, take_while}, + IResult, +}; +use regex::Regex; + +#[derive(Debug)] +enum ReadCommand { + Text(String), + Column(String), +} + +fn read(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(ReadCommand::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(ReadCommand::Column(column.to_string())); + loop_input = input; + } else { + loop_input = input; + } + if loop_input == "" { + break; + } + } + + Ok((loop_input, output)) +} + +fn column_names(commands: &[ReadCommand]) -> Vec { + let mut output = vec![]; + + for command in commands { + match command { + ReadCommand::Column(c) => { + output.push(c.clone()); + } + _ => {} + } + } + + output +} + +fn build_regex(commands: &[ReadCommand]) -> String { + let mut output = String::new(); + + for command in commands { + match command { + ReadCommand::Text(s) => { + output.push_str(&s.replace("(", "\\(")); + } + ReadCommand::Column(_) => { + output.push_str("(.*)"); + } + } + } + + return output; +} +struct Read { + regex: Regex, + column_names: Vec, +} + +impl Read { + fn new() -> Self { + Read { + regex: Regex::new("").unwrap(), + column_names: vec![], + } + } +} + +impl Plugin for Read { + fn config(&mut self) -> Result { + Ok(Signature::build("read") + .desc("Parse columns from string data 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)), + .. + } => { + //self.pattern = s.clone(); + let read_pattern = read(&pattern).unwrap(); + let read_regex = build_regex(&read_pattern.1); + + self.column_names = column_names(&read_pattern.1); + + self.regex = Regex::new(&read_regex).unwrap(); + } + Tagged { tag, .. } => { + return Err(ShellError::labeled_error( + "Unrecognized type in params", + "value", + tag, + )); + } + } + } + Ok(vec![]) + } + + fn filter(&mut self, input: Tagged) -> Result, ShellError> { + let mut results = vec![]; + match &input { + Tagged { + tag, + item: Value::Primitive(Primitive::String(s)), + } => { + //self.full_input.push_str(&s); + + for cap in self.regex.captures_iter(&s) { + let mut dict = TaggedDictBuilder::new(tag); + + for (idx, column_name) in self.column_names.iter().enumerate() { + dict.insert(column_name, Value::string(&cap[idx + 1].to_string())); + } + + results.push(ReturnSuccess::value(dict.into_tagged_value())); + } + } + _ => {} + } + Ok(results) + } +} + +fn main() { + serve_plugin(&mut Read::new()); +} diff --git a/src/utils.rs b/src/utils.rs index 6b1318f9e8..56fee491b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -448,6 +448,10 @@ mod tests { loc: fixtures().join("cargo_sample.toml"), at: 0 }, + Res { + loc: fixtures().join("fileA.txt"), + at: 0 + }, Res { loc: fixtures().join("jonathan.xml"), at: 0 diff --git a/tests/fixtures/formats/fileA.txt b/tests/fixtures/formats/fileA.txt new file mode 100644 index 0000000000..0ce9fb3fa2 --- /dev/null +++ b/tests/fixtures/formats/fileA.txt @@ -0,0 +1,3 @@ +VAR1=Chill +VAR2=StupidLongName +VAR3=AlsoChill diff --git a/tests/tests.rs b/tests/tests.rs index 25337edb09..1a739f1982 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -56,6 +56,22 @@ fn add_plugin() { assert_eq!(actual, "1"); } +#[test] +fn read_plugin() { + let actual = nu!( + cwd: "tests/fixtures/formats", h::pipeline( + r#" + open fileA.txt + | read "{Name}={Value}" + | nth 1 + | get Value + | echo $it + "# + )); + + assert_eq!(actual, "StupidLongName"); +} + #[test] fn edit_plugin() { let actual = nu!(