Repository: nushell/engine-q
Pull request #287 (2021-11-04 20:32:39)

Port str case commands
Still need to support the column paths. I guess maybe we should figure how to do this soon :)

jntrnr: Agreed, will see if we can add that first, so we don't have so many Todos 😅
jntrnr: Taking a look now
jntrnr: @onthebridgetonowhere - okay, I added some support for paths to columns: https://github.com/nushell/engine-q/pull/290
onthebridgetonowhere: Thanks, I'll have a look!

jntrnr: This looks great! Can you also throw a test in the sec/tests.rs file (or as an example to a command) that tests the optional cell path?
onthebridgetonowhere: Added a `cell path` test for each `str case` command in `src/tests.rs`. Should be good to go now :-)
jntrnr: looks good!

Patch:
diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml
index f510b22..ee1470a 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 be85486..bd84aca 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/strings/mod.rs b/crates/nu-command/src/strings/mod.rs
index a23eba5..f409575 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 0000000..9c20193
--- /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<PipelineData, ShellError> {
+        operate(engine_state, stack, call, input, &to_camel_case)
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 0000000..80714cb
--- /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<nu_protocol::PipelineData, nu_protocol::ShellError> {
+        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 0000000..9cc6f9c
--- /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<PipelineData, ShellError> {
+        operate(engine_state, stack, call, input, &to_kebab_case)
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 0000000..129674f
--- /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<F>(
+    engine_state: &EngineState,
+    stack: &mut Stack,
+    call: &Call,
+    input: PipelineData,
+    case_operation: &'static F,
+) -> Result<PipelineData, ShellError>
+where
+    F: Fn(&str) -> String + Send + Sync + 'static,
+{
+    let head = call.head;
+    let column_paths: Vec<CellPath> = 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<F>(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 0000000..5e6deaf
--- /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<PipelineData, ShellError> {
+        operate(engine_state, stack, call, input, &to_pascal_case)
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 0000000..ef178e6
--- /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<PipelineData, ShellError> {
+        operate(engine_state, stack, call, input, &to_screaming_snake_case)
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 0000000..f5bc419
--- /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<PipelineData, ShellError> {
+        operate(engine_state, stack, call, input, &to_snake_case)
+    }
+
+    fn examples(&self) -> Vec<Example> {
+        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 0000000..a8f9c53
--- /dev/null
+++ b/crates/nu-command/src/strings/str_/mod.rs
@@ -0,0 +1,3 @@
+mod case;
+
+pub use case::*;


Test patch:
diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs
index 895dec4..d11ada8 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));