replace and find-replace str plugin additions.

This commit is contained in:
Andrés N. Robalino 2019-11-26 19:03:22 -05:00
parent eb5d0d295b
commit 8cedd2ee5b
3 changed files with 372 additions and 37 deletions

View file

@ -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",

View file

@ -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<Tagged<ColumnPath>>,
params: Option<Vec<String>>,
error: Option<String>,
action: Option<Action>,
}
@ -25,7 +32,6 @@ impl Str {
fn new() -> Str {
Str {
field: None,
params: Some(Vec::<String>::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::<i64>() {
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<String>) -> Value {
UntaggedValue::string(input.into()).into_untagged_value()
}
fn table(list: &Vec<Value>) -> Value {
UntaggedValue::table(list).into_untagged_value()
}
fn column_path(paths: &Vec<Value>) -> Value {
UntaggedValue::Primitive(Primitive::ColumnPath(
table(&paths.iter().cloned().collect())
.as_column_path()
.unwrap()
.item,
))
.into_untagged_value()
}
struct CallStub {
positionals: Vec<Value>,
flags: IndexMap<String, Value>,
@ -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")),
_ => {}
}
}
}

View file

@ -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");
})
}