From 59dec999b83b55050f965bfdbc04ea08513d75af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 28 Jul 2019 02:01:32 -0500 Subject: [PATCH 1/5] string utils plugin baseline. --- Cargo.toml | 4 ++ src/plugins/str.rs | 125 ++++++++++++++++++++++++++++++++++++++++++ tests/filters_test.rs | 22 ++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/plugins/str.rs diff --git a/Cargo.toml b/Cargo.toml index 84bc8ff719..39aa0682f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,6 +104,10 @@ path = "src/plugins/add.rs" name = "nu_plugin_edit" path = "src/plugins/edit.rs" +[[bin]] +name = "nu_plugin_str" +path = "src/plugins/str.rs" + [[bin]] name = "nu_plugin_skip" path = "src/plugins/skip.rs" diff --git a/src/plugins/str.rs b/src/plugins/str.rs new file mode 100644 index 0000000000..7fdfcb21e5 --- /dev/null +++ b/src/plugins/str.rs @@ -0,0 +1,125 @@ +use indexmap::IndexMap; +use nu::{ + serve_plugin, CallInfo, CommandConfig, NamedType, Plugin, PositionalType, Primitive, + ReturnSuccess, ReturnValue, ShellError, Spanned, SpannedItem, Value, +}; + +struct Str { + field: Option, + downcase: bool, + upcase: bool, +} + +impl Str { + fn new() -> Str { + Str { + field: None, + downcase: false, + upcase: false, + } + } + + fn strutils( + &self, + value: Spanned, + field: &Option, + ) -> Result, ShellError> { + match value.item { + Value::Primitive(Primitive::String(s)) => { + let applied = if self.downcase { + Value::string(s.to_ascii_lowercase()) + } else if self.upcase { + Value::string(s.to_ascii_uppercase()) + } else { + Value::string(s) + }; + + Ok(Spanned { + item: applied, + span: value.span, + }) + } + Value::Object(_) => match field { + Some(f) => { + let replacement = match value.item.get_data_by_path(value.span, f) { + Some(result) => self.strutils(result.map(|x| x.clone()), &None)?, + None => { + return Err(ShellError::string("str could not find field to replace")) + } + }; + match value + .item + .replace_data_at_path(value.span, f, replacement.item.clone()) + { + Some(v) => return Ok(v), + None => { + return Err(ShellError::string("str could not find field to replace")) + } + } + } + None => Err(ShellError::string( + "str needs a field when applying it to a value in an object", + )), + }, + x => Err(ShellError::string(format!( + "Unrecognized type in stream: {:?}", + x + ))), + } + } +} + +impl Plugin for Str { + fn config(&mut self) -> Result { + let mut named = IndexMap::new(); + named.insert("downcase".to_string(), NamedType::Switch); + named.insert("upcase".to_string(), NamedType::Switch); + + Ok(CommandConfig { + name: "str".to_string(), + positional: vec![PositionalType::optional_any("Field")], + is_filter: true, + is_sink: false, + named, + rest_positional: true, + }) + } + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { + if call_info.args.has("downcase") { + self.downcase = true; + } else if call_info.args.has("upcase") { + self.upcase = true; + } + + if let Some(args) = call_info.args.positional { + for arg in args { + match arg { + Spanned { + item: Value::Primitive(Primitive::String(s)), + .. + } => { + self.field = Some(s); + } + _ => { + return Err(ShellError::string(format!( + "Unrecognized type in params: {:?}", + arg + ))) + } + } + } + } + + Ok(vec![]) + } + + fn filter(&mut self, input: Spanned) -> Result, ShellError> { + Ok(vec![ReturnSuccess::value( + self.strutils(input, &self.field)?, + )]) + } +} + +fn main() { + serve_plugin(&mut Str::new()); +} diff --git a/tests/filters_test.rs b/tests/filters_test.rs index bf07898a2a..5660fbaee6 100644 --- a/tests/filters_test.rs +++ b/tests/filters_test.rs @@ -65,6 +65,28 @@ fn can_split_by_column() { assert_eq!(output, "name"); } +#[test] +fn str_downcases() { + nu!( + output, + cwd("tests/fixtures/formats"), + "open caco3_plastics.csv | first 1 | str origin --downcase | get origin | echo $it" + ); + + assert_eq!(output, "spain"); +} + +#[test] +fn str_upcases() { + nu!( + output, + cwd("tests/fixtures/formats"), + "open appveyor.yml | str environment.global.PROJECT_NAME --upcase | get environment.global.PROJECT_NAME | echo $it" + ); + + assert_eq!(output, "NUSHELL"); +} + #[test] fn can_inc_version() { nu!( From 7c4706ee503ef69d40e8002fdf4161e619730a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 28 Jul 2019 18:34:37 -0500 Subject: [PATCH 2/5] Validation baseline. --- src/plugin.rs | 9 ++++++++ src/plugins/str.rs | 52 +++++++++++++++++++++++++++++++++++++++---- tests/filters_test.rs | 11 +++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/plugin.rs b/src/plugin.rs index 51366b3237..bbac5d6595 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -4,6 +4,15 @@ use std::io; pub trait Plugin { fn config(&mut self) -> Result; + + #[allow(unused)] + fn is_valid(&self) -> bool { + true + } + + #[allow(unused)] + fn log_error(&mut self, message: &str) { } + #[allow(unused)] fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { Ok(vec![]) diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 7fdfcb21e5..a929def8dd 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -1,11 +1,12 @@ use indexmap::IndexMap; use nu::{ serve_plugin, CallInfo, CommandConfig, NamedType, Plugin, PositionalType, Primitive, - ReturnSuccess, ReturnValue, ShellError, Spanned, SpannedItem, Value, + ReturnSuccess, ReturnValue, ShellError, Spanned, Value, }; struct Str { field: Option, + error: Option, downcase: bool, upcase: bool, } @@ -14,11 +15,32 @@ impl Str { fn new() -> Str { Str { field: None, + error: None, downcase: false, upcase: false, } } + fn to_downcase(&mut self) { + self.downcase = true; + + if !self.is_valid() { + self.log_error("can only apply one") + } + } + + fn to_upcase(&mut self) { + self.upcase = true; + + if !self.is_valid() { + self.log_error("can only apply one") + } + } + + fn usage(&self) -> &'static str { + "Usage: str [--downcase, --upcase]" + } + fn strutils( &self, value: Spanned, @@ -84,11 +106,22 @@ impl Plugin for Str { rest_positional: true, }) } + + fn is_valid(&self) -> bool { + (self.downcase && !self.upcase) || (!self.downcase && self.upcase) + } + + fn log_error(&mut self, message: &str) { + self.error = Some(message.to_string()); + } + fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { if call_info.args.has("downcase") { - self.downcase = true; - } else if call_info.args.has("upcase") { - self.upcase = true; + self.to_downcase(); + } + + if call_info.args.has("upcase") { + self.to_upcase(); } if let Some(args) = call_info.args.positional { @@ -110,6 +143,17 @@ impl Plugin for Str { } } + match &self.error { + Some(reason) => { + return Err(ShellError::string(format!( + "{}: {}", + reason, + self.usage() + ))) + } + None => {} + } + Ok(vec![]) } diff --git a/tests/filters_test.rs b/tests/filters_test.rs index 5660fbaee6..6c542b4424 100644 --- a/tests/filters_test.rs +++ b/tests/filters_test.rs @@ -65,6 +65,17 @@ fn can_split_by_column() { assert_eq!(output, "name"); } +#[test] +fn str_can_only_apply_one() { + nu_error!( + output, + cwd("tests/fixtures/formats"), + "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" + ); + + assert!(output.contains("Usage: str [--downcase, --upcase]")); +} + #[test] fn str_downcases() { nu!( From d1399c0c0cab57fa9a368f2ad9b690fb28c5bdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 28 Jul 2019 19:00:06 -0500 Subject: [PATCH 3/5] str filter description to readme. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9ebdb06eb6..810885d651 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Nu adheres closely to a set of goals that make up its design philosophy. As feat | edit field value | Edit an existing field to have a new value | | skip amount | Skip a number of rows | | first amount | Show only the first number of rows | +| str (field) | Apply string function. Optional use the field of a table | | to-array | Collapse rows into a single list | | to-json | Convert table into .json text | | to-toml | Convert table into .toml text | From 87b299739c74afad6e4d7e631ff6bcd1b39043f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 28 Jul 2019 20:13:06 -0500 Subject: [PATCH 4/5] Make the validation especific to str plugin for now. --- src/plugin.rs | 8 -------- src/plugins/str.rs | 16 ++++++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/plugin.rs b/src/plugin.rs index bbac5d6595..e96fc988f9 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -5,14 +5,6 @@ use std::io; pub trait Plugin { fn config(&mut self) -> Result; - #[allow(unused)] - fn is_valid(&self) -> bool { - true - } - - #[allow(unused)] - fn log_error(&mut self, message: &str) { } - #[allow(unused)] fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { Ok(vec![]) diff --git a/src/plugins/str.rs b/src/plugins/str.rs index a929def8dd..8c1a68693a 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -21,6 +21,14 @@ impl Str { } } + fn is_valid(&self) -> bool { + (self.downcase && !self.upcase) || (!self.downcase && self.upcase) + } + + fn log_error(&mut self, message: &str) { + self.error = Some(message.to_string()); + } + fn to_downcase(&mut self) { self.downcase = true; @@ -107,14 +115,6 @@ impl Plugin for Str { }) } - fn is_valid(&self) -> bool { - (self.downcase && !self.upcase) || (!self.downcase && self.upcase) - } - - fn log_error(&mut self, message: &str) { - self.error = Some(message.to_string()); - } - fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { if call_info.args.has("downcase") { self.to_downcase(); From be4262e96ae2d1f8bd5c359ae6788c2b86f6aa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20N=2E=20Robalino?= Date: Sun, 28 Jul 2019 21:30:47 -0500 Subject: [PATCH 5/5] Separate Nu plugin logic. --- src/plugins/str.rs | 52 +++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 8c1a68693a..5830d4a008 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -29,7 +29,11 @@ impl Str { self.error = Some(message.to_string()); } - fn to_downcase(&mut self) { + fn for_input(&mut self, field: String) { + self.field = Some(field); + } + + fn for_downcase(&mut self) { self.downcase = true; if !self.is_valid() { @@ -37,7 +41,7 @@ impl Str { } } - fn to_upcase(&mut self) { + fn for_upcase(&mut self) { self.upcase = true; if !self.is_valid() { @@ -45,30 +49,34 @@ impl Str { } } + fn apply(&self, input: &str) -> String { + if self.downcase { + return input.to_ascii_lowercase(); + } + + if self.upcase { + return input.to_ascii_uppercase(); + } + + input.to_string() + } + fn usage(&self) -> &'static str { "Usage: str [--downcase, --upcase]" } +} +impl Str { fn strutils( &self, value: Spanned, field: &Option, ) -> Result, ShellError> { match value.item { - Value::Primitive(Primitive::String(s)) => { - let applied = if self.downcase { - Value::string(s.to_ascii_lowercase()) - } else if self.upcase { - Value::string(s.to_ascii_uppercase()) - } else { - Value::string(s) - }; - - Ok(Spanned { - item: applied, - span: value.span, - }) - } + Value::Primitive(Primitive::String(s)) => Ok(Spanned { + item: Value::string(self.apply(&s)), + span: value.span, + }), Value::Object(_) => match field { Some(f) => { let replacement = match value.item.get_data_by_path(value.span, f) { @@ -117,11 +125,11 @@ impl Plugin for Str { fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { if call_info.args.has("downcase") { - self.to_downcase(); + self.for_downcase(); } if call_info.args.has("upcase") { - self.to_upcase(); + self.for_upcase(); } if let Some(args) = call_info.args.positional { @@ -131,7 +139,7 @@ impl Plugin for Str { item: Value::Primitive(Primitive::String(s)), .. } => { - self.field = Some(s); + self.for_input(s); } _ => { return Err(ShellError::string(format!( @@ -145,11 +153,7 @@ impl Plugin for Str { match &self.error { Some(reason) => { - return Err(ShellError::string(format!( - "{}: {}", - reason, - self.usage() - ))) + return Err(ShellError::string(format!("{}: {}", reason, self.usage()))) } None => {} }