Str plugin: replace & find-replace.

This commit is contained in:
Andrés N. Robalino 2019-08-11 12:46:14 -05:00
parent 20b909e316
commit cee2f5207e
2 changed files with 434 additions and 65 deletions

View file

@ -1,17 +1,27 @@
use indexmap::IndexMap; use indexmap::IndexMap;
use nu::{ use nu::{
serve_plugin, CallInfo, NamedType, Plugin, Primitive, ReturnSuccess, serve_plugin, CallInfo, NamedType, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError,
ReturnValue, ShellError, Signature, Tagged, Value, Signature, Tagged, Value,
}; };
use regex::Regex;
#[derive(Debug, Eq, PartialEq)]
enum Action { enum Action {
Downcase, Downcase,
Upcase, Upcase,
ToInteger, ToInteger,
Replace(ReplaceAction),
}
#[derive(Debug, Eq, PartialEq)]
enum ReplaceAction {
Direct,
FindAndReplace,
} }
struct Str { struct Str {
field: Option<String>, field: Option<String>,
params: Option<Vec<String>>,
error: Option<String>, error: Option<String>,
action: Option<Action>, action: Option<Action>,
} }
@ -20,25 +30,58 @@ impl Str {
fn new() -> Str { fn new() -> Str {
Str { Str {
field: None, field: None,
params: Some(Vec::<String>::new()),
error: None, error: None,
action: None, action: None,
} }
} }
fn apply(&self, input: &str) -> Value { fn apply(&self, input: &str) -> Value {
match self.action { if self.action.is_none() {
Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()), return Value::string(input.to_string());
Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()), }
Some(Action::ToInteger) => match input.trim().parse::<i64>() {
match self.action.as_ref().unwrap() {
Action::Downcase => Value::string(input.to_ascii_lowercase()),
Action::Upcase => Value::string(input.to_ascii_uppercase()),
Action::ToInteger => match input.trim().parse::<i64>() {
Ok(v) => Value::int(v), Ok(v) => Value::int(v),
Err(_) => Value::string(input), Err(_) => Value::string(input),
}, },
None => Value::string(input.to_string()), Action::Replace(ref mode) => match mode {
ReplaceAction::Direct => Value::string(self.first_param()),
ReplaceAction::FindAndReplace => {
let regex = Regex::new(self.first_param());
match regex {
Ok(re) => Value::string(re.replace(input, self.second_param()).to_owned()),
Err(_) => Value::string(input),
}
}
},
} }
} }
fn for_input(&mut self, field: String) { fn did_supply_field(&self) -> bool {
self.field = Some(field); self.field.is_some()
}
fn first_param(&self) -> &str {
let idx = if self.did_supply_field() { 1 } else { 0 };
self.get_param(idx)
}
fn second_param(&self) -> &str {
let idx = if self.did_supply_field() { 2 } else { 1 };
self.get_param(idx)
}
fn get_param(&self, idx: usize) -> &str {
self.params.as_ref().unwrap().get(idx).unwrap().as_str()
}
fn for_field(&mut self, field: &str) {
self.field = Some(String::from(field));
} }
fn permit(&mut self) -> bool { fn permit(&mut self) -> bool {
@ -49,6 +92,14 @@ impl Str {
self.error = Some(message.to_string()); self.error = Some(message.to_string());
} }
fn for_replace(&mut self, mode: ReplaceAction) {
if self.permit() {
self.action = Some(Action::Replace(mode));
} else {
self.log_error("can only apply one");
}
}
fn for_to_int(&mut self) { fn for_to_int(&mut self) {
if self.permit() { if self.permit() {
self.action = Some(Action::ToInteger); self.action = Some(Action::ToInteger);
@ -74,7 +125,7 @@ impl Str {
} }
fn usage(&self) -> &'static str { fn usage(&self) -> &'static str {
"Usage: str field [--downcase|--upcase|--to-int]" "Usage: str field [--downcase|--upcase|--to-int|--replace|--find-replace]"
} }
} }
@ -122,6 +173,8 @@ impl Plugin for Str {
named.insert("downcase".to_string(), NamedType::Switch); named.insert("downcase".to_string(), NamedType::Switch);
named.insert("upcase".to_string(), NamedType::Switch); named.insert("upcase".to_string(), NamedType::Switch);
named.insert("to-int".to_string(), NamedType::Switch); named.insert("to-int".to_string(), NamedType::Switch);
named.insert("replace".to_string(), NamedType::Switch);
named.insert("find-replace".to_string(), NamedType::Switch);
Ok(Signature { Ok(Signature {
name: "str".to_string(), name: "str".to_string(),
@ -133,37 +186,66 @@ impl Plugin for Str {
} }
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> { fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
if call_info.args.has("downcase") { let args = call_info.args;
if args.has("downcase") {
self.for_downcase(); self.for_downcase();
} }
if args.has("upcase") {
if call_info.args.has("upcase") {
self.for_upcase(); self.for_upcase();
} }
if args.has("to-int") {
if call_info.args.has("to-int") {
self.for_to_int(); self.for_to_int();
} }
if args.has("replace") {
self.for_replace(ReplaceAction::Direct);
}
if args.has("find-replace") {
self.for_replace(ReplaceAction::FindAndReplace);
}
if let Some(args) = call_info.args.positional { if let Some(possible_field) = args.nth(0) {
for arg in args { match possible_field {
match arg { Tagged {
Tagged { item: Value::Primitive(Primitive::String(s)),
item: Value::Primitive(Primitive::String(s)), ..
.. } => match self.action {
} => { Some(Action::Replace(ReplaceAction::Direct)) => {
self.for_input(s); if args.len() == 2 {
self.for_field(&s);
}
} }
_ => { Some(Action::Replace(ReplaceAction::FindAndReplace)) => {
return Err(ShellError::string(format!( if args.len() == 3 {
"Unrecognized type in params: {:?}", self.for_field(&s);
arg }
)))
} }
Some(Action::Downcase)
| Some(Action::Upcase)
| Some(Action::ToInteger)
| None => {
self.for_field(&s);
}
},
_ => {
return Err(ShellError::string(format!(
"Unrecognized type in params: {:?}",
possible_field
)))
} }
} }
} }
for param in args.positional_iter() {
match param {
Tagged {
item: Value::Primitive(Primitive::String(s)),
..
} => self.params.as_mut().unwrap().push(String::from(s)),
_ => {}
}
}
match &self.error { match &self.error {
Some(reason) => { Some(reason) => {
return Err(ShellError::string(format!("{}: {}", reason, self.usage()))) return Err(ShellError::string(format!("{}: {}", reason, self.usage())))
@ -184,13 +266,23 @@ fn main() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Str; use super::{Action, ReplaceAction, Str};
use indexmap::IndexMap; use indexmap::IndexMap;
use nu::{ use nu::{
CallInfo, EvaluatedArgs, Plugin, ReturnSuccess, SourceMap, Span, Tag, Tagged, CallInfo, EvaluatedArgs, Plugin, Primitive, ReturnSuccess, SourceMap, Span, Tag, Tagged,
TaggedDictBuilder, TaggedItem, Value, TaggedDictBuilder, TaggedItem, Value,
}; };
impl Str {
fn replace_with(&mut self, value: &str) {
self.params.as_mut().unwrap().push(value.to_string());
}
fn find_with(&mut self, search: &str) {
self.params.as_mut().unwrap().push(search.to_string());
}
}
struct CallStub { struct CallStub {
positionals: Vec<Tagged<Value>>, positionals: Vec<Tagged<Value>>,
flags: IndexMap<String, Tagged<Value>>, flags: IndexMap<String, Tagged<Value>>,
@ -227,19 +319,23 @@ mod tests {
} }
} }
fn sample_record(key: &str, value: &str) -> Tagged<Value> { fn structured_sample_record(key: &str, value: &str) -> Tagged<Value> {
let mut record = TaggedDictBuilder::new(Tag::unknown()); let mut record = TaggedDictBuilder::new(Tag::unknown());
record.insert(key.clone(), Value::string(value)); record.insert(key.clone(), Value::string(value));
record.into_tagged_value() record.into_tagged_value()
} }
fn unstructured_sample_record(value: &str) -> Tagged<Value> {
Tagged::from_item(Value::string(value), Tag::unknown())
}
#[test] #[test]
fn str_plugin_configuration_flags_wired() { fn str_plugin_configuration_flags_wired() {
let mut plugin = Str::new(); let mut plugin = Str::new();
let configured = plugin.config().unwrap(); let configured = plugin.config().unwrap();
for action_flag in &["downcase", "upcase", "to-int"] { for action_flag in &["downcase", "upcase", "to-int", "replace", "find-replace"] {
assert!(configured.named.get(*action_flag).is_some()); assert!(configured.named.get(*action_flag).is_some());
} }
} }
@ -251,7 +347,7 @@ mod tests {
assert!(plugin assert!(plugin
.begin_filter(CallStub::new().with_long_flag("downcase").create()) .begin_filter(CallStub::new().with_long_flag("downcase").create())
.is_ok()); .is_ok());
assert!(plugin.action.is_some()); assert_eq!(plugin.action.unwrap(), Action::Downcase);
} }
#[test] #[test]
@ -261,7 +357,7 @@ mod tests {
assert!(plugin assert!(plugin
.begin_filter(CallStub::new().with_long_flag("upcase").create()) .begin_filter(CallStub::new().with_long_flag("upcase").create())
.is_ok()); .is_ok());
assert!(plugin.action.is_some()); assert_eq!(plugin.action.unwrap(), Action::Upcase);
} }
#[test] #[test]
@ -271,7 +367,33 @@ mod tests {
assert!(plugin assert!(plugin
.begin_filter(CallStub::new().with_long_flag("to-int").create()) .begin_filter(CallStub::new().with_long_flag("to-int").create())
.is_ok()); .is_ok());
assert!(plugin.action.is_some()); assert_eq!(plugin.action.unwrap(), Action::ToInteger);
}
#[test]
fn str_plugin_accepts_replace() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(CallStub::new().with_long_flag("replace").create())
.is_ok());
assert_eq!(
plugin.action.unwrap(),
Action::Replace(ReplaceAction::Direct)
);
}
#[test]
fn str_plugin_accepts_find_replace() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(CallStub::new().with_long_flag("find-replace").create())
.is_ok());
assert_eq!(
plugin.action.unwrap(),
Action::Replace(ReplaceAction::FindAndReplace)
);
} }
#[test] #[test]
@ -327,7 +449,24 @@ mod tests {
} }
#[test] #[test]
fn str_plugin_applies_upcase() { fn str_replace() {
let mut strutils = Str::new();
strutils.for_replace(ReplaceAction::Direct);
strutils.replace_with("robalino");
assert_eq!(strutils.apply("andres"), Value::string("robalino"));
}
#[test]
fn str_find_replace() {
let mut strutils = Str::new();
strutils.for_replace(ReplaceAction::FindAndReplace);
strutils.find_with(r"kittens");
strutils.replace_with("jotandrehuda");
assert_eq!(strutils.apply("wykittens"), Value::string("wyjotandrehuda"));
}
#[test]
fn str_plugin_applies_upcase_with_field() {
let mut plugin = Str::new(); let mut plugin = Str::new();
assert!(plugin assert!(plugin
@ -339,7 +478,7 @@ mod tests {
) )
.is_ok()); .is_ok());
let subject = sample_record("name", "jotandrehuda"); let subject = structured_sample_record("name", "jotandrehuda");
let output = plugin.filter(subject).unwrap(); let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() { match output[0].as_ref().unwrap() {
@ -355,7 +494,27 @@ mod tests {
} }
#[test] #[test]
fn str_plugin_applies_downcase() { fn str_plugin_applies_upcase_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(CallStub::new().with_long_flag("upcase").create())
.is_ok());
let subject = unstructured_sample_record("jotandrehuda");
let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() {
ReturnSuccess::Value(Tagged {
item: Value::Primitive(Primitive::String(s)),
..
}) => assert_eq!(*s, String::from("JOTANDREHUDA")),
_ => {}
}
}
#[test]
fn str_plugin_applies_downcase_with_field() {
let mut plugin = Str::new(); let mut plugin = Str::new();
assert!(plugin assert!(plugin
@ -367,7 +526,7 @@ mod tests {
) )
.is_ok()); .is_ok());
let subject = sample_record("name", "JOTANDREHUDA"); let subject = structured_sample_record("name", "JOTANDREHUDA");
let output = plugin.filter(subject).unwrap(); let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() { match output[0].as_ref().unwrap() {
@ -383,7 +542,27 @@ mod tests {
} }
#[test] #[test]
fn str_plugin_applies_to_int() { fn str_plugin_applies_downcase_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(CallStub::new().with_long_flag("downcase").create())
.is_ok());
let subject = unstructured_sample_record("JOTANDREHUDA");
let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() {
ReturnSuccess::Value(Tagged {
item: Value::Primitive(Primitive::String(s)),
..
}) => assert_eq!(*s, String::from("jotandrehuda")),
_ => {}
}
}
#[test]
fn str_plugin_applies_to_int_with_field() {
let mut plugin = Str::new(); let mut plugin = Str::new();
assert!(plugin assert!(plugin
@ -395,7 +574,7 @@ mod tests {
) )
.is_ok()); .is_ok());
let subject = sample_record("Nu_birthday", "10"); let subject = structured_sample_record("Nu_birthday", "10");
let output = plugin.filter(subject).unwrap(); let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() { match output[0].as_ref().unwrap() {
@ -409,4 +588,134 @@ mod tests {
_ => {} _ => {}
} }
} }
#[test]
fn str_plugin_applies_to_int_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(CallStub::new().with_long_flag("to-int").create())
.is_ok());
let subject = unstructured_sample_record("10");
let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() {
ReturnSuccess::Value(Tagged {
item: Value::Primitive(Primitive::Int(i)),
..
}) => assert_eq!(*i, 10),
_ => {}
}
}
#[test]
fn str_plugin_applies_replace_with_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_parameter("rustconf")
.with_parameter("22nd August 2019")
.with_long_flag("replace")
.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(Tagged {
item: Value::Object(o),
..
}) => assert_eq!(
*o.get_data(&String::from("rustconf")).borrow(),
Value::string(String::from("22nd August 2019"))
),
_ => {}
}
}
#[test]
fn str_plugin_applies_replace_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_parameter("22nd August 2019")
.with_long_flag("replace")
.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(Tagged {
item: Value::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_parameter("kittens")
.with_parameter("jotandrehuda")
.with_long_flag("find-replace")
.create()
)
.is_ok());
let subject = structured_sample_record("staff", "wykittens");
let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() {
ReturnSuccess::Value(Tagged {
item: Value::Object(o),
..
}) => assert_eq!(
*o.get_data(&String::from("staff")).borrow(),
Value::string(String::from("wyjotandrehuda"))
),
_ => {}
}
}
#[test]
fn str_plugin_applies_find_replace_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_parameter("kittens")
.with_parameter("jotandrehuda")
.with_long_flag("find-replace")
.create()
)
.is_ok());
let subject = unstructured_sample_record("wykittens");
let output = plugin.filter(subject).unwrap();
match output[0].as_ref().unwrap() {
ReturnSuccess::Value(Tagged {
item: Value::Primitive(Primitive::String(s)),
..
}) => assert_eq!(*s, String::from("wyjotandrehuda")),
_ => {}
}
}
} }

