Repository: casey/just
Pull request #765 (2021-03-25 02:40:52)

Allow escaping double braces with `{{{{`


Patch:
diff --git a/README.adoc b/README.adoc
index e61e1fd7..70e00d5f 100644
--- a/README.adoc
+++ b/README.adoc
@@ -446,11 +446,11 @@ publish:
 
 ==== Escaping `{{`
 
-To write a recipe containing `{{`, use `{{ "{{" }}`:
+To write a recipe containing `{{`, use `{{{{`:
 
 ```make
 braces:
-    echo 'I {{ "{{" }}LOVE}} curly braces!'
+    echo 'I {{{{LOVE}} curly braces!'
 ```
 
 (An unmatched `}}` is ignored, so it doesn't need to be escaped.)
@@ -462,6 +462,13 @@ braces:
     echo '{{'I {{LOVE}} curly braces!'}}'
 ```
 
+Yet another option is to use `{{ "{{" }}`:
+
+```make
+braces:
+    echo 'I {{ "{{" }}LOVE}} curly braces!'
+```
+
 === Strings
 
 Double-quoted strings support escape sequences:
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 70da6057..6b382be5 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -169,12 +169,15 @@ impl<'src, 'run> Evaluator<'src, 'run> {
     let mut evaluated = String::new();
     for (i, fragment) in line.fragments.iter().enumerate() {
       match fragment {
-        Fragment::Text { token } =>
+        Fragment::Text { token } => {
+          let lexeme = token.lexeme().replace("{{{{", "{{");
+
           if i == 0 && continued {
-            evaluated += token.lexeme().trim_start();
+            evaluated += lexeme.trim_start();
           } else {
-            evaluated += token.lexeme();
-          },
+            evaluated += &lexeme;
+          }
+        },
         Fragment::Interpolation { expression } => {
           evaluated += &self.evaluate_expression(expression)?;
         },
diff --git a/src/lexer.rs b/src/lexer.rs
index 4be599a1..5857e1fa 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -91,6 +91,15 @@ impl<'src> Lexer<'src> {
     }
   }
 
+  /// Advance over N characters.
+  fn skip(&mut self, n: usize) -> CompilationResult<'src, ()> {
+    for _ in 0..n {
+      self.advance()?;
+    }
+
+    Ok(())
+  }
+
   /// Lexeme of in-progress token
   fn lexeme(&self) -> &'src str {
     &self.src[self.token_start.offset..self.token_end.offset]
@@ -515,6 +524,11 @@ impl<'src> Lexer<'src> {
     use Terminator::*;
 
     let terminator = loop {
+      if self.rest_starts_with("{{{{") {
+        self.skip(4)?;
+        continue;
+      }
+
       if let Some('\n') = self.next {
         break Newline;
       }
@@ -1239,6 +1253,23 @@ mod tests {
     )
   }
 
+  test! {
+    name: brace_escape,
+    text: "
+      foo:
+        {{{{
+    ",
+    tokens: (
+      Identifier:"foo",
+      Colon,
+      Eol,
+      Indent,
+      Text:"{{{{",
+      Eol,
+      Dedent,
+    )
+  }
+
   test! {
     name: indented_block_followed_by_item,
     text: "


Test patch:
diff --git a/tests/misc.rs b/tests/misc.rs
index e4278715..36e2e085 100644
--- a/tests/misc.rs
+++ b/tests/misc.rs
@@ -2600,6 +2600,30 @@ test! {
   shell: false,
 }
 
+test! {
+  name: brace_escape,
+  justfile: "
+    foo:
+      echo '{{{{'
+  ",
+  stdout: "{{\n",
+  stderr: "
+    echo '{{'
+  ",
+}
+
+test! {
+  name: brace_escape_extra,
+  justfile: "
+    foo:
+      echo '{{{{{'
+  ",
+  stdout: "{{{\n",
+  stderr: "
+    echo '{{{'
+  ",
+}
+
 #[cfg(windows)]
 test! {
   name: windows_interpreter_path_no_base,