Repository: casey/just
Pull request #2054 (2024-05-18 23:04:59)

Add predefined constants


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 5ac4ca8a..cf032016 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,7 +12,7 @@ keywords = ["command-line", "task", "runner", "development", "utility"]
 license = "CC0-1.0"
 readme = "crates-io-readme.md"
 repository = "https://github.com/casey/just"
-rust-version = "1.63"
+rust-version = "1.70"
 
 [workspace]
 members = [".", "crates/*"]
diff --git a/README.md b/README.md
index bf52875e..06a51c59 100644
--- a/README.md
+++ b/README.md
@@ -1541,6 +1541,26 @@ and are implemented with the
 - `executable_directory()` - The user-specific executable directory.
 - `home_directory()` - The user's home directory.
 
+### Constants
+
+A number of constants are predefined:
+
+| Name | Value |
+|------|-------------|
+| `HEX`<sup>master</sup> | `"0123456789abcdef"` |
+| `HEXLOWER`<sup>master</sup> | `"0123456789abcdef"` |
+| `HEXUPPER`<sup>master</sup> | `"0123456789ABCDEF"` |
+
+```just
+@foo:
+  echo {{HEX}}
+```
+
+```sh
+$ just foo
+0123456789abcdef
+```
+
 ### Recipe Attributes
 
 Recipes may be annotated with attributes that change their behavior.
diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs
index 1835b4f0..04a89367 100644
--- a/src/assignment_resolver.rs
+++ b/src/assignment_resolver.rs
@@ -128,7 +128,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
       Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
       Expression::Variable { name } => {
         let variable = name.lexeme();
-        if self.evaluated.contains(variable) {
+        if self.evaluated.contains(variable) || constants().contains_key(variable) {
           Ok(())
         } else if self.stack.contains(&variable) {
           self.stack.push(variable);
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644
index 00000000..e9007ea7
--- /dev/null
+++ b/src/constants.rs
@@ -0,0 +1,15 @@
+use super::*;
+
+pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> {
+  static CONSTANTS: OnceLock<HashMap<&str, &str>> = OnceLock::new();
+
+  CONSTANTS.get_or_init(|| {
+    vec![
+      ("HEX", "0123456789abcdef"),
+      ("HEXLOWER", "0123456789abcdef"),
+      ("HEXUPPER", "0123456789ABCDEF"),
+    ]
+    .into_iter()
+    .collect()
+  })
+}
diff --git a/src/justfile.rs b/src/justfile.rs
index f8f24550..30985bf6 100644
--- a/src/justfile.rs
+++ b/src/justfile.rs
@@ -135,7 +135,7 @@ impl<'src> Justfile<'src> {
       BTreeMap::new()
     };
 
-    let root = Scope::new();
+    let root = Scope::root();
 
     let scope = self.scope(config, &dotenv, search, overrides, &root)?;
 
diff --git a/src/lexer.rs b/src/lexer.rs
index 4c252239..adbad1bb 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -231,11 +231,8 @@ impl<'src> Lexer<'src> {
     // The width of the error site to highlight depends on the kind of error:
     let length = match kind {
       UnterminatedString | UnterminatedBacktick => {
-        let kind = match StringKind::from_token_start(self.lexeme()) {
-          Some(kind) => kind,
-          None => {
-            return self.internal_error("Lexer::error: expected string or backtick token start")
-          }
+        let Some(kind) = StringKind::from_token_start(self.lexeme()) else {
+          return self.internal_error("Lexer::error: expected string or backtick token start");
         };
         kind.delimiter().len()
       }
@@ -813,9 +810,7 @@ impl<'src> Lexer<'src> {
   /// Cooked string: "[^"]*" # also processes escape sequences
   /// Raw string:    '[^']*'
   fn lex_string(&mut self) -> CompileResult<'src> {
-    let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
-      kind
-    } else {
+    let Some(kind) = StringKind::from_token_start(self.rest()) else {
       self.advance()?;
       return Err(self.internal_error("Lexer::lex_string: invalid string start"));
     };
diff --git a/src/lib.rs b/src/lib.rs
index f3f0d0f1..8616e13a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -20,9 +20,9 @@ pub(crate) use {
     color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
     compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
     condition::Condition, conditional_operator::ConditionalOperator, config::Config,
-    config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
-    dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
-    expression::Expression, fragment::Fragment, function::Function,
+    config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter,
+    dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error,
+    evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
     interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
     justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
     load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
@@ -53,7 +53,7 @@ pub(crate) use {
     process::{self, Command, ExitStatus, Stdio},
     rc::Rc,
     str::{self, Chars},
-    sync::{Mutex, MutexGuard},
+    sync::{Mutex, MutexGuard, OnceLock},
     vec,
   },
   {
@@ -128,6 +128,7 @@ mod condition;
 mod conditional_operator;
 mod config;
 mod config_error;
+mod constants;
 mod count;
 mod delimiter;
 mod dependency;
diff --git a/src/ran.rs b/src/ran.rs
index a19c12db..7751e78d 100644
--- a/src/ran.rs
+++ b/src/ran.rs
@@ -8,8 +8,7 @@ impl<'src> Ran<'src> {
     self
       .0
       .get(recipe)
-      .map(|ran| ran.contains(arguments))
-      .unwrap_or_default()
+      .is_some_and(|ran| ran.contains(arguments))
   }
 
   pub(crate) fn ran(&mut self, recipe: &Namepath<'src>, arguments: Vec<String>) {
diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs
index 88e49783..5a962d88 100644
--- a/src/recipe_resolver.rs
+++ b/src/recipe_resolver.rs
@@ -58,8 +58,9 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
     parameters: &[Parameter],
   ) -> CompileResult<'src> {
     let name = variable.lexeme();
-    let undefined =
-      !self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
+    let undefined = !self.assignments.contains_key(name)
+      && !parameters.iter().any(|p| p.name.lexeme() == name)
+      && !constants().contains_key(name);
 
     if undefined {
       return Err(variable.error(UndefinedVariable { variable: name }));
diff --git a/src/scope.rs b/src/scope.rs
index 18aa17c2..dd7888c1 100644
--- a/src/scope.rs
+++ b/src/scope.rs
@@ -14,11 +14,31 @@ impl<'src, 'run> Scope<'src, 'run> {
     }
   }
 
-  pub(crate) fn new() -> Self {
-    Self {
+  pub(crate) fn root() -> Self {
+    let mut root = Self {
       parent: None,
       bindings: Table::new(),
+    };
+
+    for (key, value) in constants() {
+      root.bind(
+        false,
+        Name {
+          token: Token {
+            column: 0,
+            kind: TokenKind::Identifier,
+            length: key.len(),
+            line: 0,
+            offset: 0,
+            path: Path::new("PRELUDE"),
+            src: key,
+          },
+        },
+        (*value).into(),
+      );
     }
+
+    root
   }
 
   pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, value: String) {


Test patch:
diff --git a/tests/constants.rs b/tests/constants.rs
new file mode 100644
index 00000000..59f8b9bd
--- /dev/null
+++ b/tests/constants.rs
@@ -0,0 +1,45 @@
+use super::*;
+
+#[test]
+fn constants_are_defined() {
+  assert_eval_eq("HEX", "0123456789abcdef");
+}
+
+#[test]
+fn constants_are_defined_in_recipe_bodies() {
+  Test::new()
+    .justfile(
+      "
+        @foo:
+          echo {{HEX}}
+      ",
+    )
+    .stdout("0123456789abcdef\n")
+    .run();
+}
+
+#[test]
+fn constants_are_defined_in_recipe_parameters() {
+  Test::new()
+    .justfile(
+      "
+        @foo hex=HEX:
+          echo {{hex}}
+      ",
+    )
+    .stdout("0123456789abcdef\n")
+    .run();
+}
+
+#[test]
+fn constants_can_be_redefined() {
+  Test::new()
+    .justfile(
+      "
+        HEX := 'foo'
+      ",
+    )
+    .args(["--evaluate", "HEX"])
+    .stdout("foo")
+    .run();
+}
diff --git a/tests/functions.rs b/tests/functions.rs
index a9fb98b6..95bddfd8 100644
--- a/tests/functions.rs
+++ b/tests/functions.rs
@@ -436,15 +436,6 @@ fn semver_matches() {
     .run();
 }
 
-fn assert_eval_eq(expression: &str, result: &str) {
-  Test::new()
-    .justfile(format!("x := {expression}"))
-    .args(["--evaluate", "x"])
-    .stdout(result)
-    .unindent_stdout(false)
-    .run();
-}
-
 #[test]
 fn trim_end_matches() {
   assert_eval_eq("trim_end_matches('foo', 'o')", "f");
diff --git a/tests/lib.rs b/tests/lib.rs
index 26c9d125..d282d2de 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -3,7 +3,7 @@ pub(crate) use {
     assert_stdout::assert_stdout,
     assert_success::assert_success,
     tempdir::tempdir,
-    test::{Output, Test},
+    test::{assert_eval_eq, Output, Test},
   },
   cradle::input::Input,
   executable_path::executable_path,
@@ -46,6 +46,7 @@ mod command;
 mod completions;
 mod conditional;
 mod confirm;
+mod constants;
 mod delimiters;
 mod directories;
 mod dotenv;
diff --git a/tests/test.rs b/tests/test.rs
index 65ee30ae..58830434 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -312,3 +312,12 @@ fn test_round_trip(tmpdir: &Path) {
 
   assert_eq!(reparsed, dumped, "reparse mismatch");
 }
+
+pub fn assert_eval_eq(expression: &str, result: &str) {
+  Test::new()
+    .justfile(format!("x := {expression}"))
+    .args(["--evaluate", "x"])
+    .stdout(result)
+    .unindent_stdout(false)
+    .run();
+}