From 18a4505b9bde5eca7a935d3a8b0828aab9cc7420 Mon Sep 17 00:00:00 2001 From: k-brk <25877802+k-brk@users.noreply.github.com> Date: Thu, 30 Jul 2020 06:51:20 +0200 Subject: [PATCH] starts_with ends_with match functions for string (#2269) --- crates/nu-cli/src/cli.rs | 2 + crates/nu-cli/src/commands.rs | 6 +- crates/nu-cli/src/commands/str_/ends_with.rs | 138 ++++++++++++++++++ crates/nu-cli/src/commands/str_/mod.rs | 4 + .../nu-cli/src/commands/str_/starts_with.rs | 138 ++++++++++++++++++ 5 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 crates/nu-cli/src/commands/str_/ends_with.rs create mode 100644 crates/nu-cli/src/commands/str_/starts_with.rs diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 967d66c342..9423f9fc6d 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -313,6 +313,8 @@ pub fn create_default_context( whole_stream_command(StrTrim), whole_stream_command(StrTrimLeft), whole_stream_command(StrTrimRight), + whole_stream_command(StrStartsWith), + whole_stream_command(StrEndsWith), whole_stream_command(StrCollect), whole_stream_command(StrLength), whole_stream_command(StrReverse), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index f46b044f7b..23b7bbcc16 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -229,9 +229,9 @@ pub(crate) use sort_by::SortBy; pub(crate) use split::{Split, SplitChars, SplitColumn, SplitRow}; pub(crate) use split_by::SplitBy; pub(crate) use str_::{ - Str, StrCapitalize, StrCollect, StrDowncase, StrFindReplace, StrFrom, StrLength, StrReverse, - StrSet, StrSubstring, StrToDatetime, StrToDecimal, StrToInteger, StrTrim, StrTrimLeft, - StrTrimRight, StrUpcase, + Str, StrCapitalize, StrCollect, StrDowncase, StrEndsWith, StrFindReplace, StrFrom, StrLength, + StrReverse, StrSet, StrStartsWith, StrSubstring, StrToDatetime, StrToDecimal, StrToInteger, + StrTrim, StrTrimLeft, StrTrimRight, StrUpcase, }; pub(crate) use table::Table; pub(crate) use tags::Tags; diff --git a/crates/nu-cli/src/commands/str_/ends_with.rs b/crates/nu-cli/src/commands/str_/ends_with.rs new file mode 100644 index 0000000000..642ef38adc --- /dev/null +++ b/crates/nu-cli/src/commands/str_/ends_with.rs @@ -0,0 +1,138 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{ + ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + pattern: Tagged, + rest: Vec, +} +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str ends-with" + } + + fn signature(&self) -> Signature { + Signature::build("str ends-with") + .required("pattern", SyntaxShape::String, "the pattern to match") + .rest( + SyntaxShape::ColumnPath, + "optionally matches suffix of text by column paths", + ) + } + + fn usage(&self) -> &str { + "checks if string ends with pattern" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Checks if string ends with '.rb' pattern", + example: "echo 'my_library.rb' | str ends-with '.rb'", + result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), + }] + } +} + +async fn operate( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + + let (Arguments { pattern, rest }, input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest; + + Ok(input + .map(move |v| { + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, &pattern, v.tag())?) + } else { + let mut ret = v; + + for path in &column_paths { + let pattern = pattern.clone(); + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, &pattern, old.tag())), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +fn action(input: &Value, pattern: &str, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let ends_with = s.ends_with(pattern); + Ok(UntaggedValue::boolean(ends_with).into_value(tag)) + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_plugin::test_helpers::value::string; + use nu_protocol::{Primitive, UntaggedValue}; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn str_ends_with_pattern() { + let word = string("Cargo.toml"); + let pattern = ".toml"; + let expected = + UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value(); + + let actual = action(&word, &pattern, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn str_does_not_end_with_pattern() { + let word = string("Cargo.toml"); + let pattern = "Car"; + let expected = + UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value(); + + let actual = action(&word, &pattern, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/mod.rs b/crates/nu-cli/src/commands/str_/mod.rs index 64266c0911..8ecc87ecb7 100644 --- a/crates/nu-cli/src/commands/str_/mod.rs +++ b/crates/nu-cli/src/commands/str_/mod.rs @@ -2,11 +2,13 @@ mod capitalize; mod collect; mod command; mod downcase; +mod ends_with; mod find_replace; mod from; mod length; mod reverse; mod set; +mod starts_with; mod substring; mod to_datetime; mod to_decimal; @@ -18,11 +20,13 @@ pub use capitalize::SubCommand as StrCapitalize; pub use collect::SubCommand as StrCollect; pub use command::Command as Str; pub use downcase::SubCommand as StrDowncase; +pub use ends_with::SubCommand as StrEndsWith; pub use find_replace::SubCommand as StrFindReplace; pub use from::SubCommand as StrFrom; pub use length::SubCommand as StrLength; pub use reverse::SubCommand as StrReverse; pub use set::SubCommand as StrSet; +pub use starts_with::SubCommand as StrStartsWith; pub use substring::SubCommand as StrSubstring; pub use to_datetime::SubCommand as StrToDatetime; pub use to_decimal::SubCommand as StrToDecimal; diff --git a/crates/nu-cli/src/commands/str_/starts_with.rs b/crates/nu-cli/src/commands/str_/starts_with.rs new file mode 100644 index 0000000000..13689dd8ac --- /dev/null +++ b/crates/nu-cli/src/commands/str_/starts_with.rs @@ -0,0 +1,138 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{ + ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value, +}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + pattern: Tagged, + rest: Vec, +} +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str starts-with" + } + + fn signature(&self) -> Signature { + Signature::build("str starts-with") + .required("pattern", SyntaxShape::String, "the pattern to match") + .rest( + SyntaxShape::ColumnPath, + "optionally matches prefix of text by column paths", + ) + } + + fn usage(&self) -> &str { + "checks if string starts with pattern" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Checks if string starts with 'my' pattern", + example: "echo 'my_library.rb' | str starts-with 'my'", + result: Some(vec![UntaggedValue::boolean(true).into_untagged_value()]), + }] + } +} + +async fn operate( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let registry = registry.clone(); + + let (Arguments { pattern, rest }, input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest; + + Ok(input + .map(move |v| { + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, &pattern, v.tag())?) + } else { + let mut ret = v; + + for path in &column_paths { + let pattern = pattern.clone(); + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, &pattern, old.tag())), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +fn action(input: &Value, pattern: &str, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let starts_with = s.starts_with(pattern); + Ok(UntaggedValue::boolean(starts_with).into_value(tag)) + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::{action, SubCommand}; + use nu_plugin::test_helpers::value::string; + use nu_protocol::{Primitive, UntaggedValue}; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn str_starts_with_pattern() { + let word = string("Cargo.toml"); + let pattern = "Car"; + let expected = + UntaggedValue::Primitive(Primitive::Boolean(true.into())).into_untagged_value(); + + let actual = action(&word, &pattern, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn str_does_not_start_with_pattern() { + let word = string("Cargo.toml"); + let pattern = ".toml"; + let expected = + UntaggedValue::Primitive(Primitive::Boolean(false.into())).into_untagged_value(); + + let actual = action(&word, &pattern, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +}