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/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 | diff --git a/src/plugin.rs b/src/plugin.rs index 51366b3237..e96fc988f9 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -4,6 +4,7 @@ use std::io; pub trait Plugin { fn config(&mut self) -> Result; + #[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 new file mode 100644 index 0000000000..5830d4a008 --- /dev/null +++ b/src/plugins/str.rs @@ -0,0 +1,173 @@ +use indexmap::IndexMap; +use nu::{ + serve_plugin, CallInfo, CommandConfig, NamedType, Plugin, PositionalType, Primitive, + ReturnSuccess, ReturnValue, ShellError, Spanned, Value, +}; + +struct Str { + field: Option, + error: Option, + downcase: bool, + upcase: bool, +} + +impl Str { + fn new() -> Str { + Str { + field: None, + error: None, + downcase: false, + upcase: false, + } + } + + 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 for_input(&mut self, field: String) { + self.field = Some(field); + } + + fn for_downcase(&mut self) { + self.downcase = true; + + if !self.is_valid() { + self.log_error("can only apply one") + } + } + + fn for_upcase(&mut self) { + self.upcase = true; + + if !self.is_valid() { + self.log_error("can only apply one") + } + } + + 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)) => 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) { + 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.for_downcase(); + } + + if call_info.args.has("upcase") { + self.for_upcase(); + } + + if let Some(args) = call_info.args.positional { + for arg in args { + match arg { + Spanned { + item: Value::Primitive(Primitive::String(s)), + .. + } => { + self.for_input(s); + } + _ => { + return Err(ShellError::string(format!( + "Unrecognized type in params: {:?}", + arg + ))) + } + } + } + } + + match &self.error { + Some(reason) => { + return Err(ShellError::string(format!("{}: {}", reason, self.usage()))) + } + None => {} + } + + 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..6c542b4424 100644 --- a/tests/filters_test.rs +++ b/tests/filters_test.rs @@ -65,6 +65,39 @@ 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!( + 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!(