Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ alias : 'alias' NAME ':=' target eol

target : NAME ('::' NAME)*

assignment : NAME ':=' expression eol
assignment : 'lazy'? NAME ':=' expression eol

export : 'export' assignment

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,35 @@ braces:
echo 'I {{ "{{" }}LOVE}} curly braces!'
```

#### Lazy Evaluation

By default, variables are evaluated when they are defined. If you would like a
variable to only be evaluated when it is used for the first time, you can use the
`lazy` keyword:

```just
lazy aws_account_id := `aws sts get-caller-identity --query Account --output text`
```

Once a lazy variable has been evaluated, its value is the same for the rest of
the invocation of `just`, even if it is used multiple times:

```just
lazy timestamp := `date +%s`

foo:
# The value is computed here
echo The time is {{timestamp}}
sleep 1
# The same value is used here
echo The time is still {{timestamp}}
```

This is useful for values that are expensive to compute, or that may not be
needed in every invocation of `just`. It also saves you from having expensive
values being recomputed even for simple invocations of `just` that don't
actually use them, like `just --list`.

### Strings

`'single'`, `"double"`, and `'''triple'''` quoted string literals are
Expand Down
4 changes: 4 additions & 0 deletions src/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ impl Display for Assignment<'_> {
writeln!(f, "[private]")?;
}

if self.lazy {
write!(f, "lazy ")?;
}

if self.export {
write!(f, "export ")?;
}
Expand Down
1 change: 1 addition & 0 deletions src/binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) struct Binding<'src, V = String> {
pub(crate) export: bool,
#[serde(skip)]
pub(crate) file_depth: u32,
pub(crate) lazy: bool,
pub(crate) name: Name<'src>,
pub(crate) private: bool,
pub(crate) value: V,
Expand Down
9 changes: 7 additions & 2 deletions src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
file_depth: 0,
name: assignment.name,
private: assignment.private,
lazy: false,
value: value.clone(),
});
} else {
Expand All @@ -58,7 +59,9 @@ impl<'src, 'run> Evaluator<'src, 'run> {
};

for assignment in module.assignments.values() {
evaluator.evaluate_assignment(assignment)?;
if !assignment.lazy {
evaluator.evaluate_assignment(assignment)?;
}
}

Ok(evaluator.scope)
Expand All @@ -75,6 +78,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
file_depth: 0,
name: assignment.name,
private: assignment.private,
lazy: false,
value,
});
}
Expand Down Expand Up @@ -363,6 +367,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
file_depth: 0,
name: parameter.name,
private: false,
lazy: false,
value,
});
}
Expand All @@ -376,7 +381,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
scope: &'run Scope<'src, 'run>,
) -> Self {
Self {
assignments: None,
assignments: Some(&context.module.assignments),
context: *context,
is_dependency,
scope: scope.child(),
Expand Down
1 change: 1 addition & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub(crate) enum Keyword {
If,
IgnoreComments,
Import,
Lazy,
Mod,
NoExitMessage,
PositionalArguments,
Expand Down
31 changes: 31 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ impl<'run, 'src> Parser<'run, 'src> {
self.parse_assignment(true, take_attributes())?,
));
}
Some(Keyword::Lazy) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
self.presume_keyword(Keyword::Lazy)?;
items.push(Item::Assignment(
self.parse_assignment_lazy(take_attributes())?,
));
}
Some(Keyword::Unexport)
if self.next_are(&[Identifier, Identifier, Eof])
|| self.next_are(&[Identifier, Identifier, Eol]) =>
Expand Down Expand Up @@ -526,6 +532,31 @@ impl<'run, 'src> Parser<'run, 'src> {
file_depth: self.file_depth,
name,
private: private || name.lexeme().starts_with('_'),
lazy: false,
value,
})
}

fn parse_assignment_lazy(
&mut self,
attributes: AttributeSet<'src>,
) -> CompileResult<'src, Assignment<'src>> {
let name = self.parse_name()?;
self.presume(ColonEquals)?;
let value = self.parse_expression()?;
self.expect_eol()?;

let private = attributes.contains(AttributeDiscriminant::Private);

attributes.ensure_valid_attributes("Assignment", *name, &[AttributeDiscriminant::Private])?;

Ok(Assignment {
constant: false,
export: false,
file_depth: self.file_depth,
name,
private: private || name.lexeme().starts_with('_'),
lazy: true,
value,
})
}
Expand Down
1 change: 1 addition & 0 deletions src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ impl<'src, 'run> Scope<'src, 'run> {
},
},
private: false,
lazy: false,
value: (*value).into(),
});
}
Expand Down
2 changes: 2 additions & 0 deletions tests/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ struct Alias<'a> {
#[serde(deny_unknown_fields)]
struct Assignment<'a> {
export: bool,
#[serde(default)]
lazy: bool,
name: &'a str,
private: bool,
value: &'a str,
Expand Down
179 changes: 179 additions & 0 deletions tests/lazy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use super::*;

#[test]
fn lazy_variable_not_evaluated_if_unused() {
Test::new()
.justfile(
"
lazy expensive := `exit 1`

works:
@echo 'Success'
",
)
.stdout("Success\n")
.run();
}

#[test]
fn lazy_variable_evaluated_when_used() {
Test::new()
.justfile(
"
lazy greeting := `echo 'Hello'`

test:
@echo {{greeting}}
",
)
.stdout("Hello\n")
.run();
}

#[test]
fn lazy_variable_with_backtick_error() {
Test::new()
.justfile(
"
lazy bad := `exit 1`

test:
@echo {{bad}}
",
)
.stderr(
"
error: Backtick failed with exit code 1
——▶ justfile:1:13
1 │ lazy bad := `exit 1`
│ ^^^^^^^^
",
)
.status(EXIT_FAILURE)
.run();
}

#[test]
fn lazy_variable_used_multiple_times() {
Test::new()
.justfile(
"
lazy value := `echo 'test'`

test:
@echo {{value}}
@echo {{value}}
",
)
.stdout("test\ntest\n")
.run();
}

#[test]
fn lazy_and_export_are_separate() {
Test::new()
.justfile(
"
lazy foo := `echo 'lazy'`
export bar := 'exported'

test:
@echo {{foo}} $bar
",
)
.stdout("lazy exported\n")
.run();
}

#[test]
fn lazy_variable_dump() {
Test::new()
.justfile(
"
lazy greeting := `echo 'Hello'`
normal := 'value'
",
)
.args(["--dump"])
.stdout(
"
lazy greeting := `echo 'Hello'`
normal := 'value'
",
)
.run();
}

#[test]
fn lazy_keyword_lexeme() {
Test::new()
.justfile(
"
lazy := 'not a keyword here'

test:
@echo {{lazy}}
",
)
.stdout("not a keyword here\n")
.run();
}

#[test]
fn lazy_variable_in_dependency() {
Test::new()
.justfile(
"
lazy value := `echo 'computed'`

dep:
@echo {{value}}

main: dep
@echo 'done'
",
)
.args(["main"])
.stdout("computed\ndone\n")
.run();
}

#[test]
fn lazy_with_private() {
Test::new()
.justfile(
"
[private]
lazy _secret := `echo 'hidden'`

test:
@echo {{_secret}}
",
)
.stdout("hidden\n")
.run();
}

#[test]
fn lazy_variable_evaluated_once() {
Test::new()
.justfile(
"
lazy value := `date +%s%N`

test:
#!/usr/bin/env bash
first={{value}}
second={{value}}
if [ \"$first\" = \"$second\" ]; then
echo \"PASS: $first\"
else
echo \"FAIL: first=$first second=$second\"
exit 1
fi
",
)
.stdout_regex("^PASS: \\d+\\n$")
.run();
}
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ mod imports;
mod init;
mod invocation_directory;
mod json;
mod lazy;
mod line_prefixes;
mod list;
mod logical_operators;
Expand Down