Repository: casey/just
Pull request #888 (2021-07-03 05:22:38)

Implement four string manipulation functions
This PR implements the following functions:

- `uppercase(s)`
- `lowercase(s)`
- `trim(s)`
- `replace(s, from, to)`

In addition, this adds support for another function variant `Ternary`.

casey: This looks great. The only thing this needs is documentation. I think a new subsection in the `Functions` of the readme would be good, titled `String Manipulation`.

Patch:
diff --git a/README.adoc b/README.adoc
index e82689ec..e7fa32d1 100644
--- a/README.adoc
+++ b/README.adoc
@@ -764,6 +764,13 @@ $ just
 The executable is at: /bin/just
 ```
 
+==== String Manipulation
+
+- `uppercase(s)` - Convert `s` to uppercase.
+- `lowercase(s)` - Convert `s` to lowercase.
+- `trim(s)` - Remove leading and trailing whitespace from `s`.
+- `replace(s, from, to)` - Replace all occurrences of `from` in `s` to `to`.
+
 ==== Dotenv Integration
 
 `just` will load environment variables from a file named `.env`. This file can be located in the same directory as your justfile or in a parent directory. These variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.
diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs
index e6db4b2d..91928500 100644
--- a/src/assignment_resolver.rs
+++ b/src/assignment_resolver.rs
@@ -82,6 +82,13 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
           self.resolve_expression(a)?;
           self.resolve_expression(b)
         },
+        Thunk::Ternary {
+          args: [a, b, c], ..
+        } => {
+          self.resolve_expression(a)?;
+          self.resolve_expression(b)?;
+          self.resolve_expression(c)
+        },
       },
       Expression::Concatination { lhs, rhs } => {
         self.resolve_expression(lhs)?;
diff --git a/src/evaluator.rs b/src/evaluator.rs
index f259caba..e19a1cca 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -105,6 +105,21 @@ impl<'src, 'run> Evaluator<'src, 'run> {
             function: *name,
             message,
           }),
+          Ternary {
+            name,
+            function,
+            args: [a, b, c],
+            ..
+          } => function(
+            &context,
+            &self.evaluate_expression(a)?,
+            &self.evaluate_expression(b)?,
+            &self.evaluate_expression(c)?,
+          )
+          .map_err(|message| RuntimeError::FunctionCall {
+            function: *name,
+            message,
+          }),
         }
       },
       Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
diff --git a/src/function.rs b/src/function.rs
index c0cc9db3..80394c11 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -5,6 +5,7 @@ pub(crate) enum Function {
   Nullary(fn(&FunctionContext) -> Result<String, String>),
   Unary(fn(&FunctionContext, &str) -> Result<String, String>),
   Binary(fn(&FunctionContext, &str, &str) -> Result<String, String>),
+  Ternary(fn(&FunctionContext, &str, &str, &str) -> Result<String, String>),
 }
 
 lazy_static! {
@@ -21,9 +22,13 @@ lazy_static! {
     ("just_executable", Nullary(just_executable)),
     ("justfile", Nullary(justfile)),
     ("justfile_directory", Nullary(justfile_directory)),
+    ("lowercase", Unary(lowercase)),
     ("os", Nullary(os)),
     ("os_family", Nullary(os_family)),
     ("parent_directory", Unary(parent_directory)),
+    ("replace", Ternary(replace)),
+    ("trim", Unary(trim)),
+    ("uppercase", Unary(uppercase)),
     ("without_extension", Unary(without_extension)),
   ]
   .into_iter()
@@ -36,6 +41,7 @@ impl Function {
       Nullary(_) => 0,
       Unary(_) => 1,
       Binary(_) => 2,
+      Ternary(_) => 3,
     }
   }
 }
@@ -164,6 +170,10 @@ fn justfile_directory(context: &FunctionContext) -> Result<String, String> {
     })
 }
 
+fn lowercase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_lowercase())
+}
+
 fn os(_context: &FunctionContext) -> Result<String, String> {
   Ok(target::os().to_owned())
 }
@@ -179,6 +189,18 @@ fn parent_directory(_context: &FunctionContext, path: &str) -> Result<String, St
     .ok_or_else(|| format!("Could not extract parent directory from `{}`", path))
 }
 
+fn replace(_context: &FunctionContext, s: &str, from: &str, to: &str) -> Result<String, String> {
+  Ok(s.replace(from, to))
+}
+
+fn trim(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.trim().to_owned())
+}
+
+fn uppercase(_context: &FunctionContext, s: &str) -> Result<String, String> {
+  Ok(s.to_uppercase())
+}
+
 fn without_extension(_context: &FunctionContext, path: &str) -> Result<String, String> {
   let parent = Utf8Path::new(path)
     .parent()
diff --git a/src/node.rs b/src/node.rs
index aebfbbf4..ee666670 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -90,6 +90,16 @@ impl<'src> Node<'src> for Expression<'src> {
             tree.push_mut(a.tree());
             tree.push_mut(b.tree());
           },
+          Ternary {
+            name,
+            args: [a, b, c],
+            ..
+          } => {
+            tree.push_mut(name.lexeme());
+            tree.push_mut(a.tree());
+            tree.push_mut(b.tree());
+            tree.push_mut(c.tree());
+          },
         }
 
         tree
diff --git a/src/summary.rs b/src/summary.rs
index 9e4c7608..fbee4562 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -230,6 +230,14 @@ impl Expression {
           name:      name.lexeme().to_owned(),
           arguments: vec![Expression::new(a), Expression::new(b)],
         },
+        full::Thunk::Ternary {
+          name,
+          args: [a, b, c],
+          ..
+        } => Expression::Call {
+          name:      name.lexeme().to_owned(),
+          arguments: vec![Expression::new(a), Expression::new(b), Expression::new(c)],
+        },
       },
       Concatination { lhs, rhs } => Expression::Concatination {
         lhs: Box::new(Expression::new(lhs)),
diff --git a/src/thunk.rs b/src/thunk.rs
index bbc45c7f..5a50483c 100644
--- a/src/thunk.rs
+++ b/src/thunk.rs
@@ -20,6 +20,12 @@ pub(crate) enum Thunk<'src> {
     function: fn(&FunctionContext, &str, &str) -> Result<String, String>,
     args:     [Box<Expression<'src>>; 2],
   },
+  Ternary {
+    name:     Name<'src>,
+    #[derivative(Debug = "ignore", PartialEq = "ignore")]
+    function: fn(&FunctionContext, &str, &str, &str) -> Result<String, String>,
+    args:     [Box<Expression<'src>>; 3],
+  },
 }
 
 impl<'src> Thunk<'src> {
@@ -47,6 +53,16 @@ impl<'src> Thunk<'src> {
             name,
           })
         },
+        (Function::Ternary(function), 3) => {
+          let c = Box::new(arguments.pop().unwrap());
+          let b = Box::new(arguments.pop().unwrap());
+          let a = Box::new(arguments.pop().unwrap());
+          Ok(Thunk::Ternary {
+            function: *function,
+            args: [a, b, c],
+            name,
+          })
+        },
         _ => Err(
           name.error(CompilationErrorKind::FunctionArgumentCountMismatch {
             function: name.lexeme(),
@@ -72,6 +88,11 @@ impl Display for Thunk<'_> {
       Binary {
         name, args: [a, b], ..
       } => write!(f, "{}({}, {})", name.lexeme(), a, b),
+      Ternary {
+        name,
+        args: [a, b, c],
+        ..
+      } => write!(f, "{}({}, {}, {})", name.lexeme(), a, b, c),
     }
   }
 }


Test patch:
diff --git a/tests/functions.rs b/tests/functions.rs
index d4bcd27f..dc8be3ee 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -262,3 +262,43 @@ test! {
   stdout: "b\n",
   stderr: "echo b\n",
 }
+
+test! {
+  name: uppercase,
+  justfile: "
+    foo:
+      echo {{ uppercase('bar') }}
+  ",
+  stdout: "BAR\n",
+  stderr: "echo BAR\n",
+}
+
+test! {
+  name: lowercase,
+  justfile: "
+    foo:
+      echo {{ lowercase('BAR') }}
+  ",
+  stdout: "bar\n",
+  stderr: "echo bar\n",
+}
+
+test! {
+  name: trim,
+  justfile: "
+    foo:
+      echo {{ trim('   bar   ') }}
+  ",
+  stdout: "bar\n",
+  stderr: "echo bar\n",
+}
+
+test! {
+  name: replace,
+  justfile: "
+    foo:
+      echo {{ replace('barbarbar', 'bar', 'foo') }}
+  ",
+  stdout: "foofoofoo\n",
+  stderr: "echo foofoofoo\n",
+}