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"
|
license = "CC0-1.0"
|
||||||
readme = "crates-io-readme.md"
|
readme = "crates-io-readme.md"
|
||||||
repository = "https://github.com/casey/just"
|
repository = "https://github.com/casey/just"
|
||||||
rust-version = "1.63"
|
rust-version = "1.70"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "crates/*"]
|
members = [".", "crates/*"]
|
||||||
|
20
README.md
20
README.md
@ -1541,6 +1541,26 @@ and are implemented with the
|
|||||||
- `executable_directory()` - The user-specific executable directory.
|
- `executable_directory()` - The user-specific executable directory.
|
||||||
- `home_directory()` - The user's home 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
|
### Recipe Attributes
|
||||||
|
|
||||||
Recipes may be annotated with attributes that change their behavior.
|
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::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
||||||
Expression::Variable { name } => {
|
Expression::Variable { name } => {
|
||||||
let variable = name.lexeme();
|
let variable = name.lexeme();
|
||||||
if self.evaluated.contains(variable) {
|
if self.evaluated.contains(variable) || constants().contains_key(variable) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else if self.stack.contains(&variable) {
|
} else if self.stack.contains(&variable) {
|
||||||
self.stack.push(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()
|
BTreeMap::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = Scope::new();
|
let root = Scope::root();
|
||||||
|
|
||||||
let scope = self.scope(config, &dotenv, search, overrides, &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:
|
// The width of the error site to highlight depends on the kind of error:
|
||||||
let length = match kind {
|
let length = match kind {
|
||||||
UnterminatedString | UnterminatedBacktick => {
|
UnterminatedString | UnterminatedBacktick => {
|
||||||
let kind = match StringKind::from_token_start(self.lexeme()) {
|
let Some(kind) = StringKind::from_token_start(self.lexeme()) else {
|
||||||
Some(kind) => kind,
|
return self.internal_error("Lexer::error: expected string or backtick token start");
|
||||||
None => {
|
|
||||||
return self.internal_error("Lexer::error: expected string or backtick token start")
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
kind.delimiter().len()
|
kind.delimiter().len()
|
||||||
}
|
}
|
||||||
@ -813,9 +810,7 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Cooked string: "[^"]*" # also processes escape sequences
|
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||||
/// Raw string: '[^']*'
|
/// Raw string: '[^']*'
|
||||||
fn lex_string(&mut self) -> CompileResult<'src> {
|
fn lex_string(&mut self) -> CompileResult<'src> {
|
||||||
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
|
let Some(kind) = StringKind::from_token_start(self.rest()) else {
|
||||||
kind
|
|
||||||
} else {
|
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
return Err(self.internal_error("Lexer::lex_string: invalid string start"));
|
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,
|
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||||
condition::Condition, conditional_operator::ConditionalOperator, config::Config,
|
condition::Condition, conditional_operator::ConditionalOperator, config::Config,
|
||||||
config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
|
config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter,
|
||||||
dump_format::DumpFormat, enclosure::Enclosure, error::Error, evaluator::Evaluator,
|
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error,
|
||||||
expression::Expression, fragment::Fragment, function::Function,
|
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
|
||||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
||||||
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
||||||
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
|
load_dotenv::load_dotenv, loader::Loader, name::Name, namepath::Namepath, ordinal::Ordinal,
|
||||||
@ -53,7 +53,7 @@ pub(crate) use {
|
|||||||
process::{self, Command, ExitStatus, Stdio},
|
process::{self, Command, ExitStatus, Stdio},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
str::{self, Chars},
|
str::{self, Chars},
|
||||||
sync::{Mutex, MutexGuard},
|
sync::{Mutex, MutexGuard, OnceLock},
|
||||||
vec,
|
vec,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -128,6 +128,7 @@ mod condition;
|
|||||||
mod conditional_operator;
|
mod conditional_operator;
|
||||||
mod config;
|
mod config;
|
||||||
mod config_error;
|
mod config_error;
|
||||||
|
mod constants;
|
||||||
mod count;
|
mod count;
|
||||||
mod delimiter;
|
mod delimiter;
|
||||||
mod dependency;
|
mod dependency;
|
||||||
|
@ -8,8 +8,7 @@ impl<'src> Ran<'src> {
|
|||||||
self
|
self
|
||||||
.0
|
.0
|
||||||
.get(recipe)
|
.get(recipe)
|
||||||
.map(|ran| ran.contains(arguments))
|
.is_some_and(|ran| ran.contains(arguments))
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn ran(&mut self, recipe: &Namepath<'src>, arguments: Vec<String>) {
|
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],
|
parameters: &[Parameter],
|
||||||
) -> CompileResult<'src> {
|
) -> CompileResult<'src> {
|
||||||
let name = variable.lexeme();
|
let name = variable.lexeme();
|
||||||
let undefined =
|
let undefined = !self.assignments.contains_key(name)
|
||||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
&& !parameters.iter().any(|p| p.name.lexeme() == name)
|
||||||
|
&& !constants().contains_key(name);
|
||||||
|
|
||||||
if undefined {
|
if undefined {
|
||||||
return Err(variable.error(UndefinedVariable { variable: name }));
|
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 {
|
pub(crate) fn root() -> Self {
|
||||||
Self {
|
let mut root = Self {
|
||||||
parent: None,
|
parent: None,
|
||||||
bindings: Table::new(),
|
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) {
|
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();
|
.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]
|
#[test]
|
||||||
fn trim_end_matches() {
|
fn trim_end_matches() {
|
||||||
assert_eval_eq("trim_end_matches('foo', 'o')", "f");
|
assert_eval_eq("trim_end_matches('foo', 'o')", "f");
|
||||||
|
@ -3,7 +3,7 @@ pub(crate) use {
|
|||||||
assert_stdout::assert_stdout,
|
assert_stdout::assert_stdout,
|
||||||
assert_success::assert_success,
|
assert_success::assert_success,
|
||||||
tempdir::tempdir,
|
tempdir::tempdir,
|
||||||
test::{Output, Test},
|
test::{assert_eval_eq, Output, Test},
|
||||||
},
|
},
|
||||||
cradle::input::Input,
|
cradle::input::Input,
|
||||||
executable_path::executable_path,
|
executable_path::executable_path,
|
||||||
@ -46,6 +46,7 @@ mod command;
|
|||||||
mod completions;
|
mod completions;
|
||||||
mod conditional;
|
mod conditional;
|
||||||
mod confirm;
|
mod confirm;
|
||||||
|
mod constants;
|
||||||
mod delimiters;
|
mod delimiters;
|
||||||
mod directories;
|
mod directories;
|
||||||
mod dotenv;
|
mod dotenv;
|
||||||
|
@ -312,3 +312,12 @@ fn test_round_trip(tmpdir: &Path) {
|
|||||||
|
|
||||||
assert_eq!(reparsed, dumped, "reparse mismatch");
|
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