Repository: casey/just
Pull request #1383 (2022-10-27 03:14:35)

Add case-conversion functions (#1376)
As per #1376, adds a thin wrapper to [heck](https://github.com/withoutboats/heck)'s case conversion utilities, along with straightforward tests to each one.

Hopefully it ends up being useful! :)


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9358f3ee..8b1b515e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -316,6 +316,7 @@ dependencies = [
  "edit-distance",
  "env_logger",
  "executable-path",
+ "heck 0.4.0",
  "lazy_static",
  "lexiclean",
  "libc",
diff --git a/Cargo.toml b/Cargo.toml
index 01f9ce09..a0a06fed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ derivative = "2.0.0"
 dotenv = "0.15.0"
 edit-distance = "2.0.0"
 env_logger = "0.9.0"
+heck = "0.4.0"
 lazy_static = "1.0.0"
 lexiclean = "0.0.1"
 libc = "0.2.0"
diff --git a/README.md b/README.md
index 67823d6c..2ef98bcd 100644
--- a/README.md
+++ b/README.md
@@ -1145,12 +1145,24 @@ The executable is at: /bin/just
 
 - `capitalize(s)`<sup>master</sup> - Convert first character of `s` to uppercase and the rest to lowercase.
 
+- `kebabcase(s)`<sup>master</sup> - Convert `s` to `kebab-case`.
+
+- `lowercamelcase(s)`<sup>master</sup> - Convert `s` to `lowerCamelCase`.
+
 - `lowercase(s)` - Convert `s` to lowercase.
 
 - `quote(s)` - Replace all single quotes with `'\''` and prepend and append single quotes to `s`. This is sufficient to escape special characters for many shells, including most Bourne shell descendants.
 
 - `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`.
 
+- `shoutykebabcase(s)`<sup>master</sup> - Convert `s` to `SHOUTY-KEBAB-CASE`.
+
+- `shoutysnakecase(s)`<sup>master</sup> - Convert `s` to `SHOUTY_SNAKE_CASE`.
+
+- `snakecase(s)`<sup>master</sup> - Convert `s` to `snake_case`.
+
+- `titlecase(s)`<sup>master</sup> - Convert `s` to `Title Case`.
+
 - `trim(s)` - Remove leading and trailing whitespace from `s`.
 
 - `trim_end(s)` - Remove trailing whitespace from `s`.
@@ -1167,6 +1179,8 @@ The executable is at: /bin/just
 
 - `uppercase(s)` - Convert `s` to uppercase.
 
+- `uppercamelcase(s)`<sup>master</sup> - Convert `s` to `UpperCamelCase`.
+
 #### Path Manipulation
 
 ##### Fallible
diff --git a/extras/just.sublime-syntax b/extras/just.sublime-syntax
index b5525a69..fc51a817 100644
--- a/extras/just.sublime-syntax
+++ b/extras/just.sublime-syntax
@@ -31,7 +31,7 @@ contexts:
         - match: '\}\}'
           pop: true
   functions:
-    - match: \b(arch|os|os_family|env_var|env_var_or_default|invocation_directory|justfile|justfile_directory|just_executable|lowercase|quote|replace|trim|trim_end|trim_end_match|trim_end_matches|trim_start|trim_start_match|trim_start_matches|uppercase|absolute_path|extension|file_name|file_stem|parent_directory|without_extension|join|clean|path_exists|error|sha256|sha256_file|uuid|capitalize)\b(?=\()
+    - match: \b(arch|os|os_family|env_var|env_var_or_default|invocation_directory|justfile|justfile_directory|just_executable|lowercase|quote|replace|trim|trim_end|trim_end_match|trim_end_matches|trim_start|trim_start_match|trim_start_matches|uppercase|absolute_path|extension|file_name|file_stem|parent_directory|without_extension|join|clean|path_exists|error|sha256|sha256_file|uuid|capitalize|uppercamelcase|lowercamelcase|snakecase|kebabcase|shoutysnakecase|titlecase|shoutykebabcase)\b(?=\()
       scope: entity.name.function.just
   keywords:
     - match: \b(if|else|while)\b
diff --git a/src/function.rs b/src/function.rs
index 445adf1c..461384fb 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -3,6 +3,11 @@
 
 use super::*;
 
+use heck::{
+  ToKebabCase, ToLowerCamelCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase,
+  ToUpperCamelCase,
+};
+
 use Function::*;
 pub(crate) enum Function {
   Nullary(fn(&FunctionContext) -> Result<String, String>),
@@ -29,6 +34,8 @@ lazy_static! {
     ("just_executable", Nullary(just_executable)),
     ("justfile", Nullary(justfile)),
     ("justfile_directory", Nullary(justfile_directory)),
+    ("kebabcase", Unary(kebabcase)),
+    ("lowercamelcase", Unary(lowercamelcase)),
     ("lowercase", Unary(lowercase)),
     ("os", Nullary(os)),
     ("os_family", Nullary(os_family)),
@@ -38,6 +45,10 @@ lazy_static! {
     ("replace", Ternary(replace)),
     ("sha256", Unary(sha256)),
     ("sha256_file", Unary(sha256_file)),
+    ("shoutykebabcase", Unary(shoutykebabcase)),
+    ("shoutysnakecase", Unary(shoutysnakecase)),
+    ("snakecase", Unary(snakecase)),
+    ("titlecase", Unary(titlecase)),
     ("trim", Unary(trim)),
     ("trim_end", Unary(trim_end)),
     ("trim_end_match", Binary(trim_end_match)),
@@ -45,6 +56,7 @@ lazy_static! {
     ("trim_start", Unary(trim_start)),
     ("trim_start_match", Binary(trim_start_match)),
     ("trim_start_matches", Binary(trim_start_matches)),
+    ("uppercamelcase", Unary(uppercamelcase)),
     ("uppercase", Unary(uppercase)),
     ("uuid", Nullary(uuid)),
     ("without_extension", Unary(without_extension)),
@@ -225,6 +237,14 @@ fn justfile_directory(context: &FunctionContext) -> Result<String, String> {
     })
 }
 
+fn kebabcase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_kebab_case())
+}
+
+fn lowercamelcase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_lower_camel_case())
+}
+
 fn lowercase(_context: &FunctionContext, s: &str) -> Result<String, String> {
   Ok(s.to_lowercase())
 }
@@ -283,6 +303,22 @@ fn sha256_file(context: &FunctionContext, path: &str) -> Result<String, String>
   Ok(format!("{:x}", hash))
 }
 
