Merge pull request #895 from Flare576/substring

Adds new substring function to str plugin
This commit is contained in:
Jonathan Turner 2019-11-02 17:42:45 +13:00 committed by GitHub
commit 51879d022e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 256 additions and 10 deletions

50
docs/commands/str.md Normal file
View File

@ -0,0 +1,50 @@
# str
Consumes either a single value or a table and converts the provided data to a string and optionally applies a change.
## Examples
```shell
> shells
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /home/TUX/stuff/expr/stuff
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str path --upcase
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /HOME/TUX/STUFF/EXPR/STUFF
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str path --downcase
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ /home/tux/stuff/expr/stuff
1 │ │ filesystem │ /
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str # --substring "21, 99"
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ stuff
1 │ │ filesystem │
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> shells | str # --substring "6,"
━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# │ │ name │ path
───┼───┼────────────┼────────────────────────────────
0 │ X │ filesystem │ TUX/stuff/expr/stuff
1 │ │ filesystem │
━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
> echo "1, 2, 3" | split-row "," | str --to-int | sum
━━━━━━━━━
<value>
─────────
6
━━━━━━━━━
```

View File

@ -2,12 +2,14 @@ use nu::{
did_you_mean, serve_plugin, tag_for_tagged_list, CallInfo, Plugin, Primitive, ReturnSuccess, did_you_mean, serve_plugin, tag_for_tagged_list, CallInfo, Plugin, Primitive, ReturnSuccess,
ReturnValue, ShellError, Signature, SyntaxShape, Tagged, TaggedItem, Value, ReturnValue, ShellError, Signature, SyntaxShape, Tagged, TaggedItem, Value,
}; };
use std::cmp;
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
enum Action { enum Action {
Downcase, Downcase,
Upcase, Upcase,
ToInteger, ToInteger,
Substring(usize, usize),
} }
pub type ColumnPath = Vec<Tagged<String>>; pub type ColumnPath = Vec<Tagged<String>>;
@ -33,6 +35,17 @@ impl Str {
let applied = match self.action.as_ref() { let applied = match self.action.as_ref() {
Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()), Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()),
Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()), Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()),
Some(Action::Substring(s, e)) => {
let end: usize = cmp::min(*e, input.len());
let start: usize = *s;
if start > input.len() - 1 {
Value::string("")
} else {
// Index operator isn't perfect:
// https://users.rust-lang.org/t/how-to-get-a-substring-of-a-string/1351
Value::string(&input[start..end])
}
}
Some(Action::ToInteger) => match input.trim() { Some(Action::ToInteger) => match input.trim() {
other => match other.parse::<i64>() { other => match other.parse::<i64>() {
Ok(v) => Value::int(v), Ok(v) => Value::int(v),
@ -81,8 +94,27 @@ impl Str {
} }
} }
fn for_substring(&mut self, s: String) {
let v: Vec<&str> = s.split(',').collect();
let start: usize = match v[0] {
"" => 0,
_ => v[0].trim().parse().unwrap(),
};
let end: usize = match v[1] {
"" => usize::max_value().clone(),
_ => v[1].trim().parse().unwrap(),
};
if start > end {
self.log_error("End must be greater than or equal to Start");
} else if self.permit() {
self.action = Some(Action::Substring(start, end));
} else {
self.log_error("can only apply one");
}
}
pub fn usage() -> &'static str { pub fn usage() -> &'static str {
"Usage: str field [--downcase|--upcase|--to-int]" "Usage: str field [--downcase|--upcase|--to-int|--substring \"start,end\"]"
} }
} }
@ -167,6 +199,11 @@ impl Plugin for Str {
.switch("downcase", "convert string to lowercase") .switch("downcase", "convert string to lowercase")
.switch("upcase", "convert string to uppercase") .switch("upcase", "convert string to uppercase")
.switch("to-int", "convert string to integer") .switch("to-int", "convert string to integer")
.named(
"substring",
SyntaxShape::String,
"convert string to portion of original, requires \"start,end\"",
)
.rest(SyntaxShape::ColumnPath, "the column(s) to convert") .rest(SyntaxShape::ColumnPath, "the column(s) to convert")
.filter()) .filter())
} }
@ -183,20 +220,34 @@ impl Plugin for Str {
if args.has("to-int") { if args.has("to-int") {
self.for_to_int(); self.for_to_int();
} }
if args.has("substring") {
if let Some(start_end) = args.get("substring") {
match start_end {
Tagged {
item: Value::Primitive(Primitive::String(s)),
..
} => {
self.for_substring(s.to_string());
}
_ => {
return Err(ShellError::labeled_error(
"Unrecognized type in params",
start_end.type_name(),
&start_end.tag,
))
}
}
}
}
if let Some(possible_field) = args.nth(0) { if let Some(possible_field) = args.nth(0) {
match possible_field { match possible_field {
Tagged { Tagged {
item: Value::Primitive(Primitive::String(s)), item: Value::Primitive(Primitive::String(s)),
tag, tag,
} => match self.action { } => {
Some(Action::Downcase)
| Some(Action::Upcase)
| Some(Action::ToInteger)
| None => {
self.for_field(vec![s.clone().tagged(tag)]); self.for_field(vec![s.clone().tagged(tag)]);
} }
},
table @ Tagged { table @ Tagged {
item: Value::Table(_), item: Value::Table(_),
.. ..
@ -212,7 +263,6 @@ impl Plugin for Str {
} }
} }
} }
for param in args.positional_iter() { for param in args.positional_iter() {
match param { match param {
Tagged { Tagged {
@ -267,6 +317,14 @@ mod tests {
} }
} }
fn with_named_parameter(&mut self, name: &str, value: &str) -> &mut Self {
self.flags.insert(
name.to_string(),
Value::string(value).tagged(Tag::unknown()),
);
self
}
fn with_long_flag(&mut self, name: &str) -> &mut Self { fn with_long_flag(&mut self, name: &str) -> &mut Self {
self.flags.insert( self.flags.insert(
name.to_string(), name.to_string(),
@ -374,6 +432,7 @@ mod tests {
.with_long_flag("upcase") .with_long_flag("upcase")
.with_long_flag("downcase") .with_long_flag("downcase")
.with_long_flag("to-int") .with_long_flag("to-int")
.with_long_flag("substring")
.create(), .create(),
) )
.is_err()); .is_err());
@ -544,4 +603,141 @@ mod tests {
_ => {} _ => {}
} }
} }
#[test]
fn str_plugin_applies_substring_without_field() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", "0,1")
.create()
)
.is_ok());
let subject = unstructured_sample_record("0123456789");
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("0")),
_ => {}
}
}
#[test]
fn str_plugin_applies_substring_exceeding_string_length() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", "0,11")
.create()
)
.is_ok());
let subject = unstructured_sample_record("0123456789");
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("0123456789")),
_ => {}
}
}
#[test]
fn str_plugin_applies_substring_returns_blank_if_start_exceeds_length() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", "20,30")
.create()
)
.is_ok());
let subject = unstructured_sample_record("0123456789");
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("")),
_ => {}
}
}
#[test]
fn str_plugin_applies_substring_treats_blank_start_as_zero() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", ",5")
.create()
)
.is_ok());
let subject = unstructured_sample_record("0123456789");
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("01234")),
_ => {}
}
}
#[test]
fn str_plugin_applies_substring_treats_blank_end_as_length() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", "2,")
.create()
)
.is_ok());
let subject = unstructured_sample_record("0123456789");
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("23456789")),
_ => {}
}
}
#[test]
fn str_plugin_applies_substring_returns_error_if_start_exceeds_end() {
let mut plugin = Str::new();
assert!(plugin
.begin_filter(
CallStub::new()
.with_named_parameter("substring", "3,1")
.create()
)
.is_err());
assert_eq!(
plugin.error,
Some("End must be greater than or equal to Start".to_string())
);
}
} }