View file

@ -11,24 +11,27 @@ fn can_only_apply_one() {
"open caco3_plastics.csv | first 1 | str origin --downcase --upcase" "open caco3_plastics.csv | first 1 | str origin --downcase --upcase"
); );
assert!(output.contains("Usage: str field [--downcase|--upcase|--to-int]")); assert!(
output.contains("Usage: str field [--downcase|--upcase|--to-int|--replace|--find-replace]")
);
} }
#[test] #[test]
fn acts_without_passing_field() { fn acts_without_passing_field() {
Playground::setup_for("plugin_str_test_without_passing_field") Playground::setup_for("plugin_str_acts_without_passing_field_test").with_files(vec![
.with_files(vec![FileWithContent( FileWithContent(
"sample.yml", "sample.yml",
r#" r#"
environment: environment:
global: global:
PROJECT_NAME: nushell PROJECT_NAME: nushell
"#, "#,
)]); ),
]);
nu!( nu!(
output, output,
cwd("tests/fixtures/nuplayground/plugin_str_test_without_passing_field"), cwd("tests/fixtures/nuplayground/plugin_str_acts_without_passing_field_test"),
"open sample.yml | get environment.global.PROJECT_NAME | str --upcase | echo $it" "open sample.yml | get environment.global.PROJECT_NAME | str --upcase | echo $it"
); );
@ -37,18 +40,17 @@ fn acts_without_passing_field() {
#[test] #[test]
fn downcases() { fn downcases() {
Playground::setup_for("plugin_str_test_downcases") Playground::setup_for("plugin_str_downcases_test").with_files(vec![FileWithContent(
.with_files(vec![FileWithContent( "sample.toml",
"sample.toml", r#"
r#" [dependency]
[dependency] name = "LIGHT"
name = "LIGHT" "#,
"#, )]);
)]);
nu!( nu!(
output, output,
cwd("tests/fixtures/nuplayground/plugin_str_test_downcases"), cwd("tests/fixtures/nuplayground/plugin_str_downcases_test"),
"open sample.toml | str dependency.name --downcase | get dependency.name | echo $it" "open sample.toml | str dependency.name --downcase | get dependency.name | echo $it"
); );
@ -57,18 +59,17 @@ fn downcases() {
#[test] #[test]
fn upcases() { fn upcases() {
Playground::setup_for("plugin_str_test_upcases") Playground::setup_for("plugin_str_upcases_test").with_files(vec![FileWithContent(
.with_files(vec![FileWithContent( "sample.toml",
"sample.toml", r#"
r#" [package]
[package] name = "nushell"
name = "nushell" "#,
"#, )]);
)]);
nu!( nu!(
output, output,
cwd("tests/fixtures/nuplayground/plugin_str_test_upcases"), cwd("tests/fixtures/nuplayground/plugin_str_upcases_test"),
"open sample.toml | str package.name --upcase | get package.name | echo $it" "open sample.toml | str package.name --upcase | get package.name | echo $it"
); );
@ -84,4 +85,63 @@ fn converts_to_int() {
); );
assert_eq!(output, "2509000000"); assert_eq!(output, "2509000000");
} }
#[test]
fn replaces() {
Playground::setup_for("plugin_str_replaces_test").with_files(vec![FileWithContent(
"sample.toml",
r#"
[package]
name = "nushell"
"#,
)]);
nu!(
output,
cwd("tests/fixtures/nuplayground/plugin_str_replaces_test"),
"open sample.toml | str package.name --replace wykittenshell | get package.name | echo $it"
);
assert_eq!(output, "wykittenshell");
}
#[test]
fn find_and_replaces() {
Playground::setup_for("plugin_str_find_and_replaces_test").with_files(vec![FileWithContent(
"sample.toml",
r#"
[fortune.teller]
phone = "1-800 KATZ"
"#,
)]);
nu!(
output,
cwd("tests/fixtures/nuplayground/plugin_str_find_and_replaces_test"),
"open sample.toml | str fortune.teller.phone --find-replace KATZ \"5289\" | get fortune.teller.phone | echo $it"
);
assert_eq!(output, "1-800 5289");
}
#[test]
fn find_and_replaces_without_passing_field() {
Playground::setup_for("plugin_str_find_and_replaces_without_passing_field_test").with_files(
vec![FileWithContent(
"sample.toml",
r#"
[fortune.teller]
phone = "1-800 KATZ"
"#,
)],
);
nu!(
output,
cwd("tests/fixtures/nuplayground/plugin_str_find_and_replaces_without_passing_field_test"),
"open sample.toml | get fortune.teller.phone | str --find-replace KATZ \"5289\" | echo $it"
);
assert_eq!(output, "1-800 5289");
}