diff --git a/Cargo.lock b/Cargo.lock index 9f8ebfe787..bed539c93a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,7 +1540,7 @@ dependencies = [ "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "roxmltree 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "rustyline 5.0.3 (git+https://github.com/kkawakam/rustyline.git)", + "rustyline 5.0.4 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde-hjson 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2077,8 +2077,8 @@ dependencies = [ [[package]] name = "rustyline" -version = "5.0.3" -source = "git+https://github.com/kkawakam/rustyline.git#449c811998f630102bb2d9fb0b59b890d9eabac5" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3056,7 +3056,7 @@ dependencies = [ "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" "checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -"checksum rustyline 5.0.3 (git+https://github.com/kkawakam/rustyline.git)" = "" +"checksum rustyline 5.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e9d8eb9912bc492db051324d36f5cea56984fc2afeaa5c6fa84e0b0e3cde550f" "checksum ryu 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "19d2271fa48eaf61e53cc88b4ad9adcbafa2d512c531e7fadb6dc11a4d3656c5" "checksum safemem 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b08423011dae9a5ca23f07cf57dac3857f5c885d352b76f6d95f4aea9434d0" "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" diff --git a/docs/commands/str.md b/docs/commands/str.md new file mode 100644 index 0000000000..b9ddc8c2e4 --- /dev/null +++ b/docs/commands/str.md @@ -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 # --to-int +━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + # │ │ 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 │ +━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` diff --git a/src/plugins/str.rs b/src/plugins/str.rs index 8260bdac2c..a0950b2ec0 100644 --- a/src/plugins/str.rs +++ b/src/plugins/str.rs @@ -2,12 +2,14 @@ use nu::{ serve_plugin, CallInfo, Plugin, Primitive, ReturnSuccess, ReturnValue, ShellError, Signature, SyntaxShape, Tagged, TaggedItem, Value, }; +use std::cmp; #[derive(Debug, Eq, PartialEq)] enum Action { Downcase, Upcase, ToInteger, + Substring(String), } pub type ColumnPath = Vec>; @@ -33,6 +35,26 @@ impl Str { let applied = match self.action.as_ref() { Some(Action::Downcase) => Value::string(input.to_ascii_lowercase()), Some(Action::Upcase) => Value::string(input.to_ascii_uppercase()), + Some(Action::Substring(s)) => { + // Index operator isn't perfect: https://users.rust-lang.org/t/how-to-get-a-substring-of-a-string/1351 + let no_spaces: String = s.chars().filter(|c| !c.is_whitespace()).collect(); + let v: Vec<&str> = no_spaces.split(',').collect(); + let start: usize = match v[0] { + "" => 0, + _ => v[0].parse().unwrap(), + }; + let end: usize = match v[1] { + "" => input.len(), + _ => cmp::min(v[1].parse().unwrap(), input.len()), + }; + if start > input.len() - 1 { + Value::string("") + } else if start > end { + Value::string(input) + } else { + Value::string(&input[start..end]) + } + } Some(Action::ToInteger) => match input.trim() { other => match other.parse::() { Ok(v) => Value::int(v), @@ -81,8 +103,16 @@ impl Str { } } + fn for_substring(&mut self, start_end: String) { + if self.permit() { + self.action = Some(Action::Substring(start_end)); + } else { + self.log_error("can only apply one"); + } + } + pub fn usage() -> &'static str { - "Usage: str field [--downcase|--upcase|--to-int]" + "Usage: str field [--downcase|--upcase|--to-int|--substring \"start,end\"]" } } @@ -132,6 +162,11 @@ impl Plugin for Str { .switch("downcase", "convert string to lowercase") .switch("upcase", "convert string to uppercase") .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") .filter()) } @@ -148,20 +183,34 @@ impl Plugin for Str { if args.has("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) { match possible_field { Tagged { item: Value::Primitive(Primitive::String(s)), 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 { item: Value::Table(_), .. @@ -177,7 +226,6 @@ impl Plugin for Str { } } } - for param in args.positional_iter() { match param { Tagged { @@ -232,6 +280,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 { self.flags.insert( name.to_string(), @@ -339,6 +395,7 @@ mod tests { .with_long_flag("upcase") .with_long_flag("downcase") .with_long_flag("to-int") + .with_long_flag("substring") .create(), ) .is_err()); @@ -509,4 +566,148 @@ 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_string_if_start_exceeds_end() { + let mut plugin = Str::new(); + + assert!(plugin + .begin_filter( + CallStub::new() + .with_named_parameter("substring", "3,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("0123456789")), + _ => {} + } + } }