diff --git a/Cargo.lock b/Cargo.lock index e4ab4e7bbe..f64b3f66c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2164,7 +2164,6 @@ dependencies = [ "nu_plugin_post", "nu_plugin_ps", "nu_plugin_start", - "nu_plugin_str", "nu_plugin_sys", "nu_plugin_textview", "nu_plugin_tree", @@ -2528,22 +2527,6 @@ dependencies = [ "url", ] -[[package]] -name = "nu_plugin_str" -version = "0.14.1" -dependencies = [ - "bigdecimal", - "chrono", - "nu-build", - "nu-errors", - "nu-plugin", - "nu-protocol", - "nu-source", - "nu-value-ext", - "num-bigint", - "regex", -] - [[package]] name = "nu_plugin_sys" version = "0.14.1" diff --git a/Cargo.toml b/Cargo.toml index aa3419912b..671341589c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ nu_plugin_parse = { version = "0.14.1", path = "./crates/nu_plugin_parse", optio nu_plugin_post = { version = "0.14.1", path = "./crates/nu_plugin_post", optional=true } nu_plugin_ps = { version = "0.14.1", path = "./crates/nu_plugin_ps", optional=true } nu_plugin_start = { version = "0.1.0", path = "./crates/nu_plugin_start", optional=true } -nu_plugin_str = { version = "0.14.1", path = "./crates/nu_plugin_str", optional=true } nu_plugin_sys = { version = "0.14.1", path = "./crates/nu_plugin_sys", optional=true } nu_plugin_textview = { version = "0.14.1", path = "./crates/nu_plugin_textview", optional=true } nu_plugin_tree = { version = "0.14.1", path = "./crates/nu_plugin_tree", optional=true } @@ -60,7 +59,7 @@ serde = { version = "1.0.110", features = ["derive"] } nu-build = { version = "0.14.1", path = "./crates/nu-build" } [features] -default = ["sys", "ps", "textview", "inc", "str"] +default = ["sys", "ps", "textview", "inc"] stable = ["default", "starship-prompt", "binaryview", "match", "tree", "average", "parse", "post", "fetch", "clipboard-cli", "trash-support", "start"] # Default @@ -68,7 +67,6 @@ textview = ["crossterm", "syntect", "url", "nu_plugin_textview"] sys = ["nu_plugin_sys"] ps = ["nu_plugin_ps"] inc = ["semver", "nu_plugin_inc"] -str = ["nu_plugin_str"] # Stable average = ["nu_plugin_average"] @@ -103,11 +101,6 @@ name = "nu_plugin_core_ps" path = "src/plugins/nu_plugin_core_ps.rs" required-features = ["ps"] -[[bin]] -name = "nu_plugin_core_str" -path = "src/plugins/nu_plugin_core_str.rs" -required-features = ["str"] - [[bin]] name = "nu_plugin_core_sys" path = "src/plugins/nu_plugin_core_sys.rs" diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index e72e837cff..4d6edc670f 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -285,6 +285,17 @@ pub fn create_default_context( whole_stream_command(Lines), whole_stream_command(Trim), whole_stream_command(Echo), + whole_stream_command(Str), + whole_stream_command(StrToFloat), + whole_stream_command(StrToInteger), + whole_stream_command(StrDowncase), + whole_stream_command(StrUpcase), + whole_stream_command(StrCapitalize), + whole_stream_command(StrFindReplace), + whole_stream_command(StrSubstring), + whole_stream_command(StrSet), + whole_stream_command(StrToDatetime), + whole_stream_command(StrTrim), whole_stream_command(BuildString), // Column manipulation whole_stream_command(Reject), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 4dd14359e8..0279d40bf0 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -98,6 +98,7 @@ pub(crate) mod skip_while; pub(crate) mod sort_by; pub(crate) mod split; pub(crate) mod split_by; +pub(crate) mod str_; pub(crate) mod sum; #[allow(unused)] pub(crate) mod t_sort_by; @@ -225,6 +226,10 @@ pub(crate) use split::Split; pub(crate) use split::SplitColumn; pub(crate) use split::SplitRow; pub(crate) use split_by::SplitBy; +pub(crate) use str_::{ + Str, StrCapitalize, StrDowncase, StrFindReplace, StrSet, StrSubstring, StrToDatetime, + StrToFloat, StrToInteger, StrTrim, StrUpcase, +}; pub(crate) use sum::Sum; #[allow(unused_imports)] pub(crate) use t_sort_by::TSortBy; diff --git a/crates/nu-cli/src/commands/str_/capitalize.rs b/crates/nu-cli/src/commands/str_/capitalize.rs new file mode 100644 index 0000000000..d2f133e001 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/capitalize.rs @@ -0,0 +1,144 @@ +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; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str capitalize" + } + + fn signature(&self) -> Signature { + Signature::build("str capitalize").rest( + SyntaxShape::ColumnPath, + "optionally capitalize text by column paths", + ) + } + + fn usage(&self) -> &str { + "capitalizes text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Capitalize contents", + example: "echo 'good day' | str capitalize", + result: Some(vec![Value::from("Good day")]), + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let mut capitalized = String::new(); + + for (idx, character) in s.chars().enumerate() { + let out = if idx == 0 { + character.to_uppercase().to_string() + } else { + character.to_lowercase().to_string() + }; + + capitalized.push_str(&out); + } + + Ok(UntaggedValue::string(capitalized).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_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn capitalizes() { + let word = string("andres"); + let expected = string("Andres"); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/command.rs b/crates/nu-cli/src/commands/str_/command.rs new file mode 100644 index 0000000000..3cc4fd1086 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/command.rs @@ -0,0 +1,51 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; + +pub struct Command; + +impl WholeStreamCommand for Command { + fn name(&self) -> &str { + "str" + } + + fn signature(&self) -> Signature { + Signature::build("str").rest( + SyntaxShape::ColumnPath, + "optionally convert by column paths", + ) + } + + fn usage(&self) -> &str { + "Apply string function." + } + + fn run( + &self, + _args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let registry = registry.clone(); + let stream = async_stream! { + yield Ok(ReturnSuccess::Value( + UntaggedValue::string(crate::commands::help::get_help(&Command, ®istry)) + .into_value(Tag::unknown()), + )); + }; + + Ok(stream.to_output_stream()) + } +} + +#[cfg(test)] +mod tests { + use super::Command; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Command {}) + } +} diff --git a/crates/nu-cli/src/commands/str_/downcase.rs b/crates/nu-cli/src/commands/str_/downcase.rs new file mode 100644 index 0000000000..0f00425093 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/downcase.rs @@ -0,0 +1,132 @@ +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; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str downcase" + } + + fn signature(&self) -> Signature { + Signature::build("str downcase").rest( + SyntaxShape::ColumnPath, + "optionally downcase text by column paths", + ) + } + + fn usage(&self) -> &str { + "downcases text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Downcase contents", + example: "echo 'NU' | str downcase", + result: Some(vec![Value::from("nu")]), + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(UntaggedValue::string(s.to_ascii_lowercase()).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_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn downcases() { + let word = string("ANDRES"); + let expected = string("andres"); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/find_replace.rs b/crates/nu-cli/src/commands/str_/find_replace.rs new file mode 100644 index 0000000000..737f6cdd0c --- /dev/null +++ b/crates/nu-cli/src/commands/str_/find_replace.rs @@ -0,0 +1,159 @@ +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; + +use regex::Regex; + +#[derive(Deserialize)] +struct Arguments { + find: Tagged, + replace: Tagged, + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str find-replace" + } + + fn signature(&self) -> Signature { + Signature::build("str find-replace") + .required("find", SyntaxShape::String, "the pattern to find") + .required("replace", SyntaxShape::String, "the replacement pattern") + .rest( + SyntaxShape::ColumnPath, + "optionally find and replace text by column paths", + ) + } + + fn usage(&self) -> &str { + "finds and replaces text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Find and replace contents with capture group", + example: "echo 'my_library.rb' | str find-replace '(.+).rb' '$1.nu'", + result: Some(vec![Value::from("my_library.nu")]), + }] + } +} + +#[derive(Clone)] +struct FindReplace(String, String); + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { find, replace, rest }, mut input) = args.process(®istry).await?; + let options = FindReplace(find.item, replace.item); + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, &options, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let options = options.clone(); + + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| { + action(old, &options, old.tag()) + })); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, options: &FindReplace, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let find = &options.0; + let replacement = &options.1; + + let regex = Regex::new(find.as_str()); + + let out = match regex { + Ok(re) => UntaggedValue::string(re.replace(s, replacement.as_str()).to_owned()), + Err(_) => UntaggedValue::string(s), + }; + + Ok(out.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, FindReplace, 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 can_have_capture_groups() { + let word = string("Cargo.toml"); + let expected = string("Carga.toml"); + + let find_replace_options = FindReplace("Cargo.(.+)".to_string(), "Carga.$1".to_string()); + + let actual = action(&word, &find_replace_options, 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 new file mode 100644 index 0000000000..ea7f643353 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/mod.rs @@ -0,0 +1,23 @@ +mod capitalize; +mod command; +mod downcase; +mod find_replace; +mod set; +mod substring; +mod to_datetime; +mod to_float; +mod to_integer; +mod trim; +mod upcase; + +pub use capitalize::SubCommand as StrCapitalize; +pub use command::Command as Str; +pub use downcase::SubCommand as StrDowncase; +pub use find_replace::SubCommand as StrFindReplace; +pub use set::SubCommand as StrSet; +pub use substring::SubCommand as StrSubstring; +pub use to_datetime::SubCommand as StrToDatetime; +pub use to_float::SubCommand as StrToFloat; +pub use to_integer::SubCommand as StrToInteger; +pub use trim::SubCommand as StrTrim; +pub use upcase::SubCommand as StrUpcase; diff --git a/crates/nu-cli/src/commands/str_/set.rs b/crates/nu-cli/src/commands/str_/set.rs new file mode 100644 index 0000000000..8da0a478d6 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/set.rs @@ -0,0 +1,135 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + replace: Tagged, + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str set" + } + + fn signature(&self) -> Signature { + Signature::build("str set") + .required("set", SyntaxShape::String, "the new string to set") + .rest( + SyntaxShape::ColumnPath, + "optionally set text by column paths", + ) + } + + fn usage(&self) -> &str { + "sets text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Set contents with preferred string", + example: "echo 'good day' | str set 'good bye'", + result: Some(vec![Value::from("good bye")]), + }, + Example { + description: "Set the contents on preferred column paths", + example: "open Cargo.toml | str set '255' package.version", + result: None, + }, + ] + } +} + +#[derive(Clone)] +struct Replace(String); + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { replace, rest }, mut input) = args.process(®istry).await?; + let options = Replace(replace.item); + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, &options, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let options = options.clone(); + + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, &options, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(_input: &Value, options: &Replace, tag: impl Into) -> Result { + let replacement = &options.0; + Ok(UntaggedValue::string(replacement.as_str()).into_value(tag)) +} + +#[cfg(test)] +mod tests { + use super::{action, Replace, 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 sets() { + let word = string("andres"); + let expected = string("robalino"); + + let set_options = Replace(String::from("robalino")); + + let actual = action(&word, &set_options, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/substring.rs b/crates/nu-cli/src/commands/str_/substring.rs new file mode 100644 index 0000000000..34a0e0eb81 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/substring.rs @@ -0,0 +1,215 @@ +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; + +use std::cmp; + +#[derive(Deserialize)] +struct Arguments { + range: Tagged, + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str substring" + } + + fn signature(&self) -> Signature { + Signature::build("str substring") + .required( + "range", + SyntaxShape::String, + "the indexes to substring \"start, end\"", + ) + .rest( + SyntaxShape::ColumnPath, + "optionally substring text by column paths", + ) + } + + fn usage(&self) -> &str { + "substrings text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get a substring from the text", + example: "echo 'good nushell' | str substring '5,12'", + result: Some(vec![Value::from("nushell")]), + }, + Example { + description: "Get the remaining characters from a starting index", + example: "echo 'good nushell' | str substring '5,'", + result: Some(vec![Value::from("nushell")]), + }, + Example { + description: "Get the characters from the beginning until ending index", + example: "echo 'good nushell' | str substring ',7'", + result: Some(vec![Value::from("good nu")]), + }, + ] + } +} + +#[derive(Clone)] +struct Substring(usize, usize); + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let name = args.call_info.name_tag.clone(); + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { range, rest }, mut input) = args.process(®istry).await?; + + let v: Vec<&str> = range.item.split(',').collect(); + + let start = match v[0] { + "" => 0, + _ => v[0] + .trim() + .parse() + .map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + }; + + let end = match v[1] { + "" => usize::max_value(), + _ => v[1] + .trim() + .parse() + .map_err(|_| { + ShellError::labeled_error( + "could not perform substring", + "could not perform substring", + name.span, + ) + })? + }; + + if start > end { + yield Err(ShellError::labeled_error( + "End must be greater than or equal to Start", + "End must be greater than or equal to Start", + name.span, + )); + return; + } + + let options = Substring(start, end); + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, &options, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let options = options.clone(); + + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, &options, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, options: &Substring, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let start = options.0; + let end: usize = cmp::min(options.1, s.len()); + + let out = { + if start > s.len() - 1 { + UntaggedValue::string("") + } else { + UntaggedValue::string( + s.chars().skip(start).take(end - start).collect::(), + ) + } + }; + + Ok(out.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, Substring}; + 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 given_start_and_end_indexes() { + let word = string("andresS"); + let expected = string("andres"); + + let substring_options = Substring(0, 6); + + let actual = action(&word, &substring_options, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/to_datetime.rs b/crates/nu-cli/src/commands/str_/to_datetime.rs new file mode 100644 index 0000000000..3a45ed25f0 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/to_datetime.rs @@ -0,0 +1,169 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ + ColumnPath, Primitive, ReturnSuccess, ShellTypeName, Signature, SyntaxShape, UntaggedValue, + Value, +}; +use nu_source::{Tag, Tagged}; +use nu_value_ext::ValueExt; + +use chrono::DateTime; + +#[derive(Deserialize)] +struct Arguments { + format: Option>, + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str to-datetime" + } + + fn signature(&self) -> Signature { + Signature::build("str to-datetime") + .named( + "format", + SyntaxShape::String, + "Specify date and time formatting", + Some('f'), + ) + .rest( + SyntaxShape::ColumnPath, + "optionally convert text into datetime by column paths", + ) + } + + fn usage(&self) -> &str { + "converts text into datetime" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Convert to datetime", + example: "echo '16.11.1984 8:00 am +0000' | str to-datetime", + result: None, + }] + } +} + +#[derive(Clone)] +struct DatetimeFormat(String); + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { format, rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + let options = if let Some(Tagged { item: fmt, tag }) = format { + DatetimeFormat(fmt) + } else { + DatetimeFormat(String::from("%d.%m.%Y %H:%M %P %z")) + }; + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, &options, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let options = options.clone(); + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, &options, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action( + input: &Value, + options: &DatetimeFormat, + tag: impl Into, +) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let dt = &options.0; + + let out = match DateTime::parse_from_str(s, dt) { + Ok(d) => UntaggedValue::date(d), + Err(_) => UntaggedValue::string(s), + }; + + Ok(out.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, DatetimeFormat, 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 takes_a_date_format() { + let date_str = string("16.11.1984 8:00 am +0000"); + + let fmt_options = DatetimeFormat("%d.%m.%Y %H:%M %P %z".to_string()); + + let actual = action(&date_str, &fmt_options, Tag::unknown()).unwrap(); + + match actual.value { + UntaggedValue::Primitive(Primitive::Date(_)) => {} + _ => panic!("Didn't convert to date"), + } + } +} diff --git a/crates/nu-cli/src/commands/str_/to_float.rs b/crates/nu-cli/src/commands/str_/to_float.rs new file mode 100644 index 0000000000..ea583c688c --- /dev/null +++ b/crates/nu-cli/src/commands/str_/to_float.rs @@ -0,0 +1,141 @@ +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; +use nu_value_ext::ValueExt; + +use bigdecimal::BigDecimal; +use std::str::FromStr; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str to-float" + } + + fn signature(&self) -> Signature { + Signature::build("str to-float").rest( + SyntaxShape::ColumnPath, + "optionally convert text into float by column paths", + ) + } + + fn usage(&self) -> &str { + "converts text into float" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Convert to float", + example: "echo '3.1415' | str to-float", + result: None, + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let other = s.trim(); + let out = match BigDecimal::from_str(other) { + Ok(v) => UntaggedValue::decimal(v), + Err(_) => UntaggedValue::string(s), + }; + Ok(out.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::{decimal, string}; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + #[allow(clippy::approx_constant)] + fn turns_to_integer() { + let word = string("3.1415"); + let expected = decimal(3.1415); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/to_integer.rs b/crates/nu-cli/src/commands/str_/to_integer.rs new file mode 100644 index 0000000000..ec03ac0d8b --- /dev/null +++ b/crates/nu-cli/src/commands/str_/to_integer.rs @@ -0,0 +1,140 @@ +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; +use nu_value_ext::ValueExt; + +use num_bigint::BigInt; +use std::str::FromStr; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str to-int" + } + + fn signature(&self) -> Signature { + Signature::build("str to-int").rest( + SyntaxShape::ColumnPath, + "optionally convert text into integer by column paths", + ) + } + + fn usage(&self) -> &str { + "converts text into integer" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Convert to an integer", + example: "echo '255' | str to-int", + result: None, + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + let other = s.trim(); + let out = match BigInt::from_str(other) { + Ok(v) => UntaggedValue::int(v), + Err(_) => UntaggedValue::string(s), + }; + Ok(out.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::{int, string}; + use nu_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn turns_to_integer() { + let word = string("10"); + let expected = int(10); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/trim.rs b/crates/nu-cli/src/commands/str_/trim.rs new file mode 100644 index 0000000000..c63f722664 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/trim.rs @@ -0,0 +1,132 @@ +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; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +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", + ) + } + + fn usage(&self) -> &str { + "trims text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Trim contents", + example: "echo 'Nu shell ' | str trim", + result: Some(vec![Value::from("Nu shell")]), + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(UntaggedValue::string(s.trim()).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_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()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/src/commands/str_/upcase.rs b/crates/nu-cli/src/commands/str_/upcase.rs new file mode 100644 index 0000000000..99861851d3 --- /dev/null +++ b/crates/nu-cli/src/commands/str_/upcase.rs @@ -0,0 +1,132 @@ +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; +use nu_value_ext::ValueExt; + +#[derive(Deserialize)] +struct Arguments { + rest: Vec, +} + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "str upcase" + } + + fn signature(&self) -> Signature { + Signature::build("str upcase").rest( + SyntaxShape::ColumnPath, + "optionally upcase text by column paths", + ) + } + + fn usage(&self) -> &str { + "upcases text" + } + + fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Upcase contents", + example: "echo 'nu' | str upcase", + result: Some(vec![Value::from("NU")]), + }] + } +} + +fn operate(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + + let stream = async_stream! { + let (Arguments { rest }, mut input) = args.process(®istry).await?; + + let column_paths: Vec<_> = rest.iter().map(|x| x.clone()).collect(); + + while let Some(v) = input.next().await { + if column_paths.is_empty() { + match action(&v, v.tag()) { + Ok(out) => yield ReturnSuccess::value(out), + Err(err) => { + yield Err(err); + return; + } + } + } else { + + let mut ret = v.clone(); + + for path in &column_paths { + let swapping = ret.swap_data_by_column_path(path, Box::new(move |old| action(old, old.tag()))); + + match swapping { + Ok(new_value) => { + ret = new_value; + } + Err(err) => { + yield Err(err); + return; + } + } + } + + yield ReturnSuccess::value(ret); + } + } + }; + + Ok(stream.to_output_stream()) +} + +fn action(input: &Value, tag: impl Into) -> Result { + match &input.value { + UntaggedValue::Primitive(Primitive::Line(s)) + | UntaggedValue::Primitive(Primitive::String(s)) => { + Ok(UntaggedValue::string(s.to_ascii_uppercase()).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_source::Tag; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } + + #[test] + fn upcases() { + let word = string("andres"); + let expected = string("ANDRES"); + + let actual = action(&word, Tag::unknown()).unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/crates/nu-cli/tests/commands/keep_until.rs b/crates/nu-cli/tests/commands/keep_until.rs index 654998c403..55524538b0 100644 --- a/crates/nu-cli/tests/commands/keep_until.rs +++ b/crates/nu-cli/tests/commands/keep_until.rs @@ -39,7 +39,7 @@ fn condition_is_met() { | headers | skip-while "Chickens Collction" != "Blue Chickens" | keep-until "Chicken Collection" == "Red Chickens" - | str "31/04/2020" --to-int + | str to-int "31/04/2020" | get "31/04/2020" | sum | echo $it diff --git a/crates/nu-cli/tests/commands/keep_while.rs b/crates/nu-cli/tests/commands/keep_while.rs index 83afd9e9fb..e8c3e69342 100644 --- a/crates/nu-cli/tests/commands/keep_while.rs +++ b/crates/nu-cli/tests/commands/keep_while.rs @@ -39,7 +39,7 @@ fn condition_is_met() { | headers | skip 1 | keep-while "Chicken Collection" != "Blue Chickens" - | str "31/04/2020" --to-int + | str to-int "31/04/2020" | get "31/04/2020" | sum | echo $it diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 0104f5dd9c..d31304f188 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -42,6 +42,7 @@ mod sort_by; mod split_by; mod split_column; mod split_row; +mod str_; mod sum; mod touch; mod trim; diff --git a/crates/nu-cli/tests/commands/skip_until.rs b/crates/nu-cli/tests/commands/skip_until.rs index b9ff9d3089..f901e8b655 100644 --- a/crates/nu-cli/tests/commands/skip_until.rs +++ b/crates/nu-cli/tests/commands/skip_until.rs @@ -38,7 +38,7 @@ fn condition_is_met() { | split column ',' | headers | skip-until "Chicken Collection" == "Red Chickens" - | str "31/04/2020" --to-int + | str to-int "31/04/2020" | get "31/04/2020" | sum | echo $it diff --git a/crates/nu-cli/tests/commands/str_.rs b/crates/nu-cli/tests/commands/str_.rs new file mode 100644 index 0000000000..f4bdb6d10a --- /dev/null +++ b/crates/nu-cli/tests/commands/str_.rs @@ -0,0 +1,342 @@ +use nu_test_support::fs::Stub::FileWithContent; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn trims() { + Playground::setup("str_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "nu " + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str trim dependency.name | get dependency.name | echo $it" + ); + + assert_eq!(actual.out, "nu"); + }) +} + +#[test] +fn capitalizes() { + Playground::setup("str_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "nu" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str capitalize dependency.name | get dependency.name | echo $it" + ); + + assert_eq!(actual.out, "Nu"); + }) +} + +#[test] +fn downcases() { + Playground::setup("str_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [dependency] + name = "LIGHT" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str downcase dependency.name | get dependency.name | echo $it" + ); + + assert_eq!(actual.out, "light"); + }) +} + +#[test] +fn upcases() { + Playground::setup("str_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), + "open sample.toml | str upcase package.name | get package.name | echo $it" + ); + + assert_eq!(actual.out, "NUSHELL"); + }) +} + +#[test] +fn converts_to_int() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo '{number_as_string: "1"}' + | from json + | str to-int number_as_string + | rename number + | where number == 1 + | get number + | echo $it + "# + )); + + assert_eq!(actual.out, "1"); +} + +#[test] +fn converts_to_float() { + let actual = nu!( + cwd: "tests/fixtures/formats", pipeline( + r#" + echo "3.1, 0.0415" + | split row "," + | str to-float + | sum + "# + )); + + assert_eq!(actual.out, "3.1415"); +} + +#[test] +fn sets() { + Playground::setup("str_test_5", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nushell" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str set wykittenshell package.name + | get package.name + | echo $it + "# + )); + + assert_eq!(actual.out, "wykittenshell"); + }) +} + +#[test] +fn find_and_replaces() { + Playground::setup("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(), pipeline( + r#" + open sample.toml + | str find-replace KATZ "5289" fortune.teller.phone + | get fortune.teller.phone + | echo $it + "# + )); + + assert_eq!(actual.out, "1-800-5289"); + }) +} + +#[test] +fn find_and_replaces_without_passing_field() { + Playground::setup("str_test_7", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-KATZ" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | get fortune.teller.phone + | str find-replace KATZ "5289" + | echo $it + "# + )); + + assert_eq!(actual.out, "1-800-5289"); + }) +} + +#[test] +fn substrings_the_input() { + Playground::setup("str_test_8", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-ROBALINO" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 6,14 fortune.teller.phone + | get fortune.teller.phone + | echo $it + "# + )); + + assert_eq!(actual.out, "ROBALINO"); + }) +} + +#[test] +fn substring_errors_if_start_index_is_greater_than_end_index() { + Playground::setup("str_test_9", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [fortune.teller] + phone = "1-800-ROBALINO" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 6,5 fortune.teller.phone + | echo $it + "# + )); + + assert!(actual + .err + .contains("End must be greater than or equal to Start")) + }) +} + +#[test] +fn substrings_the_input_and_returns_the_string_if_end_index_exceeds_length() { + Playground::setup("str_test_10", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 0,999 package.name + | get package.name + | echo $it + "# + )); + + assert_eq!(actual.out, "nu-arepas"); + }) +} + +#[test] +fn substrings_the_input_and_returns_blank_if_start_index_exceeds_length() { + Playground::setup("str_test_11", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 50,999 package.name + | get package.name + | echo $it + "# + )); + + assert_eq!(actual.out, ""); + }) +} + +#[test] +fn substrings_the_input_and_treats_start_index_as_zero_if_blank_start_index_given() { + Playground::setup("str_test_12", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring ,2 package.name + | get package.name + | echo $it + "# + )); + + assert_eq!(actual.out, "nu"); + }) +} + +#[test] +fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given() { + Playground::setup("str_test_13", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContent( + "sample.toml", + r#" + [package] + name = "nu-arepas" + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.toml + | str substring 3, package.name + | get package.name + | echo $it + "# + )); + + assert_eq!(actual.out, "arepas"); + }) +} diff --git a/crates/nu-value-ext/src/lib.rs b/crates/nu-value-ext/src/lib.rs index 48ac533447..677dd7f2a0 100644 --- a/crates/nu-value-ext/src/lib.rs +++ b/crates/nu-value-ext/src/lib.rs @@ -1,3 +1,4 @@ +use indexmap::set::IndexSet; use itertools::Itertools; use nu_errors::{ExpectedRange, ShellError}; use nu_protocol::{ @@ -17,6 +18,11 @@ pub trait ValueExt { path: &ColumnPath, callback: Box ShellError>, ) -> Result; + fn swap_data_by_column_path( + &self, + path: &ColumnPath, + callback: Box Result>, + ) -> Result; fn insert_data_at_path(&self, path: &str, new_value: Value) -> Option; fn insert_data_at_member( &mut self, @@ -63,6 +69,14 @@ impl ValueExt for Value { get_data_by_column_path(self, path, callback) } + fn swap_data_by_column_path( + &self, + path: &ColumnPath, + callback: Box Result>, + ) -> Result { + swap_data_by_column_path(self, path, callback) + } + fn insert_data_at_path(&self, path: &str, new_value: Value) -> Option { insert_data_at_path(self, path, new_value) } @@ -195,6 +209,159 @@ pub fn get_data_by_column_path( Ok(current) } +pub fn swap_data_by_column_path( + value: &Value, + path: &ColumnPath, + callback: Box Result>, +) -> Result { + let fields = path.clone(); + + let to_replace = get_data_by_column_path( + &value, + path, + Box::new(move |(obj_source, column_path_tried, error)| { + let path_members_span = + nu_source::span_for_spanned_list(fields.members().iter().map(|p| p.span)); + + match &obj_source.value { + UntaggedValue::Table(rows) => match column_path_tried { + PathMember { + unspanned: UnspannedPathMember::String(column), + .. + } => { + let primary_label = format!("There isn't a column named '{}'", &column); + + let suggestions: IndexSet<_> = rows + .iter() + .filter_map(|r| nu_protocol::did_you_mean(&r, &column_path_tried)) + .map(|s| s[0].1.to_owned()) + .collect(); + let mut existing_columns: IndexSet<_> = IndexSet::default(); + let mut names: Vec = vec![]; + + for row in rows { + for field in row.data_descriptors() { + if !existing_columns.contains(&field[..]) { + existing_columns.insert(field.clone()); + names.push(field); + } + } + } + + if names.is_empty() { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + "Appears to contain rows. Try indexing instead.", + column_path_tried.span.since(path_members_span), + ); + } else { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + format!( + "Perhaps you meant '{}'? Columns available: {}", + suggestions + .iter() + .map(|x| x.to_owned()) + .collect::>() + .join(","), + names.join(",") + ), + column_path_tried.span.since(path_members_span), + ); + }; + } + PathMember { + unspanned: UnspannedPathMember::Int(idx), + .. + } => { + let total = rows.len(); + + let secondary_label = if total == 1 { + "The table only has 1 row".to_owned() + } else { + format!("The table only has {} rows (0 to {})", total, total - 1) + }; + + return ShellError::labeled_error_with_secondary( + "Row not found", + format!("There isn't a row indexed at {}", idx), + column_path_tried.span, + secondary_label, + column_path_tried.span.since(path_members_span), + ); + } + }, + UntaggedValue::Row(columns) => match column_path_tried { + PathMember { + unspanned: UnspannedPathMember::String(column), + .. + } => { + let primary_label = format!("There isn't a column named '{}'", &column); + + if let Some(suggestions) = + nu_protocol::did_you_mean(&obj_source, column_path_tried) + { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + format!( + "Perhaps you meant '{}'? Columns available: {}", + suggestions[0].1, + &obj_source.data_descriptors().join(",") + ), + column_path_tried.span.since(path_members_span), + ); + } + } + PathMember { + unspanned: UnspannedPathMember::Int(idx), + .. + } => { + return ShellError::labeled_error_with_secondary( + "No rows available", + format!("A row at '{}' can't be indexed.", &idx), + column_path_tried.span, + format!( + "Appears to contain columns. Columns available: {}", + columns.keys().join(",") + ), + column_path_tried.span.since(path_members_span), + ) + } + }, + _ => {} + } + + if let Some(suggestions) = nu_protocol::did_you_mean(&obj_source, column_path_tried) { + return ShellError::labeled_error( + "Unknown column", + format!("did you mean '{}'?", suggestions[0].1), + column_path_tried.span.since(path_members_span), + ); + } + + error + }), + ); + + let to_replace = to_replace?; + let replacement = callback(&to_replace)?; + + match value.replace_data_at_column_path(&path, replacement) { + Some(replaced) => Ok(replaced), + None => Err(ShellError::labeled_error( + "missing column-path", + "missing column-path", + value.tag.span, + )), + } +} + pub fn insert_data_at_path(value: &Value, path: &str, new_value: Value) -> Option { let mut new_obj = value.clone(); diff --git a/crates/nu_plugin_str/Cargo.toml b/crates/nu_plugin_str/Cargo.toml deleted file mode 100644 index c895484b4c..0000000000 --- a/crates/nu_plugin_str/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "nu_plugin_str" -version = "0.14.1" -authors = ["The Nu Project Contributors"] -edition = "2018" -description = "A string manipulation plugin for Nushell" -license = "MIT" - -[lib] -doctest = false - -[dependencies] -nu-plugin = { path = "../nu-plugin", version = "0.14.1" } -nu-protocol = { path = "../nu-protocol", version = "0.14.1" } -nu-source = { path = "../nu-source", version = "0.14.1" } -nu-errors = { path = "../nu-errors", version = "0.14.1" } -nu-value-ext = { path = "../nu-value-ext", version = "0.14.1" } -chrono = { version = "0.4.11", features = ["serde"] } - -regex = "1" -num-bigint = "0.2.6" -bigdecimal = { version = "0.1.2", features = ["serde"] } - -[build-dependencies] -nu-build = { version = "0.14.1", path = "../nu-build" } diff --git a/crates/nu_plugin_str/build.rs b/crates/nu_plugin_str/build.rs deleted file mode 100644 index b7511cfc6a..0000000000 --- a/crates/nu_plugin_str/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() -> Result<(), Box> { - nu_build::build() -} diff --git a/crates/nu_plugin_str/src/lib.rs b/crates/nu_plugin_str/src/lib.rs deleted file mode 100644 index 625c337662..0000000000 --- a/crates/nu_plugin_str/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod nu; -mod strutils; - -pub use strutils::Str; - -#[cfg(test)] -mod tests { - use super::Str; - use crate::strutils::Action; - use nu_protocol::Value; - use nu_value_ext::ValueExt; - - impl Str { - pub fn expect_action(&self, action: Action) { - match &self.action { - Some(set) if set == &action => {} - Some(other) => panic!(format!("\nExpected {:#?}\n\ngot {:#?}", action, other)), - None => panic!(format!("\nAction {:#?} not found.", action)), - } - } - - pub fn expect_field(&self, field: Value) { - let field = match field.as_column_path() { - Ok(column_path) => column_path, - Err(reason) => panic!(format!( - "\nExpected {:#?} to be a ColumnPath, \n\ngot {:#?}", - field, reason - )), - }; - - match &self.field { - Some(column_path) if column_path == &field => {} - Some(other) => panic!(format!("\nExpected {:#?} \n\ngot {:#?}", field, other)), - None => panic!(format!("\nField {:#?} not found.", field)), - } - } - } -} diff --git a/crates/nu_plugin_str/src/main.rs b/crates/nu_plugin_str/src/main.rs deleted file mode 100644 index b1d8d8c8ab..0000000000 --- a/crates/nu_plugin_str/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -use nu_plugin::serve_plugin; -use nu_plugin_str::Str; - -fn main() { - serve_plugin(&mut Str::new()) -} diff --git a/crates/nu_plugin_str/src/nu/mod.rs b/crates/nu_plugin_str/src/nu/mod.rs deleted file mode 100644 index efd54c75f7..0000000000 --- a/crates/nu_plugin_str/src/nu/mod.rs +++ /dev/null @@ -1,156 +0,0 @@ -#[cfg(test)] -mod tests; - -use crate::strutils::ReplaceAction; -use crate::Str; -use nu_errors::ShellError; -use nu_plugin::Plugin; -use nu_protocol::{ - CallInfo, Primitive, ReturnSuccess, ReturnValue, ShellTypeName, Signature, SyntaxShape, - UntaggedValue, Value, -}; -use nu_value_ext::ValueExt; - -impl Plugin for Str { - fn config(&mut self) -> Result { - Ok(Signature::build("str") - .desc("Apply string function. Optional use the column of a table") - .switch("capitalize", "capitalizes the string", Some('c')) - .switch("downcase", "convert string to lowercase", Some('d')) - .switch("upcase", "convert string to uppercase", Some('U')) - .switch("to-int", "convert string to integer", Some('i')) - .switch("to-float", "convert string to float", Some('F')) - .switch("trim", "trims the string", Some('t')) - .named( - "replace", - SyntaxShape::String, - "replaces the string", - Some('r'), - ) - .named( - "find-replace", - SyntaxShape::Any, - "finds and replaces [pattern replacement]", - Some('f'), - ) - .named( - "substring", - SyntaxShape::String, - "convert string to portion of original, requires \"start,end\"", - Some('s'), - ) - .named( - "to-date-time", - SyntaxShape::String, - "Convert string to Date/Time", - Some('D'), - ) - .rest(SyntaxShape::ColumnPath, "the column(s) to convert") - .filter()) - } - - fn begin_filter(&mut self, call_info: CallInfo) -> Result, ShellError> { - let args = call_info.args; - - if args.has("trim") { - self.for_trim(); - } - if args.has("capitalize") { - self.for_capitalize(); - } - if args.has("downcase") { - self.for_downcase(); - } - if args.has("upcase") { - self.for_upcase(); - } - if args.has("to-int") { - self.for_to_int(); - } - if args.has("to-float") { - self.for_to_float(); - } - if args.has("substring") { - if let Some(start_end) = args.get("substring") { - match start_end { - Value { - value: UntaggedValue::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 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), - tag, - }) = args.get("find-replace") - { - self.for_replace(ReplaceAction::FindAndReplace( - arguments - .get(0) - .ok_or_else(|| { - ShellError::labeled_error( - "expected file and replace strings eg) [find replace]", - "missing find-replace values", - tag, - ) - })? - .as_string()?, - arguments - .get(1) - .ok_or_else(|| { - ShellError::labeled_error( - "expected file and replace strings eg) [find replace]", - "missing find-replace values", - tag, - ) - })? - .as_string()?, - )); - } - } - - if let Some(possible_field) = args.nth(0) { - let possible_field = possible_field.as_column_path()?; - self.for_field(possible_field); - } - - if let Some(dt) = args.get("to-date-time") { - let dt = dt.as_string()?; - self.for_date_time(dt); - } - - match &self.error { - Some(reason) => Err(ShellError::untagged_runtime_error(format!( - "{}: {}", - reason, - Str::usage() - ))), - None => Ok(vec![]), - } - } - - fn filter(&mut self, input: Value) -> Result, ShellError> { - Ok(vec![ReturnSuccess::value(self.strutils(input)?)]) - } -} diff --git a/crates/nu_plugin_str/src/nu/tests.rs b/crates/nu_plugin_str/src/nu/tests.rs deleted file mode 100644 index 0e5c13d8a2..0000000000 --- a/crates/nu_plugin_str/src/nu/tests.rs +++ /dev/null @@ -1,512 +0,0 @@ -mod integration { - use crate::strutils::{Action, ReplaceAction}; - use crate::Str; - use nu_errors::ShellError; - use nu_plugin::test_helpers::value::{ - column_path, decimal, get_data, int, string, structured_sample_record, table, - unstructured_sample_record, - }; - use nu_plugin::test_helpers::{expect_return_value_at, plugin, CallStub}; - use nu_protocol::{Primitive, UntaggedValue}; - - #[test] - fn picks_up_date_time() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("to-date-time", string("%d.%m.%Y %H:%M %P %z")) - .create(), - ) - .input(string("5.8.1994 8:00 am +0000")) - .input(string("6.9.1995 10:00 am +0000")) - .input(string("5.8.1994 20:00 pm +0000")) - .input(string("20.4.2020 8:00 am +0000")) - .setup(|_, _| {}) - .test(); - let ret_vals = run.unwrap(); - for r in ret_vals { - let r = r - .as_ref() - .unwrap() - .raw_value() - .unwrap() - .as_primitive() - .unwrap(); - match r { - Primitive::Date(_) => (), - _ => panic!("failed to convert string to date"), - } - } - } - - #[test] - fn picks_up_one_action_flag_only() { - plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("upcase") - .with_long_flag("downcase") - .create(), - ) - .setup(|plugin, returned_values| { - let actual = format!("{}", returned_values.unwrap_err()); - - assert!(actual.contains("can only apply one")); - assert_eq!(plugin.error, Some("can only apply one".to_string())); - }); - } - - #[test] - fn picks_up_trim_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("trim").create()) - .setup(|plugin, _| plugin.expect_action(Action::Trim)); - } - - #[test] - fn picks_up_capitalize_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("capitalize").create()) - .setup(|plugin, _| plugin.expect_action(Action::Capitalize)); - } - - #[test] - fn picks_up_downcase_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("downcase").create()) - .setup(|plugin, _| plugin.expect_action(Action::Downcase)); - } - - #[test] - fn picks_up_upcase_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("upcase").create()) - .setup(|plugin, _| plugin.expect_action(Action::Upcase)); - } - - #[test] - fn picks_up_to_int_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("to-int").create()) - .setup(|plugin, _| plugin.expect_action(Action::ToInteger)); - } - - #[test] - fn picks_up_to_float_flag() { - plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("to-float").create()) - .setup(|plugin, _| plugin.expect_action(Action::ToFloat)); - } - - #[test] - fn picks_up_arguments_for_replace_flag() { - let argument = String::from("replace_text"); - - plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("replace", string(&argument)) - .create(), - ) - .setup(|plugin, _| { - let strategy = ReplaceAction::Direct(argument); - plugin.expect_action(Action::Replace(strategy)); - }); - } - - #[test] - fn picks_up_arguments_for_find_replace() { - let search_argument = String::from("kittens"); - let replace_argument = String::from("jotandrehuda"); - - plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter( - "find-replace", - table(&[string(&search_argument), string(&replace_argument)]), - ) - .create(), - ) - .setup(|plugin, _| { - let strategy = ReplaceAction::FindAndReplace(search_argument, replace_argument); - plugin.expect_action(Action::Replace(strategy)) - }); - } - - #[test] - fn picks_up_argument_for_field() -> Result<(), ShellError> { - plugin(&mut Str::new()) - .args( - CallStub::new() - .with_parameter("package.description")? - .create(), - ) - .setup(|plugin, _| { - //FIXME: this is possibly not correct - if let Ok(column_path) = column_path(&[string("package"), string("description")]) { - plugin.expect_field(column_path) - } - }); - - Ok(()) - } - - #[test] - fn substring_errors_if_start_index_is_greater_than_end_index() { - plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string("3,1")) - .create(), - ) - .setup(|plugin, returned_values| { - let actual = format!("{}", returned_values.unwrap_err()); - - assert!(actual.contains("End must be greater than or equal to Start")); - assert_eq!( - plugin.error, - Some("End must be greater than or equal to Start".to_string()) - ); - }); - } - - #[test] - fn upcases_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("upcase") - .with_parameter("name")? - .create(), - ) - .input(structured_sample_record("name", "jotandrehuda")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "name"), string("JOTANDREHUDA")); - Ok(()) - } - - #[test] - fn trims_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("trim") - .with_parameter("name")? - .create(), - ) - .input(structured_sample_record("name", "andres ")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "name"), string("andres")); - Ok(()) - } - - #[test] - fn capitalizes_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("capitalize") - .with_parameter("name")? - .create(), - ) - .input(structured_sample_record("name", "andres")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "name"), string("Andres")); - Ok(()) - } - - #[test] - fn downcases_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("downcase") - .with_parameter("name")? - .create(), - ) - .input(structured_sample_record("name", "JOTANDREHUDA")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "name"), string("jotandrehuda")); - Ok(()) - } - - #[test] - fn converts_the_input_to_integer_using_the_field_passed_as_parameter() -> Result<(), ShellError> - { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("to-int") - .with_parameter("Nu_birthday")? - .create(), - ) - .input(structured_sample_record("Nu_birthday", "10")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "Nu_birthday"), int(10)); - Ok(()) - } - #[test] - #[allow(clippy::approx_constant)] - fn converts_the_input_to_float_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_long_flag("to-float") - .with_parameter("PI")? - .create(), - ) - .input(structured_sample_record("PI", "3.1415")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "PI"), decimal(3.1415)); - Ok(()) - } - - #[test] - fn replaces_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_parameter("rustconf")? - .with_named_parameter("replace", string("22nd August 2019")) - .create(), - ) - .input(structured_sample_record("rustconf", "1st January 1970")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "rustconf"), string("22nd August 2019")); - Ok(()) - } - - #[test] - fn find_and_replaces_the_input_using_the_field_passed_as_parameter() -> Result<(), ShellError> { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_parameter("staff")? - .with_named_parameter( - "find-replace", - table(&[string("kittens"), string("jotandrehuda")]), - ) - .create(), - ) - .input(structured_sample_record("staff", "wykittens")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(get_data(actual, "staff"), string("wyjotandrehuda")); - Ok(()) - } - - #[test] - fn upcases_the_input() { - let run = plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("upcase").create()) - .input(unstructured_sample_record("joandrehuda")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - assert_eq!(actual, string("JOANDREHUDA")); - } - - #[test] - fn trims_the_input() { - let run = plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("trim").create()) - .input(unstructured_sample_record("andres ")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - assert_eq!(actual, string("andres")); - } - - #[test] - fn capitalizes_the_input() { - let run = plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("capitalize").create()) - .input(unstructured_sample_record("andres")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - assert_eq!(actual, string("Andres")); - } - - #[test] - fn downcases_the_input() { - let run = plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("downcase").create()) - .input(unstructured_sample_record("JOANDREHUDA")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - assert_eq!(actual, string("joandrehuda")); - } - - #[test] - fn converts_the_input_to_integer() { - let run = plugin(&mut Str::new()) - .args(CallStub::new().with_long_flag("to-int").create()) - .input(unstructured_sample_record("10")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, UntaggedValue::int(10).into_untagged_value()); - } - - #[test] - fn substrings_the_input() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string("0,1")) - .create(), - ) - .input(unstructured_sample_record("0123456789")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("0")); - } - - #[test] - fn substrings_the_input_and_returns_the_string_if_end_index_exceeds_length() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string("0,11")) - .create(), - ) - .input(unstructured_sample_record("0123456789")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("0123456789")); - } - - #[test] - fn substrings_the_input_and_returns_blank_if_start_index_exceeds_length() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string("20,30")) - .create(), - ) - .input(unstructured_sample_record("0123456789")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("")); - } - - #[test] - fn substrings_the_input_and_treats_start_index_as_zero_if_blank_start_index_given() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string(",5")) - .create(), - ) - .input(unstructured_sample_record("0123456789")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("01234")); - } - - #[test] - fn substrings_the_input_and_treats_end_index_as_length_if_blank_end_index_given() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("substring", string("2,")) - .create(), - ) - .input(unstructured_sample_record("0123456789")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("23456789")); - } - - #[test] - fn replaces_the_input() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter("replace", string("22nd August 2019")) - .create(), - ) - .input(unstructured_sample_record("1st January 1970")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("22nd August 2019")); - } - - #[test] - fn find_and_replaces_the_input() { - let run = plugin(&mut Str::new()) - .args( - CallStub::new() - .with_named_parameter( - "find-replace", - table(&[string("kittens"), string("jotandrehuda")]), - ) - .create(), - ) - .input(unstructured_sample_record("wykittens")) - .setup(|_, _| {}) - .test(); - - let actual = expect_return_value_at(run, 0); - - assert_eq!(actual, string("wyjotandrehuda")); - } -} diff --git a/crates/nu_plugin_str/src/strutils.rs b/crates/nu_plugin_str/src/strutils.rs deleted file mode 100644 index 6b61876e16..0000000000 --- a/crates/nu_plugin_str/src/strutils.rs +++ /dev/null @@ -1,325 +0,0 @@ -extern crate chrono; - -use bigdecimal::BigDecimal; -use chrono::DateTime; -use nu_errors::ShellError; -use nu_protocol::{did_you_mean, ColumnPath, Primitive, ShellTypeName, UntaggedValue, Value}; -use nu_source::{span_for_spanned_list, Tagged}; -use nu_value_ext::ValueExt; -use regex::Regex; -use std::cmp; -use std::str::FromStr; - -#[derive(Debug, Eq, PartialEq)] -pub enum Action { - Capitalize, - Downcase, - Upcase, - ToInteger, - ToFloat, - Substring(usize, usize), - Replace(ReplaceAction), - ToDateTime(String), - Trim, -} - -#[derive(Debug, Eq, PartialEq)] -pub enum ReplaceAction { - Direct(String), - FindAndReplace(String, String), -} - -#[derive(Default)] -pub struct Str { - pub field: Option>, - pub error: Option, - pub action: Option, -} - -impl Str { - pub fn new() -> Self { - Default::default() - } - - fn apply(&self, input: &str) -> Result { - let applied = match self.action.as_ref() { - Some(Action::Trim) => UntaggedValue::string(input.trim()), - Some(Action::Capitalize) => { - let mut capitalized = String::new(); - - for (idx, character) in input.chars().enumerate() { - let out = if idx == 0 { - character.to_uppercase().to_string() - } else { - character.to_lowercase().to_string() - }; - - capitalized.push_str(&out); - } - - UntaggedValue::string(capitalized) - } - Some(Action::Downcase) => UntaggedValue::string(input.to_ascii_lowercase()), - Some(Action::Upcase) => UntaggedValue::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 { - UntaggedValue::string("") - } else { - UntaggedValue::string( - &input - .chars() - .skip(start) - .take(end - start) - .collect::(), - ) - } - } - 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) => { - let other = input.trim(); - match other.parse::() { - Ok(v) => UntaggedValue::int(v), - Err(_) => UntaggedValue::string(input), - } - } - Some(Action::ToFloat) => match BigDecimal::from_str(input.trim()) { - Ok(v) => UntaggedValue::decimal(v), - Err(_) => UntaggedValue::string(input), - }, - Some(Action::ToDateTime(dt)) => match DateTime::parse_from_str(input, dt) { - Ok(d) => UntaggedValue::date(d), - Err(_) => UntaggedValue::string(input), - }, - None => UntaggedValue::string(input), - }; - - Ok(applied) - } - - pub fn for_field(&mut self, column_path: Tagged) { - self.field = Some(column_path); - } - - fn permit(&mut self) -> bool { - self.action.is_none() - } - - fn log_error(&mut self, message: &str) { - self.error = Some(message.to_string()); - } - - pub fn for_to_int(&mut self) { - self.add_action(Action::ToInteger); - } - - pub fn for_to_float(&mut self) { - self.add_action(Action::ToFloat); - } - - pub fn for_capitalize(&mut self) { - self.add_action(Action::Capitalize); - } - - pub fn for_trim(&mut self) { - self.add_action(Action::Trim); - } - - pub fn for_downcase(&mut self) { - self.add_action(Action::Downcase); - } - - pub fn for_upcase(&mut self) { - self.add_action(Action::Upcase); - } - - pub fn for_substring(&mut self, s: String) -> Result<(), ShellError> { - let v: Vec<&str> = s.split(',').collect(); - let start: usize = match v[0] { - "" => 0, - _ => v[0] - .trim() - .parse() - .map_err(|_| ShellError::untagged_runtime_error("Could not perform substring"))?, - }; - let end: usize = match v[1] { - "" => usize::max_value(), - _ => v[1] - .trim() - .parse() - .map_err(|_| ShellError::untagged_runtime_error("Could not perform substring"))?, - }; - if start > end { - self.log_error("End must be greater than or equal to Start"); - } else { - self.add_action(Action::Substring(start, end)); - } - - Ok(()) - } - - pub fn for_replace(&mut self, mode: ReplaceAction) { - self.add_action(Action::Replace(mode)); - } - - pub fn for_date_time(&mut self, dt: String) { - self.add_action(Action::ToDateTime(dt)); - } - - fn add_action(&mut self, act: Action) { - if self.permit() { - self.action = Some(act); - } else { - self.log_error("can only apply one"); - } - } - - pub fn usage() -> &'static str { - "Usage: str field [--capitalize|--downcase|--upcase|--to-int|--to-float|--substring \"start,end\"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]" - } - - pub fn strutils(&self, value: Value) -> Result { - match &value.value { - UntaggedValue::Primitive(Primitive::String(ref s)) => { - Ok(self.apply(&s)?.into_value(value.tag())) - } - UntaggedValue::Primitive(Primitive::Line(ref s)) => { - Ok(self.apply(&s)?.into_value(value.tag())) - } - UntaggedValue::Row(_) => match self.field { - Some(ref f) => { - let fields = f.clone(); - - let replace_for = - value.get_data_by_column_path( - &f, - Box::new(move |(obj_source, column_path_tried, error)| { - match did_you_mean(&obj_source, &column_path_tried) { - Some(suggestions) => ShellError::labeled_error( - "Unknown column", - format!("did you mean '{}'?", suggestions[0].1), - span_for_spanned_list(fields.iter().map(|p| p.span)), - ), - None => error, - } - }), - ); - - let got = replace_for?; - let replacement = self.strutils(got)?; - - match value - .replace_data_at_column_path(&f, replacement.value.into_untagged_value()) - { - Some(v) => Ok(v), - None => Err(ShellError::labeled_error( - "str could not find field to replace", - "column name", - value.tag(), - )), - } - } - None => Err(ShellError::untagged_runtime_error(format!( - "{}: {}", - "str needs a column when applied to a value in a row", - Str::usage() - ))), - }, - _ => Err(ShellError::labeled_error( - "Unrecognized type in stream", - value.type_name(), - value.tag, - )), - } - } -} - -#[cfg(test)] -pub mod tests { - use super::ReplaceAction; - use super::Str; - use nu_plugin::test_helpers::value::{decimal, int, string}; - - #[test] - fn trim() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_trim(); - assert_eq!(strutils.apply("andres ")?, string("andres").value); - Ok(()) - } - - #[test] - fn capitalize() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_capitalize(); - assert_eq!(strutils.apply("andres")?, string("Andres").value); - Ok(()) - } - - #[test] - fn downcases() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_downcase(); - assert_eq!(strutils.apply("ANDRES")?, string("andres").value); - Ok(()) - } - - #[test] - fn upcases() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_upcase(); - assert_eq!(strutils.apply("andres")?, string("ANDRES").value); - Ok(()) - } - - #[test] - fn converts_to_int() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_to_int(); - assert_eq!(strutils.apply("9999")?, int(9999 as i64).value); - Ok(()) - } - - #[test] - #[allow(clippy::approx_constant)] - fn converts_to_float() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_to_float(); - assert_eq!(strutils.apply("3.1415")?, decimal(3.1415).value); - Ok(()) - } - - #[test] - fn replaces() -> Result<(), Box> { - let mut strutils = Str::new(); - strutils.for_replace(ReplaceAction::Direct("robalino".to_string())); - assert_eq!(strutils.apply("andres")?, string("robalino").value); - Ok(()) - } - - #[test] - fn find_and_replaces() -> Result<(), Box> { - let mut strutils = Str::new(); - - strutils.for_replace(ReplaceAction::FindAndReplace( - "kittens".to_string(), - "jotandrehuda".to_string(), - )); - - assert_eq!(strutils.apply("wykittens")?, string("wyjotandrehuda").value); - Ok(()) - } -} diff --git a/docs/commands/str.md b/docs/commands/str.md index 9d1c092179..ce358043db 100644 --- a/docs/commands/str.md +++ b/docs/commands/str.md @@ -1,6 +1,6 @@ # str -Consumes either a single value or a table and converts the provided data to a string and optionally applies a change. +Applies the subcommand to a value or a table. ## Examples @@ -12,28 +12,28 @@ Consumes either a single value or a table and converts the provided data to a st 0 │ X │ filesystem │ /home/TUX/stuff/expr/stuff 1 │ │ filesystem │ / ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> shells | str path --upcase +> shells | str upcase path ━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # │ │ name │ path ───┼───┼────────────┼──────────────────────────────── 0 │ X │ filesystem │ /HOME/TUX/STUFF/EXPR/STUFF 1 │ │ filesystem │ / ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> shells | str path --downcase +> shells | str downcase path ━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # │ │ name │ path ───┼───┼────────────┼──────────────────────────────── 0 │ X │ filesystem │ /home/tux/stuff/expr/stuff 1 │ │ filesystem │ / ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> shells | str # --substring "21, 99" +> shells | str substring "21, 99" path ━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # │ │ name │ path ───┼───┼────────────┼──────────────────────────────── 0 │ X │ filesystem │ stuff 1 │ │ filesystem │ ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> shells | str # --substring "6," +> shells | str substring "6," path ━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # │ │ name │ path ───┼───┼────────────┼──────────────────────────────── @@ -41,27 +41,27 @@ Consumes either a single value or a table and converts the provided data to a st 1 │ │ filesystem │ ━━━┷━━━┷━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -> echo "1, 2, 3" | split-row "," | str --to-int | sum +> echo "1, 2, 3" | split row "," | str to-int | sum ━━━━━━━━━ ───────── 6 ━━━━━━━━━ -> echo "nu" | str --capitalize +> echo "nu" | str capitalize ━━━━━━━━━ ───────── Nu ━━━━━━━━━ -> echo "Nu " | str --trim +> echo "Nu " | str trim ━━━━━━━━━ ───────── Nu ━━━━━━━━━ -> shells | str path --find-replace ["TUX" "skipper"] +> shells | str find-replace "TUX" "skipper" path ━━━┯━━━┯━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # │ │ name │ path ───┼───┼────────────┼──────────────────────────────── diff --git a/src/plugins/nu_plugin_core_str.rs b/src/plugins/nu_plugin_core_str.rs deleted file mode 100644 index 5ff87af4c8..0000000000 --- a/src/plugins/nu_plugin_core_str.rs +++ /dev/null @@ -1,6 +0,0 @@ -use nu_plugin::serve_plugin; -use nu_plugin_str::Str; - -fn main() { - serve_plugin(&mut Str::new()); -} diff --git a/tests/plugins/core_str.rs b/tests/plugins/core_str.rs deleted file mode 100644 index 22da0821f5..0000000000 --- a/tests/plugins/core_str.rs +++ /dev/null @@ -1,222 +0,0 @@ -use nu_test_support::fs::Stub::FileWithContent; -use nu_test_support::playground::Playground; -use nu_test_support::{nu, pipeline}; - -#[test] -fn can_only_apply_one() { - let actual = nu!( - cwd: "tests/fixtures/formats", - "open caco3_plastics.csv | first 1 | str origin --downcase --upcase" - ); - - assert!(actual.err.contains(r#"--capitalize|--downcase|--upcase|--to-int|--to-float|--substring "start,end"|--replace|--find-replace [pattern replacement]|to-date-time|--trim]"#)); -} - -#[test] -fn acts_without_passing_field() { - Playground::setup("plugin_str_test_1", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.yml", - r#" - environment: - global: - PROJECT_NAME: nushell - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), - "open sample.yml | get environment.global.PROJECT_NAME | str --upcase | echo $it" - ); - - assert_eq!(actual.out, "NUSHELL"); - }) -} - -#[test] -fn trims() { - Playground::setup("plugin_str_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [dependency] - name = "nu " - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), - "open sample.toml | str dependency.name --trim | get dependency.name | echo $it" - ); - - assert_eq!(actual.out, "nu"); - }) -} - -#[test] -fn capitalizes() { - Playground::setup("plugin_str_test_3", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [dependency] - name = "nu" - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), - "open sample.toml | str dependency.name --capitalize | get dependency.name | echo $it" - ); - - assert_eq!(actual.out, "Nu"); - }) -} - -#[test] -fn downcases() { - Playground::setup("plugin_str_test_4", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [dependency] - name = "LIGHT" - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), - "open sample.toml | str dependency.name -d | get dependency.name | echo $it" - ); - - assert_eq!(actual.out, "light"); - }) -} - -#[test] -fn upcases() { - Playground::setup("plugin_str_test_5", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [package] - name = "nushell" - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), - "open sample.toml | str package.name --upcase | get package.name | echo $it" - ); - - assert_eq!(actual.out, "NUSHELL"); - }) -} - -#[test] -fn converts_to_int() { - let actual = nu!( - cwd: "tests/fixtures/formats", pipeline( - r#" - echo '{number_as_string: "1"}' - | from json - | str number_as_string --to-int - | rename number - | where number == 1 - | get number - | echo $it - "# - )); - - assert_eq!(actual.out, "1"); -} - -#[test] -fn converts_to_float() { - let actual = nu!( - cwd: "tests/fixtures/formats", pipeline( - r#" - echo "3.1, 0.0415" - | split row "," - | str --to-float - | sum - "# - )); - - assert_eq!(actual.out, "3.1415"); -} - -#[test] -fn replaces() { - Playground::setup("plugin_str_test_5", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [package] - name = "nushell" - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open sample.toml - | str package.name --replace wykittenshell - | get package.name - | echo $it - "# - )); - - assert_eq!(actual.out, "wykittenshell"); - }) -} - -#[test] -fn find_and_replaces() { - 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(), pipeline( - r#" - open sample.toml - | str fortune.teller.phone --find-replace [KATZ "5289"] - | get fortune.teller.phone - | echo $it - "# - )); - - assert_eq!(actual.out, "1-800-5289"); - }) -} - -#[test] -fn find_and_replaces_without_passing_field() { - Playground::setup("plugin_str_test_7", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContent( - "sample.toml", - r#" - [fortune.teller] - phone = "1-800-KATZ" - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open sample.toml - | get fortune.teller.phone - | str --find-replace [KATZ "5289"] - | echo $it - "# - )); - - assert_eq!(actual.out, "1-800-5289"); - }) -} diff --git a/tests/plugins/mod.rs b/tests/plugins/mod.rs index bfb41ea20a..e3d59b2205 100644 --- a/tests/plugins/mod.rs +++ b/tests/plugins/mod.rs @@ -1,2 +1 @@ mod core_inc; -mod core_str;