+fn shoutykebabcase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_shouty_kebab_case())
+}
+
+fn shoutysnakecase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_shouty_snake_case())
+}
+
+fn snakecase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_snake_case())
+}
+
+fn titlecase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_title_case())
+}
+
 fn trim(_context: &FunctionContext, s: &str) -> Result<String, String> {
   Ok(s.trim().to_owned())
 }
@@ -311,6 +347,10 @@ fn trim_start_matches(_context: &FunctionContext, s: &str, pat: &str) -> Result<
   Ok(s.trim_start_matches(pat).to_owned())
 }
 
+fn uppercamelcase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_upper_camel_case())
+}
+
 fn uppercase(_context: &FunctionContext, s: &str) -> Result<String, String> {
   Ok(s.to_uppercase())
 }


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index f2c2ae8a..6a7ae0c5 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -283,6 +283,76 @@ test! {
   stderr: "echo bar\n",
 }
 
+test! {
+  name: uppercamelcase,
+  justfile: "
+    foo:
+      echo {{ uppercamelcase('foo bar') }}
+  ",
+  stdout: "FooBar\n",
+  stderr: "echo FooBar\n",
+}
+
+test! {
+  name: lowercamelcase,
+  justfile: "
+    foo:
+      echo {{ lowercamelcase('foo bar') }}
+  ",
+  stdout: "fooBar\n",
+  stderr: "echo fooBar\n",
+}
+
+test! {
+  name: snakecase,
+  justfile: "
+    foo:
+      echo {{ snakecase('foo bar') }}
+  ",
+  stdout: "foo_bar\n",
+  stderr: "echo foo_bar\n",
+}
+
+test! {
+  name: kebabcase,
+  justfile: "
+    foo:
+      echo {{ kebabcase('foo bar') }}
+  ",
+  stdout: "foo-bar\n",
+  stderr: "echo foo-bar\n",
+}
+
+test! {
+  name: shoutysnakecase,
+  justfile: "
+    foo:
+      echo {{ shoutysnakecase('foo bar') }}
+  ",
+  stdout: "FOO_BAR\n",
+  stderr: "echo FOO_BAR\n",
+}
+
+test! {
+  name: titlecase,
+  justfile: "
+    foo:
+      echo {{ titlecase('foo bar') }}
+  ",
+  stdout: "Foo Bar\n",
+  stderr: "echo Foo Bar\n",
+}
+
+test! {
+  name: shoutykebabcase,
+  justfile: "
+    foo:
+      echo {{ shoutykebabcase('foo bar') }}
+  ",
+  stdout: "FOO-BAR\n",
+  stderr: "echo FOO-BAR\n",
+}
+
 test! {
   name: trim,
   justfile: "