Add predefined constants (#2054)
This commit is contained in:
parent
f3eebb74d8
commit
6907847a77
@ -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/*"]
|
||||
|
20
README.md
20
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.
|
||||
|
@ -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);
|
||||
|
15
src/constants.rs
Normal file
15
src/constants.rs
Normal file
@ -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()
|
||||
})
|
||||
}
|
@ -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)?;
|
||||
|
||||
|
11
src/lexer.rs
11
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"));
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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>) {
|
||||
|
@ -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 }));
|
||||
|
24
src/scope.rs
24
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) {
|
||||
|
45
tests/constants.rs
Normal file
45
tests/constants.rs
Normal file
@ -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();
|
||||
}
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user