From 5e0a9aecaa3a2b7becbd2063411c631b1e345fc9 Mon Sep 17 00:00:00 2001 From: k-brk <25877802+k-brk@users.noreply.github.com> Date: Sun, 26 Jul 2020 20:09:35 +0200 Subject: [PATCH] ltrim and rtrim for string (#2262) * Trim string from left and right * Move trim to folder * fmt * Clippy --- crates/nu-cli/src/cli.rs | 2 + crates/nu-cli/src/commands.rs | 3 +- crates/nu-cli/src/commands/str_/from.rs | 3 +- crates/nu-cli/src/commands/str_/mod.rs | 4 +- crates/nu-cli/src/commands/str_/trim.rs | 168 ------------------ crates/nu-cli/src/commands/str_/trim/mod.rs | 81 +++++++++ .../src/commands/str_/trim/trim_both_ends.rs | 94 ++++++++++ .../src/commands/str_/trim/trim_left.rs | 94 ++++++++++ .../src/commands/str_/trim/trim_right.rs | 95 ++++++++++ 9 files changed, 372 insertions(+), 172 deletions(-) delete mode 100644 crates/nu-cli/src/commands/str_/trim.rs create mode 100644 crates/nu-cli/src/commands/str_/trim/mod.rs create mode 100644 crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs create mode 100644 crates/nu-cli/src/commands/str_/trim/trim_left.rs create mode 100644 crates/nu-cli/src/commands/str_/trim/trim_right.rs diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index dd74893f73..f8b2611332 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -311,6 +311,8 @@ pub fn create_default_context( whole_stream_command(StrSet), whole_stream_command(StrToDatetime), whole_stream_command(StrTrim), + whole_stream_command(StrTrimLeft), + whole_stream_command(StrTrimRight), 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 9bf4c647f3..118a402f2a 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -229,7 +229,8 @@ 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, StrUpcase, + StrSet, 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_/from.rs b/crates/nu-cli/src/commands/str_/from.rs index 67b605f726..12e4e73a40 100644 --- a/crates/nu-cli/src/commands/str_/from.rs +++ b/crates/nu-cli/src/commands/str_/from.rs @@ -1,4 +1,3 @@ -use crate::commands::str_::trim::trim_char; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; @@ -196,7 +195,7 @@ fn format_decimal(mut decimal: BigDecimal, digits: Option, group_digits: bo .take(n as usize) .collect() } else { - trim_char(&dec_part, '0', false, true) + String::from(dec_part.trim_end_matches('0')) }; let format_default_loc = |int_part: BigInt| { diff --git a/crates/nu-cli/src/commands/str_/mod.rs b/crates/nu-cli/src/commands/str_/mod.rs index 759d2e38cc..64266c0911 100644 --- a/crates/nu-cli/src/commands/str_/mod.rs +++ b/crates/nu-cli/src/commands/str_/mod.rs @@ -27,5 +27,7 @@ pub use substring::SubCommand as StrSubstring; pub use to_datetime::SubCommand as StrToDatetime; pub use to_decimal::SubCommand as StrToDecimal; pub use to_integer::SubCommand as StrToInteger; -pub use trim::SubCommand as StrTrim; +pub use trim::Trim as StrTrim; +pub use trim::TrimLeft as StrTrimLeft; +pub use trim::TrimRight as StrTrimRight; pub use upcase::SubCommand as StrUpcase; diff --git a/crates/nu-cli/src/commands/str_/trim.rs b/crates/nu-cli/src/commands/str_/trim.rs deleted file mode 100644 index 03e4883b08..0000000000 --- a/crates/nu-cli/src/commands/str_/trim.rs +++ /dev/null @@ -1,168 +0,0 @@ -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 { - rest: Vec, - #[serde(rename(deserialize = "char"))] - char_: Option>, -} - -pub struct SubCommand; - -#[async_trait] -impl WholeStreamCommand for SubCommand { - fn name(&self) -> &str { - "str trim" - } - - fn signature(&self) -> Signature { - Signature::build("str trim") - .rest( - SyntaxShape::ColumnPath, - "optionally trim text by column paths", - ) - .named( - "char", - SyntaxShape::String, - "character to trim (default: whitespace)", - Some('c'), - ) - } - - fn usage(&self) -> &str { - "trims text" - } - - async fn run( - &self, - args: CommandArgs, - registry: &CommandRegistry, - ) -> Result { - operate(args, registry).await - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Trim whitespace", - example: "echo 'Nu shell ' | str trim", - result: Some(vec![Value::from("Nu shell")]), - }, - Example { - description: "Trim a specific character", - example: "echo '=== Nu shell ===' | str trim -c '=' | str trim", - result: Some(vec![Value::from("Nu shell")]), - }, - ] - } -} - -async fn operate( - args: CommandArgs, - registry: &CommandRegistry, -) -> Result { - let registry = registry.clone(); - - let (Arguments { rest, char_ }, input) = args.process(®istry).await?; - - let column_paths: Vec<_> = rest; - let to_trim = char_.map(|tagged| tagged.item); - - Ok(input - .map(move |v| { - if column_paths.is_empty() { - ReturnSuccess::value(action(&v, v.tag(), to_trim)?) - } else { - let mut ret = v; - - for path in &column_paths { - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| action(old, old.tag(), to_trim)), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .to_output_stream()) -} - -fn action(input: &Value, tag: impl Into, char_: Option) -> Result { - match &input.value { - UntaggedValue::Primitive(Primitive::Line(s)) - | UntaggedValue::Primitive(Primitive::String(s)) => { - Ok(UntaggedValue::string(match char_ { - None => String::from(s.trim()), - Some(ch) => trim_char(s, ch, true, true), - }) - .into_value(tag)) - } - other => { - let got = format!("got {}", other.type_name()); - Err(ShellError::labeled_error( - "value is not string", - got, - tag.into().span, - )) - } - } -} - -pub fn trim_char(from: &str, to_trim: char, leading: bool, trailing: bool) -> String { - let mut trimmed = String::from(""); - let mut backlog = String::from(""); - let mut at_left = true; - from.chars().for_each(|ch| match ch { - c if c == to_trim => { - if !(leading && at_left) { - if trailing { - backlog.push(c) - } else { - trimmed.push(c) - } - } - } - other => { - at_left = false; - if trailing { - trimmed.push_str(backlog.as_str()); - backlog = String::from(""); - } - trimmed.push(other); - } - }); - - trimmed -} - -#[cfg(test)] -mod tests { - use super::{action, SubCommand}; - use nu_plugin::test_helpers::value::string; - use nu_source::Tag; - - #[test] - fn examples_work_as_expected() { - use crate::examples::test as test_examples; - - test_examples(SubCommand {}) - } - - #[test] - fn trims() { - let word = string("andres "); - let expected = string("andres"); - - let actual = action(&word, Tag::unknown(), None).unwrap(); - assert_eq!(actual, expected); - } -} diff --git a/crates/nu-cli/src/commands/str_/trim/mod.rs b/crates/nu-cli/src/commands/str_/trim/mod.rs new file mode 100644 index 0000000000..78da9f055e --- /dev/null +++ b/crates/nu-cli/src/commands/str_/trim/mod.rs @@ -0,0 +1,81 @@ +mod trim_both_ends; +mod trim_left; +mod trim_right; + +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::ShellTypeName; +use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, UntaggedValue, Value}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +pub use trim_both_ends::SubCommand as Trim; +pub use trim_left::SubCommand as TrimLeft; +pub use trim_right::SubCommand as TrimRight; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, + #[serde(rename(deserialize = "char"))] + char_: Option>, +} + +pub async fn operate( + args: CommandArgs, + registry: &CommandRegistry, + trim_operation: &'static F, +) -> Result +where + F: Fn(&str, Option) -> String + Send + Sync + 'static, +{ + let registry = registry.clone(); + + let (Arguments { rest, char_ }, input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest; + let to_trim = char_.map(|tagged| tagged.item); + + Ok(input + .map(move |v| { + if column_paths.is_empty() { + ReturnSuccess::value(action(&v, v.tag(), to_trim, &trim_operation)?) + } else { + let mut ret = v; + + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, old.tag(), to_trim, &trim_operation)), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +} + +pub fn action( + input: &Value, + tag: impl Into, + char_: Option, + trim_operation: &F, +) -> Result +where + F: Fn(&str, Option) -> String + Send + Sync + 'static, +{ + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(UntaggedValue::string(trim_operation(s, char_)).into_value(tag)) + } + other => { + let got = format!("got {}", other.type_name()); + Err(ShellError::labeled_error( + "value is not string", + got, + tag.into().span, + )) + } + } +} diff --git a/crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs b/crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs new file mode 100644 index 0000000000..8eaf200fc5 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/trim/trim_both_ends.rs @@ -0,0 +1,94 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str trim" + } + + fn signature(&self) -> Signature { + Signature::build("str trim") + .rest( + SyntaxShape::ColumnPath, + "optionally trim text by column paths", + ) + .named( + "char", + SyntaxShape::String, + "character to trim (default: whitespace)", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "trims text" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &trim).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Trim whitespace", + example: "echo 'Nu shell ' | str trim", + result: Some(vec![Value::from("Nu shell")]), + }, + Example { + description: "Trim a specific character", + example: "echo '=== Nu shell ===' | str trim -c '=' | str trim", + result: Some(vec![Value::from("Nu shell")]), + }, + ] + } +} +fn trim(s: &str, char_: Option) -> String { + match char_ { + None => String::from(s.trim()), + Some(ch) => String::from(s.trim_matches(ch)), + } +} + +#[cfg(test)] +mod tests { + use super::{trim, SubCommand}; + use crate::commands::str_::trim::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn trims() { + let word = string("andres "); + let expected = string("andres"); + + let actual = action(&word, Tag::unknown(), None, &trim).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_character_both_ends() { + let word = string("!#andres#!"); + let expected = string("#andres#"); + + let actual = action(&word, Tag::unknown(), Some('!'), &trim).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/trim/trim_left.rs b/crates/nu-cli/src/commands/str_/trim/trim_left.rs new file mode 100644 index 0000000000..ae54b1e063 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/trim/trim_left.rs @@ -0,0 +1,94 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str ltrim" + } + + fn signature(&self) -> Signature { + Signature::build("str ltrim") + .rest( + SyntaxShape::ColumnPath, + "optionally trim text starting from the beginning by column paths", + ) + .named( + "char", + SyntaxShape::String, + "character to trim (default: whitespace)", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "trims whitespace or character from the beginning of text" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &trim_left).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Trim whitespace from the beginning of string", + example: "echo ' Nu shell ' | str ltrim", + result: Some(vec![Value::from("Nu shell ")]), + }, + Example { + description: "Trim a specific character", + example: "echo '=== Nu shell ===' | str ltrim -c '='", + result: Some(vec![Value::from(" Nu shell ===")]), + }, + ] + } +} + +fn trim_left(s: &str, char_: Option) -> String { + match char_ { + None => String::from(s.trim_start()), + Some(ch) => String::from(s.trim_start_matches(ch)), + } +} + +#[cfg(test)] +mod tests { + use super::{trim_left, SubCommand}; + use crate::commands::str_::trim::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn trims_whitespace_from_left() { + let word = string(" andres "); + let expected = string("andres "); + + let actual = action(&word, Tag::unknown(), None, &trim_left).unwrap(); + assert_eq!(actual, expected); + } + #[test] + fn trims_custom_chars_from_left() { + let word = string("!!! andres !!!"); + let expected = string(" andres !!!"); + + let actual = action(&word, Tag::unknown(), Some('!'), &trim_left).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/trim/trim_right.rs b/crates/nu-cli/src/commands/str_/trim/trim_right.rs new file mode 100644 index 0000000000..bc874ec8c3 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/trim/trim_right.rs @@ -0,0 +1,95 @@ +use super::operate; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, Value}; + +pub struct SubCommand; + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str rtrim" + } + + fn signature(&self) -> Signature { + Signature::build("str rtrim") + .rest( + SyntaxShape::ColumnPath, + "optionally trim text starting from the end by column paths", + ) + .named( + "char", + SyntaxShape::String, + "character to trim (default: whitespace)", + Some('c'), + ) + } + + fn usage(&self) -> &str { + "trims whitespace or character from the end of text" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry, &trim_right).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Trim whitespace from the end of string", + example: "echo ' Nu shell ' | str rtrim", + result: Some(vec![Value::from(" Nu shell")]), + }, + Example { + description: "Trim a specific character", + example: "echo '=== Nu shell ===' | str rtrim -c '='", + result: Some(vec![Value::from("=== Nu shell ")]), + }, + ] + } +} + +fn trim_right(s: &str, char_: Option) -> String { + match char_ { + None => String::from(s.trim_end()), + Some(ch) => String::from(s.trim_end_matches(ch)), + } +} + +#[cfg(test)] +mod tests { + use super::{trim_right, SubCommand}; + use crate::commands::str_::trim::action; + use nu_plugin::test_helpers::value::string; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn trims_whitespace_from_right() { + let word = string(" andres "); + let expected = string(" andres"); + + let actual = action(&word, Tag::unknown(), None, &trim_right).unwrap(); + assert_eq!(actual, expected); + } + + #[test] + fn trims_custom_chars_from_right() { + let word = string("#@! andres !@#"); + let expected = string("#@! andres !@"); + + let actual = action(&word, Tag::unknown(), Some('#'), &trim_right).unwrap(); + assert_eq!(actual, expected); + } +}