diff --git a/src/data/base/property_get.rs b/src/data/base/property_get.rs index 81589a11b3..047484918d 100644 --- a/src/data/base/property_get.rs +++ b/src/data/base/property_get.rs @@ -263,6 +263,10 @@ impl Value { Ok(ColumnPath::new(out).tagged(&self.tag)) } + UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(ColumnPath::new(vec![PathMember::string(s, &self.tag.span)]).tagged(&self.tag)) + } + UntaggedValue::Primitive(Primitive::ColumnPath(path)) => { Ok(path.clone().tagged(self.tag.clone())) } @@ -299,6 +303,9 @@ impl Value { UntaggedValue::Primitive(Primitive::Int(x)) => Ok(format!("{}", x)), UntaggedValue::Primitive(Primitive::Bytes(x)) => Ok(format!("{}", x)), UntaggedValue::Primitive(Primitive::Path(x)) => Ok(format!("{}", x.display())), + UntaggedValue::Primitive(Primitive::ColumnPath(path)) => { + Ok(path.iter().map(|member| member.display()).join(".")) + } // TODO: this should definitely be more general with better errors other => Err(ShellError::labeled_error( "Expected string", diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 38b69d6826..bb9e9017d3 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -4,6 +4,7 @@ use nu::{ }; use nu_source::{span_for_spanned_list, Tagged}; +use regex::Regex; use std::cmp; #[derive(Debug, Eq, PartialEq)] @@ -12,11 +13,17 @@ enum Action { Upcase, ToInteger, Substring(usize, usize), + Replace(ReplaceAction), +} + +#[derive(Debug, Eq, PartialEq)] +enum ReplaceAction { + Direct(String), + FindAndReplace(String, String), } struct Str { field: Option>, - params: Option>, error: Option, action: Option, } @@ -25,7 +32,6 @@ impl Str { fn new() -> Str { Str { field: None, - params: Some(Vec::::new()), error: None, action: None, } @@ -50,6 +56,19 @@ impl Str { ) } } + Some(Action::Replace(mode)) => match mode { + ReplaceAction::Direct(replacement) => UntaggedValue::string(replacement.as_str()), + ReplaceAction::FindAndReplace(find, replacement) => { + let regex = Regex::new(find.as_str()); + + match regex { + Ok(re) => UntaggedValue::string( + re.replace(input, replacement.as_str()).to_owned(), + ), + Err(_) => UntaggedValue::string(input), + } + } + }, Some(Action::ToInteger) => match input.trim() { other => match other.parse::() { Ok(v) => UntaggedValue::int(v), @@ -117,8 +136,16 @@ impl Str { } } + fn for_replace(&mut self, mode: ReplaceAction) { + if self.permit() { + self.action = Some(Action::Replace(mode)); + } else { + self.log_error("can only apply one"); + } + } + pub fn usage() -> &'static str { - "Usage: str field [--downcase|--upcase|--to-int|--substring \"start,end\"]" + "Usage: str field [--downcase|--upcase|--to-int|--substring \"start,end\"|--replace|--find-replace [pattern replacement]]]" } } @@ -186,6 +213,12 @@ impl Plugin for Str { .switch("downcase", "convert string to lowercase") .switch("upcase", "convert string to uppercase") .switch("to-int", "convert string to integer") + .named("replace", SyntaxShape::String, "replaces the string") + .named( + "find-replace", + SyntaxShape::Any, + "finds and replaces [pattern replacement]", + ) .named( "substring", SyntaxShape::String, @@ -226,21 +259,33 @@ impl Plugin for Str { } } } + if args.has("replace") { + if let Some(Value { + value: UntaggedValue::Primitive(Primitive::String(replacement)), + .. + }) = args.get("replace") + { + self.for_replace(ReplaceAction::Direct(replacement.clone())); + } + } + + if args.has("find-replace") { + if let Some(Value { + value: UntaggedValue::Table(arguments), + .. + }) = args.get("find-replace") + { + self.for_replace(ReplaceAction::FindAndReplace( + arguments.get(0).unwrap().as_string()?, + arguments.get(1).unwrap().as_string()?, + )); + } + } if let Some(possible_field) = args.nth(0) { let possible_field = possible_field.as_column_path()?; - self.for_field(possible_field); } - for param in args.positional_iter() { - match param { - Value { - value: UntaggedValue::Primitive(Primitive::String(s)), - .. - } => self.params.as_mut().unwrap().push(String::from(s)), - _ => {} - } - } match &self.error { Some(reason) => { @@ -265,15 +310,32 @@ fn main() { #[cfg(test)] mod tests { - use super::{Action, Str}; + use super::{Action, ReplaceAction, Str}; use indexmap::IndexMap; use nu::{ CallInfo, EvaluatedArgs, Plugin, Primitive, ReturnSuccess, TaggedDictBuilder, - UnspannedPathMember, UntaggedValue, Value, + UntaggedValue, Value, }; use nu_source::Tag; use num_bigint::BigInt; + fn string(input: impl Into) -> Value { + UntaggedValue::string(input.into()).into_untagged_value() + } + + fn table(list: &Vec) -> Value { + UntaggedValue::table(list).into_untagged_value() + } + + fn column_path(paths: &Vec) -> Value { + UntaggedValue::Primitive(Primitive::ColumnPath( + table(&paths.iter().cloned().collect()) + .as_column_path() + .unwrap() + .item, + )) + .into_untagged_value() + } struct CallStub { positionals: Vec, flags: IndexMap, @@ -287,11 +349,8 @@ mod tests { } } - fn with_named_parameter(&mut self, name: &str, value: &str) -> &mut Self { - self.flags.insert( - name.to_string(), - UntaggedValue::string(value).into_value(Tag::unknown()), - ); + fn with_named_parameter(&mut self, name: &str, value: Value) -> &mut Self { + self.flags.insert(name.to_string(), value); self } @@ -309,8 +368,7 @@ mod tests { .map(|s| UntaggedValue::string(s.to_string()).into_value(Tag::unknown())) .collect(); - self.positionals - .push(UntaggedValue::Table(fields).into_value(Tag::unknown())); + self.positionals.push(column_path(&fields)); self } @@ -338,7 +396,14 @@ mod tests { let configured = plugin.config().unwrap(); - for action_flag in &["downcase", "upcase", "to-int"] { + for action_flag in &[ + "downcase", + "upcase", + "to-int", + "substring", + "replace", + "find-replace", + ] { assert!(configured.named.get(*action_flag).is_some()); } } @@ -372,6 +437,55 @@ mod tests { .is_ok()); assert_eq!(plugin.action.unwrap(), Action::ToInteger); } + + #[test] + fn str_plugin_accepts_replace() { + let mut plugin = Str::new(); + + let argument = String::from("replace_text"); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("replace", string(&argument)) + .create() + ) + .is_ok()); + + match plugin.action { + Some(Action::Replace(ReplaceAction::Direct(replace_with))) => { + assert_eq!(replace_with, argument) + } + Some(_) | None => panic!("Din't accept."), + } + } + + #[test] + fn str_plugin_accepts_find_replace() { + let mut plugin = Str::new(); + + let search_argument = String::from("kittens"); + let replace_argument = String::from("jotandrehuda"); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter( + "find-replace", + table(&vec![string(&search_argument), string(&replace_argument)]) + ) + .create() + ) + .is_ok()); + + match plugin.action { + Some(Action::Replace(ReplaceAction::FindAndReplace(find_with, replace_with))) => { + assert_eq!(find_with, search_argument); + assert_eq!(replace_with, replace_argument); + } + Some(_) | None => panic!("Din't accept."), + } + } #[test] fn str_plugin_accepts_field() { let mut plugin = Str::new(); @@ -384,14 +498,13 @@ mod tests { ) .is_ok()); + let actual = &*plugin.field.unwrap(); + let actual = UntaggedValue::Primitive(Primitive::ColumnPath(actual.clone())); + let actual = actual.into_value(Tag::unknown()); + assert_eq!( - plugin - .field - .map(|f| f.iter().cloned().map(|f| f.unspanned).collect()), - Some(vec![ - UnspannedPathMember::String("package".to_string()), - UnspannedPathMember::String("description".to_string()) - ]) + actual, + column_path(&vec![string("package"), string("description")]) ) } @@ -442,6 +555,30 @@ mod tests { ); } + #[test] + fn str_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::Direct("robalino".to_string())); + + assert_eq!( + strutils.apply("andres").unwrap(), + UntaggedValue::string("robalino") + ); + } + + #[test] + fn str_find_replace() { + let mut strutils = Str::new(); + strutils.for_replace(ReplaceAction::FindAndReplace( + "kittens".to_string(), + "jotandrehuda".to_string(), + )); + assert_eq!( + strutils.apply("wykittens").unwrap(), + UntaggedValue::string("wyjotandrehuda") + ); + } + #[test] fn str_plugin_applies_upcase_with_field() { let mut plugin = Str::new(); @@ -593,7 +730,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", "0,1") + .with_named_parameter("substring", string("0,1")) .create() ) .is_ok()); @@ -617,7 +754,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", "0,11") + .with_named_parameter("substring", string("0,11")) .create() ) .is_ok()); @@ -641,7 +778,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", "20,30") + .with_named_parameter("substring", string("20,30")) .create() ) .is_ok()); @@ -665,7 +802,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", ",5") + .with_named_parameter("substring", string(",5")) .create() ) .is_ok()); @@ -689,7 +826,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", "2,") + .with_named_parameter("substring", string("2,")) .create() ) .is_ok()); @@ -713,7 +850,7 @@ mod tests { assert!(plugin .begin_filter( CallStub::new() - .with_named_parameter("substring", "3,1") + .with_named_parameter("substring", string("3,1")) .create() ) .is_err()); @@ -722,4 +859,120 @@ mod tests { Some("End must be greater than or equal to Start".to_string()) ); } + + #[test] + fn str_plugin_applies_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("rustconf") + .with_named_parameter("replace", string("22nd August 2019")) + .create() + ) + .is_ok()); + + let subject = structured_sample_record("rustconf", "1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("rustconf")).borrow(), + Value { + value: UntaggedValue::string(String::from("22nd August 2019")), + tag: Tag::unknown() + } + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("replace", string("22nd August 2019")) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("1st January 1970"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("22nd August 2019")), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_with_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_parameter("staff") + .with_named_parameter( + "find-replace", + table(&vec![string("kittens"), string("jotandrehuda")]) + ) + .create() + ) + .is_ok()); + + let subject = structured_sample_record("staff", "wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Row(o), + .. + }) => assert_eq!( + *o.get_data(&String::from("staff")).borrow(), + Value { + value: UntaggedValue::string(String::from("wyjotandrehuda")), + tag: Tag::unknown() + } + ), + _ => {} + } + } + + #[test] + fn str_plugin_applies_find_replace_without_field() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter( + "find-replace", + table(&vec![string("kittens"), string("jotandrehuda")]) + ) + .create() + ) + .is_ok()); + + let subject = unstructured_sample_record("wykittens"); + let output = plugin.filter(subject).unwrap(); + + match output[0].as_ref().unwrap() { + ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + .. + }) => assert_eq!(*s, String::from("wyjotandrehuda")), + _ => {} + } + } } diff --git a/tests/filter_str_tests.rs b/tests/filter_str_tests.rs index 9f92186fa6..a58c5439b6 100644 --- a/tests/filter_str_tests.rs +++ b/tests/filter_str_tests.rs @@ -10,7 +10,7 @@ fn can_only_apply_one() { "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" ); - assert!(actual.contains("Usage: str field [--downcase|--upcase|--to-int")); + assert!(actual.contains(r#"--downcase|--upcase|--to-int|--substring "start,end"|--replace|--find-replace [pattern replacement]]"#)); } #[test] @@ -90,3 +90,78 @@ fn converts_to_int() { assert_eq!(actual, "2509000000"); } + +#[test] +fn replaces() { + Playground::setup("plugin_str_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), h::pipeline( + r#" + open sample.toml + | str package.name --replace wykittenshell + | get package.name + | echo $it + "# + )); + + assert_eq!(actual, "wykittenshell"); + }) +} + +#[test] +fn find_and_replaces() { + Playground::setup("plugin_str_test_5", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-KATZ" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), h::pipeline( + r#" + open sample.toml + | str fortune.teller.phone --find-replace [KATZ 5289] + | get fortune.teller.phone + | echo $it + "# + )); + + assert_eq!(actual, "1-800-5289"); + }) +} + +#[test] +fn find_and_replaces_without_passing_field() { + Playground::setup("plugin_str_test_6", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-KATZ" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), h::pipeline( + r#" + open sample.toml + | get fortune.teller.phone + | str --find-replace [KATZ 5289] + | echo $it + "# + )); + + assert_eq!(actual, "1-800-5289"); + }) +}