diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index f510b22f99..ee1470a882 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -19,6 +19,7 @@ unicode-segmentation = "1.8.0" # Potential dependencies for extras glob = "0.3.0" +Inflector = "0.11" thiserror = "1.0.29" sysinfo = "0.20.4" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index be85486ff4..bd84acafc0 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -25,6 +25,7 @@ pub fn create_default_context() -> EngineState { Alias, Benchmark, BuildString, + CamelCase, Cd, Cp, Date, @@ -47,6 +48,7 @@ pub fn create_default_context() -> EngineState { FromJson, Get, Griddle, + KebabCase, Help, Hide, If, @@ -80,12 +82,15 @@ pub fn create_default_context() -> EngineState { Mkdir, Module, Mv, + PascalCase, ParEach, Ps, Range, Rm, Select, Size, + ScreamingSnakeCase, + SnakeCase, Split, SplitChars, SplitColumn, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 895dec4cc0..d11ada8a0e 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -7,7 +7,7 @@ use nu_protocol::{ use crate::To; -use super::{Date, From, Into, Math, Split}; +use super::{Case, Date, From, Into, Math, Split}; pub fn test_examples(cmd: impl Command + 'static) { let examples = cmd.examples(); @@ -17,6 +17,7 @@ pub fn test_examples(cmd: impl Command + 'static) { // Base functions that are needed for testing // Try to keep this working set small to keep tests running as fast as possible let mut working_set = StateWorkingSet::new(&*engine_state); + working_set.add_decl(Box::new(Case)); working_set.add_decl(Box::new(From)); working_set.add_decl(Box::new(To)); working_set.add_decl(Box::new(Into)); diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index a23eba5ff0..f409575e2d 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -2,8 +2,10 @@ mod build_string; mod format; mod size; mod split; +mod str_; pub use build_string::BuildString; pub use format::*; pub use size::Size; pub use split::*; +pub use str_::*; diff --git a/crates/nu-command/src/strings/str_/case/camel_case.rs b/crates/nu-command/src/strings/str_/case/camel_case.rs new file mode 100644 index 0000000000..9c201937eb --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/camel_case.rs @@ -0,0 +1,96 @@ +use inflector::cases::camelcase::to_camel_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str camel-case" + } + + fn signature(&self) -> Signature { + Signature::build("str camel-case").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to camelCase by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to camelCase" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_camel_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: " 'NuShell' | str camel-case", + result: Some(Value::String { + val: "nuShell".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: "'this-is-the-first-case' | str camel-case", + result: Some(Value::String { + val: "thisIsTheFirstCase".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: " 'this_is_the_second_case' | str camel-case", + result: Some(Value::String { + val: "thisIsTheSecondCase".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a column from a table to camelCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str camel-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::unknown(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nuTest".to_string(), + span: Span::unknown(), + }, + Value::test_int(100), + ], + }], + span: Span::unknown(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/command.rs b/crates/nu-command/src/strings/str_/case/command.rs new file mode 100644 index 0000000000..80714cbd77 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/command.rs @@ -0,0 +1,49 @@ +use nu_engine::get_full_help; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + IntoPipelineData, PipelineData, Signature, Value, +}; + +#[derive(Clone)] +pub struct Case; + +impl Command for Case { + fn name(&self) -> &str { + "str" + } + + fn signature(&self) -> Signature { + Signature::build("str") + } + + fn usage(&self) -> &str { + "Converts strings into different kind of cases: camel, kebab, pascal, snake, and screaming snake." + } + + fn run( + &self, + engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Ok(Value::String { + val: get_full_help(&Case.signature(), &Case.examples(), engine_state), + span: call.head, + } + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Case {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/kebab_case.rs b/crates/nu-command/src/strings/str_/case/kebab_case.rs new file mode 100644 index 0000000000..9cc6f9c154 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/kebab_case.rs @@ -0,0 +1,95 @@ +use inflector::cases::kebabcase::to_kebab_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str kebab-case" + } + + fn signature(&self) -> Signature { + Signature::build("str kebab-case").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to kebab-case by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to kebab-case" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_kebab_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to kebab-case", + example: "'NuShell' | str kebab-case", + result: Some(Value::String { + val: "nu-shell".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to kebab-case", + example: "'thisIsTheFirstCase' | str kebab-case", + result: Some(Value::String { + val: "this-is-the-first-case".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to kebab-case", + example: "'THIS_IS_THE_SECOND_CASE' | str kebab-case", + result: Some(Value::String { + val: "this-is-the-second-case".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a column from a table to kebab-case", + example: r#"[[lang, gems]; [nuTest, 100]] | str kebab-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::unknown(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nu-test".to_string(), + span: Span::unknown(), + }, + Value::test_int(100), + ], + }], + span: Span::unknown(), + }), + }, + ] + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/mod.rs b/crates/nu-command/src/strings/str_/case/mod.rs new file mode 100644 index 0000000000..129674fe3e --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/mod.rs @@ -0,0 +1,75 @@ +pub mod camel_case; +pub mod command; +pub mod kebab_case; +pub mod pascal_case; +pub mod screaming_snake_case; +pub mod snake_case; + +pub use camel_case::SubCommand as CamelCase; +pub use command::Case; +pub use kebab_case::SubCommand as KebabCase; +pub use pascal_case::SubCommand as PascalCase; +pub use screaming_snake_case::SubCommand as ScreamingSnakeCase; +pub use snake_case::SubCommand as SnakeCase; + +use nu_engine::CallExt; + +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{EngineState, Stack}; +use nu_protocol::{PipelineData, ShellError, Span, Value}; + +pub fn operate( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + case_operation: &'static F, +) -> Result +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + let head = call.head; + let column_paths: Vec = call.rest(engine_state, stack, 0)?; + + input.map( + move |v| { + if column_paths.is_empty() { + action(&v, case_operation, head) + } else { + let mut ret = v; + for path in &column_paths { + let r = ret.update_cell_path( + &path.members, + Box::new(move |old| action(old, case_operation, head)), + ); + if let Err(error) = r { + return Value::Error { error }; + } + } + ret + } + }, + engine_state.ctrlc.clone(), + ) +} + +pub fn action(input: &Value, case_operation: &F, head: Span) -> Value +where + F: Fn(&str) -> String + Send + Sync + 'static, +{ + match input { + Value::String { val, .. } => Value::String { + val: case_operation(val), + span: head, + }, + other => Value::Error { + error: ShellError::UnsupportedInput( + format!( + "Input's type is {}. This command only works with strings.", + other.get_type() + ), + Span::unknown(), + ), + }, + } +} diff --git a/crates/nu-command/src/strings/str_/case/pascal_case.rs b/crates/nu-command/src/strings/str_/case/pascal_case.rs new file mode 100644 index 0000000000..5e6deaf40a --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/pascal_case.rs @@ -0,0 +1,96 @@ +use inflector::cases::pascalcase::to_pascal_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +use crate::operate; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str pascal-case" + } + + fn signature(&self) -> Signature { + Signature::build("str pascal-case").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to PascalCase by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to PascalCase" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_pascal_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to PascalCase", + example: "'nu-shell' | str pascal-case", + result: Some(Value::String { + val: "NuShell".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to PascalCase", + example: "'this-is-the-first-case' | str pascal-case", + result: Some(Value::String { + val: "ThisIsTheFirstCase".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to PascalCase", + example: "'this_is_the_second_case' | str pascal-case", + result: Some(Value::String { + val: "ThisIsTheSecondCase".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a column from a table to PascalCase", + example: r#"[[lang, gems]; [nu_test, 100]] | str pascal-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::unknown(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "NuTest".to_string(), + span: Span::unknown(), + }, + Value::test_int(100), + ], + }], + span: Span::unknown(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs b/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs new file mode 100644 index 0000000000..ef178e66b2 --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/screaming_snake_case.rs @@ -0,0 +1,95 @@ +use inflector::cases::screamingsnakecase::to_screaming_snake_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +use crate::operate; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str screaming-snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str screaming-snake-case").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to SCREAMING_SNAKE_CASE by column paths", + ) + } + + fn usage(&self) -> &str { + "converts a string to SCREAMING_SNAKE_CASE" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_screaming_snake_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: r#" "NuShell" | str screaming-snake-case"#, + result: Some(Value::String { + val: "NU_SHELL".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#" "this_is_the_second_case" | str screaming-snake-case"#, + result: Some(Value::String { + val: "THIS_IS_THE_SECOND_CASE".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#""this-is-the-first-case" | str screaming-snake-case"#, + result: Some(Value::String { + val: "THIS_IS_THE_FIRST_CASE".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a column from a table to SCREAMING_SNAKE_CASE", + example: r#"[[lang, gems]; [nu_test, 100]] | str screaming-snake-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::unknown(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "NU_TEST".to_string(), + span: Span::unknown(), + }, + Value::test_int(100), + ], + }], + span: Span::unknown(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/case/snake_case.rs b/crates/nu-command/src/strings/str_/case/snake_case.rs new file mode 100644 index 0000000000..f5bc419ddb --- /dev/null +++ b/crates/nu-command/src/strings/str_/case/snake_case.rs @@ -0,0 +1,94 @@ +use inflector::cases::snakecase::to_snake_case; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value}; + +use crate::operate; +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "str snake-case" + } + + fn signature(&self) -> Signature { + Signature::build("str snake-case").rest( + "rest", + SyntaxShape::CellPath, + "optionally convert text to snake_case by column paths", + ) + } + fn usage(&self) -> &str { + "converts a string to snake_case" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + operate(engine_state, stack, call, input, &to_snake_case) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "convert a string to camelCase", + example: r#" "NuShell" | str snake-case"#, + result: Some(Value::String { + val: "nu_shell".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#" "this_is_the_second_case" | str snake-case"#, + result: Some(Value::String { + val: "this_is_the_second_case".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a string to camelCase", + example: r#""this-is-the-first-case" | str snake-case"#, + result: Some(Value::String { + val: "this_is_the_first_case".to_string(), + span: Span::unknown(), + }), + }, + Example { + description: "convert a column from a table to snake-case", + example: r#"[[lang, gems]; [nuTest, 100]] | str snake-case lang"#, + result: Some(Value::List { + vals: vec![Value::Record { + span: Span::unknown(), + cols: vec!["lang".to_string(), "gems".to_string()], + vals: vec![ + Value::String { + val: "nu_test".to_string(), + span: Span::unknown(), + }, + Value::test_int(100), + ], + }], + span: Span::unknown(), + }), + }, + ] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/strings/str_/mod.rs b/crates/nu-command/src/strings/str_/mod.rs new file mode 100644 index 0000000000..a8f9c53892 --- /dev/null +++ b/crates/nu-command/src/strings/str_/mod.rs @@ -0,0 +1,3 @@ +mod case; + +pub use case::*;