mirror of
https://github.com/casey/just.git
synced 2024-11-26 00:24:22 +03:00
Compare commits
3 Commits
528c9f0e3c
...
8cdff483bf
Author | SHA1 | Date | |
---|---|---|---|
|
8cdff483bf | ||
|
7030e9cac6 | ||
|
4c6368ecfc |
@ -90,7 +90,13 @@ import : 'import' '?'? string? eol
|
|||||||
|
|
||||||
module : 'mod' '?'? NAME string? eol
|
module : 'mod' '?'? NAME string? eol
|
||||||
|
|
||||||
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
expression : disjunct || expression
|
||||||
|
| disjunct
|
||||||
|
|
||||||
|
disjunct : conjunct && disjunct
|
||||||
|
| conjunct
|
||||||
|
|
||||||
|
conjunct : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
||||||
| 'assert' '(' condition ',' expression ')'
|
| 'assert' '(' condition ',' expression ')'
|
||||||
| '/' expression
|
| '/' expression
|
||||||
| value '/' expression
|
| value '/' expression
|
||||||
|
105
README.md
105
README.md
@ -1290,9 +1290,11 @@ Available recipes:
|
|||||||
test
|
test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Variables and Substitution
|
### Expressions and Substitutions
|
||||||
|
|
||||||
Variables, strings, concatenation, path joining, substitution using `{{…}}`, and function calls are supported:
|
Various operators and function calls are supported in expressions, which may be
|
||||||
|
used in assignments, default recipe arguments, and inside recipe body `{{…}}`
|
||||||
|
substitutions.
|
||||||
|
|
||||||
```just
|
```just
|
||||||
tmpdir := `mktemp -d`
|
tmpdir := `mktemp -d`
|
||||||
@ -1310,6 +1312,39 @@ publish:
|
|||||||
rm -rf {{tarball}} {{tardir}}
|
rm -rf {{tarball}} {{tardir}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Concatenation
|
||||||
|
|
||||||
|
The `+` operator returns the left-hand argument concatenated with the
|
||||||
|
right-hand argument:
|
||||||
|
|
||||||
|
```just
|
||||||
|
foobar := 'foo' + 'bar'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Logical Operators
|
||||||
|
|
||||||
|
The logical operators `&&` and `||` can be used to coalesce string
|
||||||
|
values<sup>master</sup>, similar to Python's `and` and `or`. These operators
|
||||||
|
consider the empty string `''` to be false, and all other strings to be true.
|
||||||
|
|
||||||
|
These operators are currently unstable.
|
||||||
|
|
||||||
|
The `&&` operator returns the empty string if the left-hand argument is the
|
||||||
|
empty string, otherwise it returns the right-hand argument:
|
||||||
|
|
||||||
|
```justfile
|
||||||
|
foo := '' && 'goodbye' # ''
|
||||||
|
bar := 'hello' && 'goodbye' # 'goodbye'
|
||||||
|
```
|
||||||
|
|
||||||
|
The `||` operator returns the left-hand argument if it is non-empty, otherwise
|
||||||
|
it returns the right-hand argument:
|
||||||
|
|
||||||
|
```justfile
|
||||||
|
foo := '' || 'goodbye' # 'goodbye'
|
||||||
|
bar := 'hello' || 'goodbye' # 'hello'
|
||||||
|
```
|
||||||
|
|
||||||
#### Joining Paths
|
#### Joining Paths
|
||||||
|
|
||||||
The `/` operator can be used to join two strings with a slash:
|
The `/` operator can be used to join two strings with a slash:
|
||||||
@ -2367,8 +2402,8 @@ Testing server:unit…
|
|||||||
./test --tests unit server
|
./test --tests unit server
|
||||||
```
|
```
|
||||||
|
|
||||||
Default values may be arbitrary expressions, but concatenations or path joins
|
Default values may be arbitrary expressions, but expressions containing the
|
||||||
must be parenthesized:
|
`+`, `&&`, `||`, or `/` operators must be parenthesized:
|
||||||
|
|
||||||
```just
|
```just
|
||||||
arch := "wasm"
|
arch := "wasm"
|
||||||
@ -2740,7 +2775,7 @@ pass a Windows-style path to the interpreter.
|
|||||||
Recipe lines are interpreted by the shell, not `just`, so it's not possible to
|
Recipe lines are interpreted by the shell, not `just`, so it's not possible to
|
||||||
set `just` variables in the middle of a recipe:
|
set `just` variables in the middle of a recipe:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
foo:
|
foo:
|
||||||
x := "hello" # This doesn't work!
|
x := "hello" # This doesn't work!
|
||||||
echo {{x}}
|
echo {{x}}
|
||||||
@ -2872,7 +2907,7 @@ means that multi-line constructs probably won't do what you want.
|
|||||||
|
|
||||||
For example, with the following `justfile`:
|
For example, with the following `justfile`:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
conditional:
|
conditional:
|
||||||
if true; then
|
if true; then
|
||||||
echo 'True!'
|
echo 'True!'
|
||||||
@ -3279,7 +3314,7 @@ One `justfile` can include the contents of another using `import` statements.
|
|||||||
|
|
||||||
If you have the following `justfile`:
|
If you have the following `justfile`:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
import 'foo/bar.just'
|
import 'foo/bar.just'
|
||||||
|
|
||||||
a: b
|
a: b
|
||||||
@ -3319,7 +3354,7 @@ set, variables in parent modules override variables in imports.
|
|||||||
|
|
||||||
Imports may be made optional by putting a `?` after the `import` keyword:
|
Imports may be made optional by putting a `?` after the `import` keyword:
|
||||||
|
|
||||||
```mf
|
```just
|
||||||
import? 'foo/bar.just'
|
import? 'foo/bar.just'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -3328,19 +3363,19 @@ This allows importing multiple justfiles, for example `foo.just` and
|
|||||||
`bar.just`, which both import a third justfile containing shared recipes, for
|
`bar.just`, which both import a third justfile containing shared recipes, for
|
||||||
example `baz.just`, without the duplicate import of `baz.just` being an error:
|
example `baz.just`, without the duplicate import of `baz.just` being an error:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
# justfile
|
# justfile
|
||||||
import 'foo.just'
|
import 'foo.just'
|
||||||
import 'bar.just'
|
import 'bar.just'
|
||||||
```
|
```
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
# foo.just
|
# foo.just
|
||||||
import 'baz.just'
|
import 'baz.just'
|
||||||
foo: baz
|
foo: baz
|
||||||
```
|
```
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
# bar.just
|
# bar.just
|
||||||
import 'baz.just'
|
import 'baz.just'
|
||||||
bar: baz
|
bar: baz
|
||||||
@ -3361,7 +3396,7 @@ versions, you'll need to use the `--unstable` flag, `set unstable`, or set the
|
|||||||
|
|
||||||
If you have the following `justfile`:
|
If you have the following `justfile`:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
mod bar
|
mod bar
|
||||||
|
|
||||||
a:
|
a:
|
||||||
@ -3399,7 +3434,7 @@ the module file may have any capitalization.
|
|||||||
|
|
||||||
Module statements may be of the form:
|
Module statements may be of the form:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
mod foo 'PATH'
|
mod foo 'PATH'
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -3423,7 +3458,7 @@ recipes.
|
|||||||
|
|
||||||
Modules may be made optional by putting a `?` after the `mod` keyword:
|
Modules may be made optional by putting a `?` after the `mod` keyword:
|
||||||
|
|
||||||
```mf
|
```just
|
||||||
mod? foo
|
mod? foo
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -3433,7 +3468,7 @@ Optional modules with no source file do not conflict, so you can have multiple
|
|||||||
mod statements with the same name, but with different source file paths, as
|
mod statements with the same name, but with different source file paths, as
|
||||||
long as at most one source file exists:
|
long as at most one source file exists:
|
||||||
|
|
||||||
```mf
|
```just
|
||||||
mod? foo 'bar.just'
|
mod? foo 'bar.just'
|
||||||
mod? foo 'baz.just'
|
mod? foo 'baz.just'
|
||||||
```
|
```
|
||||||
@ -3441,7 +3476,7 @@ mod? foo 'baz.just'
|
|||||||
Modules may be given doc comments which appear in `--list`
|
Modules may be given doc comments which appear in `--list`
|
||||||
output<sup>1.30.0</sup>:
|
output<sup>1.30.0</sup>:
|
||||||
|
|
||||||
```mf
|
```justfile
|
||||||
# foo is a great module!
|
# foo is a great module!
|
||||||
mod foo
|
mod foo
|
||||||
```
|
```
|
||||||
@ -3583,9 +3618,9 @@ The following command will create two files, `some` and `argument.txt`:
|
|||||||
$ just foo "some argument.txt"
|
$ just foo "some argument.txt"
|
||||||
```
|
```
|
||||||
|
|
||||||
The users shell will parse `"some argument.txt"` as a single argument, but when
|
The user's shell will parse `"some argument.txt"` as a single argument, but
|
||||||
`just` replaces `touch {{argument}}` with `touch some argument.txt`, the quotes
|
when `just` replaces `touch {{argument}}` with `touch some argument.txt`, the
|
||||||
are not preserved, and `touch` will receive two arguments.
|
quotes are not preserved, and `touch` will receive two arguments.
|
||||||
|
|
||||||
There are a few ways to avoid this: quoting, positional arguments, and exported
|
There are a few ways to avoid this: quoting, positional arguments, and exported
|
||||||
arguments.
|
arguments.
|
||||||
@ -3910,6 +3945,38 @@ fetch:
|
|||||||
Given the above `justfile`, after running `just fetch`, the recipes in
|
Given the above `justfile`, after running `just fetch`, the recipes in
|
||||||
`foo.just` will be available.
|
`foo.just` will be available.
|
||||||
|
|
||||||
|
### Printing Complex Strings
|
||||||
|
|
||||||
|
`echo` can be used to print strings, but because it processes escape sequences,
|
||||||
|
like `\n`, and different implementations of `echo` recognize different escape
|
||||||
|
sequences, using `printf` is often a better choice.
|
||||||
|
|
||||||
|
`printf` takes a C-style format string and any number of arguments, which are
|
||||||
|
interpolated into the format string.
|
||||||
|
|
||||||
|
This can be combined with indented, triple quoted strings to emulate shell
|
||||||
|
heredocs.
|
||||||
|
|
||||||
|
Substitution complex strings into recipe bodies with `{…}` can also lead to
|
||||||
|
trouble as it may be split by the shell into multiple arguments depending on
|
||||||
|
the presence of whitespace and quotes. Exporting complex strings as environment
|
||||||
|
variables and referring to them with `"$NAME"`, note the double quotes, can
|
||||||
|
also help.
|
||||||
|
|
||||||
|
Putting all this together, to print a string verbatim to standard output, with
|
||||||
|
all its various escape sequences and quotes undisturbed:
|
||||||
|
|
||||||
|
```just
|
||||||
|
export FOO := '''
|
||||||
|
a complicated string with
|
||||||
|
some dis\tur\bi\ng escape sequences
|
||||||
|
and "quotes" of 'different' kinds
|
||||||
|
'''
|
||||||
|
|
||||||
|
bar:
|
||||||
|
printf %s "$FOO"
|
||||||
|
```
|
||||||
|
|
||||||
### Alternatives and Prior Art
|
### Alternatives and Prior Art
|
||||||
|
|
||||||
There is no shortage of command runners! Some more or less similar alternatives
|
There is no shortage of command runners! Some more or less similar alternatives
|
||||||
|
@ -36,12 +36,15 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
|||||||
) -> CompileResult<'src, Justfile<'src>> {
|
) -> CompileResult<'src, Justfile<'src>> {
|
||||||
let mut definitions = HashMap::new();
|
let mut definitions = HashMap::new();
|
||||||
let mut imports = HashSet::new();
|
let mut imports = HashSet::new();
|
||||||
|
let mut unstable_features = BTreeSet::new();
|
||||||
|
|
||||||
let mut stack = Vec::new();
|
let mut stack = Vec::new();
|
||||||
let ast = asts.get(root).unwrap();
|
let ast = asts.get(root).unwrap();
|
||||||
stack.push(ast);
|
stack.push(ast);
|
||||||
|
|
||||||
while let Some(ast) = stack.pop() {
|
while let Some(ast) = stack.pop() {
|
||||||
|
unstable_features.extend(&ast.unstable_features);
|
||||||
|
|
||||||
for item in &ast.items {
|
for item in &ast.items {
|
||||||
match item {
|
match item {
|
||||||
Item::Alias(alias) => {
|
Item::Alias(alias) => {
|
||||||
@ -166,8 +169,6 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
|||||||
aliases.insert(Self::resolve_alias(&recipes, alias)?);
|
aliases.insert(Self::resolve_alias(&recipes, alias)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut unstable_features = BTreeSet::new();
|
|
||||||
|
|
||||||
for recipe in recipes.values() {
|
for recipe in recipes.values() {
|
||||||
for attribute in &recipe.attributes {
|
for attribute in &recipe.attributes {
|
||||||
if let Attribute::Script(_) = attribute {
|
if let Attribute::Script(_) = attribute {
|
||||||
|
@ -31,7 +31,29 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
self.stack.push(name);
|
self.stack.push(name);
|
||||||
|
|
||||||
if let Some(assignment) = self.assignments.get(name) {
|
if let Some(assignment) = self.assignments.get(name) {
|
||||||
self.resolve_expression(&assignment.value)?;
|
for variable in assignment.value.variables() {
|
||||||
|
let name = variable.lexeme();
|
||||||
|
|
||||||
|
if self.evaluated.contains(name) || constants().contains_key(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.stack.contains(&name) {
|
||||||
|
self.stack.push(name);
|
||||||
|
return Err(
|
||||||
|
self.assignments[name]
|
||||||
|
.name
|
||||||
|
.error(CircularVariableDependency {
|
||||||
|
variable: name,
|
||||||
|
circle: self.stack.clone(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if self.assignments.contains_key(name) {
|
||||||
|
self.resolve_assignment(name)?;
|
||||||
|
} else {
|
||||||
|
return Err(variable.error(UndefinedVariable { variable: name }));
|
||||||
|
}
|
||||||
|
}
|
||||||
self.evaluated.insert(name);
|
self.evaluated.insert(name);
|
||||||
} else {
|
} else {
|
||||||
let message = format!("attempted to resolve unknown assignment `{name}`");
|
let message = format!("attempted to resolve unknown assignment `{name}`");
|
||||||
@ -51,112 +73,6 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src> {
|
|
||||||
match expression {
|
|
||||||
Expression::Assert {
|
|
||||||
condition: Condition {
|
|
||||||
lhs,
|
|
||||||
rhs,
|
|
||||||
operator: _,
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
} => {
|
|
||||||
self.resolve_expression(lhs)?;
|
|
||||||
self.resolve_expression(rhs)?;
|
|
||||||
self.resolve_expression(error)
|
|
||||||
}
|
|
||||||
Expression::Call { thunk } => match thunk {
|
|
||||||
Thunk::Nullary { .. } => Ok(()),
|
|
||||||
Thunk::Unary { arg, .. } => self.resolve_expression(arg),
|
|
||||||
Thunk::UnaryOpt { args: (a, b), .. } => {
|
|
||||||
self.resolve_expression(a)?;
|
|
||||||
if let Some(b) = b.as_ref() {
|
|
||||||
self.resolve_expression(b)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Thunk::UnaryPlus {
|
|
||||||
args: (a, rest), ..
|
|
||||||
} => {
|
|
||||||
self.resolve_expression(a)?;
|
|
||||||
for arg in rest {
|
|
||||||
self.resolve_expression(arg)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Thunk::Binary { args: [a, b], .. } => {
|
|
||||||
self.resolve_expression(a)?;
|
|
||||||
self.resolve_expression(b)
|
|
||||||
}
|
|
||||||
Thunk::BinaryPlus {
|
|
||||||
args: ([a, b], rest),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.resolve_expression(a)?;
|
|
||||||
self.resolve_expression(b)?;
|
|
||||||
for arg in rest {
|
|
||||||
self.resolve_expression(arg)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Thunk::Ternary {
|
|
||||||
args: [a, b, c], ..
|
|
||||||
} => {
|
|
||||||
self.resolve_expression(a)?;
|
|
||||||
self.resolve_expression(b)?;
|
|
||||||
self.resolve_expression(c)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Expression::Concatenation { lhs, rhs } => {
|
|
||||||
self.resolve_expression(lhs)?;
|
|
||||||
self.resolve_expression(rhs)
|
|
||||||
}
|
|
||||||
Expression::Conditional {
|
|
||||||
condition: Condition {
|
|
||||||
lhs,
|
|
||||||
rhs,
|
|
||||||
operator: _,
|
|
||||||
},
|
|
||||||
then,
|
|
||||||
otherwise,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.resolve_expression(lhs)?;
|
|
||||||
self.resolve_expression(rhs)?;
|
|
||||||
self.resolve_expression(then)?;
|
|
||||||
self.resolve_expression(otherwise)
|
|
||||||
}
|
|
||||||
Expression::Group { contents } => self.resolve_expression(contents),
|
|
||||||
Expression::Join { lhs, rhs } => {
|
|
||||||
if let Some(lhs) = lhs {
|
|
||||||
self.resolve_expression(lhs)?;
|
|
||||||
}
|
|
||||||
self.resolve_expression(rhs)
|
|
||||||
}
|
|
||||||
Expression::StringLiteral { .. } | Expression::Backtick { .. } => Ok(()),
|
|
||||||
Expression::Variable { name } => {
|
|
||||||
let variable = name.lexeme();
|
|
||||||
if self.evaluated.contains(variable) || constants().contains_key(variable) {
|
|
||||||
Ok(())
|
|
||||||
} else if self.stack.contains(&variable) {
|
|
||||||
self.stack.push(variable);
|
|
||||||
Err(
|
|
||||||
self.assignments[variable]
|
|
||||||
.name
|
|
||||||
.error(CircularVariableDependency {
|
|
||||||
variable,
|
|
||||||
circle: self.stack.clone(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else if self.assignments.contains_key(variable) {
|
|
||||||
self.resolve_assignment(variable)
|
|
||||||
} else {
|
|
||||||
Err(name.token.error(UndefinedVariable { variable }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -6,6 +6,7 @@ use super::*;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Ast<'src> {
|
pub(crate) struct Ast<'src> {
|
||||||
pub(crate) items: Vec<Item<'src>>,
|
pub(crate) items: Vec<Item<'src>>,
|
||||||
|
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
|
||||||
pub(crate) warnings: Vec<Warning>,
|
pub(crate) warnings: Vec<Warning>,
|
||||||
pub(crate) working_directory: PathBuf,
|
pub(crate) working_directory: PathBuf,
|
||||||
}
|
}
|
||||||
|
@ -84,24 +84,31 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
expression: &Expression<'src>,
|
expression: &Expression<'src>,
|
||||||
) -> RunResult<'src, String> {
|
) -> RunResult<'src, String> {
|
||||||
match expression {
|
match expression {
|
||||||
Expression::Variable { name, .. } => {
|
Expression::And { lhs, rhs } => {
|
||||||
let variable = name.lexeme();
|
let lhs = self.evaluate_expression(lhs)?;
|
||||||
if let Some(value) = self.scope.value(variable) {
|
if lhs.is_empty() {
|
||||||
Ok(value.to_owned())
|
return Ok(String::new());
|
||||||
} else if let Some(assignment) = self
|
}
|
||||||
.assignments
|
self.evaluate_expression(rhs)
|
||||||
.and_then(|assignments| assignments.get(variable))
|
}
|
||||||
{
|
Expression::Assert { condition, error } => {
|
||||||
Ok(self.evaluate_assignment(assignment)?.to_owned())
|
if self.evaluate_condition(condition)? {
|
||||||
|
Ok(String::new())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Internal {
|
Err(Error::Assert {
|
||||||
message: format!("attempted to evaluate undefined variable `{variable}`"),
|
message: self.evaluate_expression(error)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Expression::Backtick { contents, token } => {
|
||||||
|
if self.context.config.dry_run {
|
||||||
|
Ok(format!("`{contents}`"))
|
||||||
|
} else {
|
||||||
|
Ok(self.run_backtick(contents, token)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
Expression::Call { thunk } => {
|
Expression::Call { thunk } => {
|
||||||
use Thunk::*;
|
use Thunk::*;
|
||||||
|
|
||||||
let result = match thunk {
|
let result = match thunk {
|
||||||
Nullary { function, .. } => function(function::Context::new(self, thunk.name())),
|
Nullary { function, .. } => function(function::Context::new(self, thunk.name())),
|
||||||
Unary { function, arg, .. } => {
|
Unary { function, arg, .. } => {
|
||||||
@ -118,7 +125,6 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
Some(b) => Some(self.evaluate_expression(b)?),
|
Some(b) => Some(self.evaluate_expression(b)?),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
function(function::Context::new(self, thunk.name()), &a, b.as_deref())
|
function(function::Context::new(self, thunk.name()), &a, b.as_deref())
|
||||||
}
|
}
|
||||||
UnaryPlus {
|
UnaryPlus {
|
||||||
@ -175,20 +181,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
function(function::Context::new(self, thunk.name()), &a, &b, &c)
|
function(function::Context::new(self, thunk.name()), &a, &b, &c)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
result.map_err(|message| Error::FunctionCall {
|
result.map_err(|message| Error::FunctionCall {
|
||||||
function: thunk.name(),
|
function: thunk.name(),
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
|
|
||||||
Expression::Backtick { contents, token } => {
|
|
||||||
if self.context.config.dry_run {
|
|
||||||
Ok(format!("`{contents}`"))
|
|
||||||
} else {
|
|
||||||
Ok(self.run_backtick(contents, token)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Expression::Concatenation { lhs, rhs } => {
|
Expression::Concatenation { lhs, rhs } => {
|
||||||
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
|
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
|
||||||
}
|
}
|
||||||
@ -209,12 +206,26 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
lhs: Some(lhs),
|
lhs: Some(lhs),
|
||||||
rhs,
|
rhs,
|
||||||
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
|
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
|
||||||
Expression::Assert { condition, error } => {
|
Expression::Or { lhs, rhs } => {
|
||||||
if self.evaluate_condition(condition)? {
|
let lhs = self.evaluate_expression(lhs)?;
|
||||||
Ok(String::new())
|
if !lhs.is_empty() {
|
||||||
|
return Ok(lhs);
|
||||||
|
}
|
||||||
|
self.evaluate_expression(rhs)
|
||||||
|
}
|
||||||
|
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
|
||||||
|
Expression::Variable { name, .. } => {
|
||||||
|
let variable = name.lexeme();
|
||||||
|
if let Some(value) = self.scope.value(variable) {
|
||||||
|
Ok(value.to_owned())
|
||||||
|
} else if let Some(assignment) = self
|
||||||
|
.assignments
|
||||||
|
.and_then(|assignments| assignments.get(variable))
|
||||||
|
{
|
||||||
|
Ok(self.evaluate_assignment(assignment)?.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::Assert {
|
Err(Error::Internal {
|
||||||
message: self.evaluate_expression(error)?,
|
message: format!("attempted to evaluate undefined variable `{variable}`"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,11 @@ use super::*;
|
|||||||
/// The parser parses both values and expressions into `Expression`s.
|
/// The parser parses both values and expressions into `Expression`s.
|
||||||
#[derive(PartialEq, Debug, Clone)]
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub(crate) enum Expression<'src> {
|
pub(crate) enum Expression<'src> {
|
||||||
|
/// `lhs && rhs`
|
||||||
|
And {
|
||||||
|
lhs: Box<Expression<'src>>,
|
||||||
|
rhs: Box<Expression<'src>>,
|
||||||
|
},
|
||||||
/// `assert(condition, error)`
|
/// `assert(condition, error)`
|
||||||
Assert {
|
Assert {
|
||||||
condition: Condition<'src>,
|
condition: Condition<'src>,
|
||||||
@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> {
|
|||||||
lhs: Option<Box<Expression<'src>>>,
|
lhs: Option<Box<Expression<'src>>>,
|
||||||
rhs: Box<Expression<'src>>,
|
rhs: Box<Expression<'src>>,
|
||||||
},
|
},
|
||||||
|
/// `lhs || rhs`
|
||||||
|
Or {
|
||||||
|
lhs: Box<Expression<'src>>,
|
||||||
|
rhs: Box<Expression<'src>>,
|
||||||
|
},
|
||||||
/// `"string_literal"` or `'string_literal'`
|
/// `"string_literal"` or `'string_literal'`
|
||||||
StringLiteral { string_literal: StringLiteral<'src> },
|
StringLiteral { string_literal: StringLiteral<'src> },
|
||||||
/// `variable`
|
/// `variable`
|
||||||
@ -53,23 +63,25 @@ impl<'src> Expression<'src> {
|
|||||||
impl<'src> Display for Expression<'src> {
|
impl<'src> Display for Expression<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
Self::And { lhs, rhs } => write!(f, "{lhs} && {rhs}"),
|
||||||
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
||||||
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
||||||
Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
|
Self::Call { thunk } => write!(f, "{thunk}"),
|
||||||
Self::Join {
|
|
||||||
lhs: Some(lhs),
|
|
||||||
rhs,
|
|
||||||
} => write!(f, "{lhs} / {rhs}"),
|
|
||||||
Self::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
|
Self::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
|
||||||
Self::Conditional {
|
Self::Conditional {
|
||||||
condition,
|
condition,
|
||||||
then,
|
then,
|
||||||
otherwise,
|
otherwise,
|
||||||
} => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"),
|
} => write!(f, "if {condition} {{ {then} }} else {{ {otherwise} }}"),
|
||||||
|
Self::Group { contents } => write!(f, "({contents})"),
|
||||||
|
Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
|
||||||
|
Self::Join {
|
||||||
|
lhs: Some(lhs),
|
||||||
|
rhs,
|
||||||
|
} => write!(f, "{lhs} / {rhs}"),
|
||||||
|
Self::Or { lhs, rhs } => write!(f, "{lhs} || {rhs}"),
|
||||||
Self::StringLiteral { string_literal } => write!(f, "{string_literal}"),
|
Self::StringLiteral { string_literal } => write!(f, "{string_literal}"),
|
||||||
Self::Variable { name } => write!(f, "{}", name.lexeme()),
|
Self::Variable { name } => write!(f, "{}", name.lexeme()),
|
||||||
Self::Call { thunk } => write!(f, "{thunk}"),
|
|
||||||
Self::Group { contents } => write!(f, "({contents})"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +92,13 @@ impl<'src> Serialize for Expression<'src> {
|
|||||||
S: Serializer,
|
S: Serializer,
|
||||||
{
|
{
|
||||||
match self {
|
match self {
|
||||||
|
Self::And { lhs, rhs } => {
|
||||||
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
|
seq.serialize_element("and")?;
|
||||||
|
seq.serialize_element(lhs)?;
|
||||||
|
seq.serialize_element(rhs)?;
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
Self::Assert { condition, error } => {
|
Self::Assert { condition, error } => {
|
||||||
let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;
|
let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;
|
||||||
seq.serialize_element("assert")?;
|
seq.serialize_element("assert")?;
|
||||||
@ -101,13 +120,6 @@ impl<'src> Serialize for Expression<'src> {
|
|||||||
seq.serialize_element(rhs)?;
|
seq.serialize_element(rhs)?;
|
||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
Self::Join { lhs, rhs } => {
|
|
||||||
let mut seq = serializer.serialize_seq(None)?;
|
|
||||||
seq.serialize_element("join")?;
|
|
||||||
seq.serialize_element(lhs)?;
|
|
||||||
seq.serialize_element(rhs)?;
|
|
||||||
seq.end()
|
|
||||||
}
|
|
||||||
Self::Conditional {
|
Self::Conditional {
|
||||||
condition,
|
condition,
|
||||||
then,
|
then,
|
||||||
@ -121,6 +133,20 @@ impl<'src> Serialize for Expression<'src> {
|
|||||||
seq.end()
|
seq.end()
|
||||||
}
|
}
|
||||||
Self::Group { contents } => contents.serialize(serializer),
|
Self::Group { contents } => contents.serialize(serializer),
|
||||||
|
Self::Join { lhs, rhs } => {
|
||||||
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
|
seq.serialize_element("join")?;
|
||||||
|
seq.serialize_element(lhs)?;
|
||||||
|
seq.serialize_element(rhs)?;
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
|
Self::Or { lhs, rhs } => {
|
||||||
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
|
seq.serialize_element("or")?;
|
||||||
|
seq.serialize_element(lhs)?;
|
||||||
|
seq.serialize_element(rhs)?;
|
||||||
|
seq.end()
|
||||||
|
}
|
||||||
Self::StringLiteral { string_literal } => string_literal.serialize(serializer),
|
Self::StringLiteral { string_literal } => string_literal.serialize(serializer),
|
||||||
Self::Variable { name } => {
|
Self::Variable { name } => {
|
||||||
let mut seq = serializer.serialize_seq(None)?;
|
let mut seq = serializer.serialize_seq(None)?;
|
||||||
|
@ -496,6 +496,7 @@ impl<'src> Lexer<'src> {
|
|||||||
']' => self.lex_delimiter(BracketR),
|
']' => self.lex_delimiter(BracketR),
|
||||||
'`' | '"' | '\'' => self.lex_string(),
|
'`' | '"' | '\'' => self.lex_string(),
|
||||||
'{' => self.lex_delimiter(BraceL),
|
'{' => self.lex_delimiter(BraceL),
|
||||||
|
'|' => self.lex_digraph('|', '|', BarBar),
|
||||||
'}' => self.lex_delimiter(BraceR),
|
'}' => self.lex_delimiter(BraceR),
|
||||||
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
||||||
_ => {
|
_ => {
|
||||||
@ -948,6 +949,7 @@ mod tests {
|
|||||||
Asterisk => "*",
|
Asterisk => "*",
|
||||||
At => "@",
|
At => "@",
|
||||||
BangEquals => "!=",
|
BangEquals => "!=",
|
||||||
|
BarBar => "||",
|
||||||
BraceL => "{",
|
BraceL => "{",
|
||||||
BraceR => "}",
|
BraceR => "}",
|
||||||
BracketL => "[",
|
BracketL => "[",
|
||||||
|
43
src/node.rs
43
src/node.rs
@ -88,6 +88,7 @@ impl<'src> Node<'src> for Assignment<'src> {
|
|||||||
impl<'src> Node<'src> for Expression<'src> {
|
impl<'src> Node<'src> for Expression<'src> {
|
||||||
fn tree(&self) -> Tree<'src> {
|
fn tree(&self) -> Tree<'src> {
|
||||||
match self {
|
match self {
|
||||||
|
Self::And { lhs, rhs } => Tree::atom("&&").push(lhs.tree()).push(rhs.tree()),
|
||||||
Self::Assert {
|
Self::Assert {
|
||||||
condition: Condition { lhs, rhs, operator },
|
condition: Condition { lhs, rhs, operator },
|
||||||
error,
|
error,
|
||||||
@ -96,25 +97,10 @@ impl<'src> Node<'src> for Expression<'src> {
|
|||||||
.push(operator.to_string())
|
.push(operator.to_string())
|
||||||
.push(rhs.tree())
|
.push(rhs.tree())
|
||||||
.push(error.tree()),
|
.push(error.tree()),
|
||||||
Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
|
||||||
Self::Conditional {
|
|
||||||
condition: Condition { lhs, rhs, operator },
|
|
||||||
then,
|
|
||||||
otherwise,
|
|
||||||
} => {
|
|
||||||
let mut tree = Tree::atom(Keyword::If.lexeme());
|
|
||||||
tree.push_mut(lhs.tree());
|
|
||||||
tree.push_mut(operator.to_string());
|
|
||||||
tree.push_mut(rhs.tree());
|
|
||||||
tree.push_mut(then.tree());
|
|
||||||
tree.push_mut(otherwise.tree());
|
|
||||||
tree
|
|
||||||
}
|
|
||||||
Self::Call { thunk } => {
|
Self::Call { thunk } => {
|
||||||
use Thunk::*;
|
use Thunk::*;
|
||||||
|
|
||||||
let mut tree = Tree::atom("call");
|
let mut tree = Tree::atom("call");
|
||||||
|
|
||||||
match thunk {
|
match thunk {
|
||||||
Nullary { name, .. } => tree.push_mut(name.lexeme()),
|
Nullary { name, .. } => tree.push_mut(name.lexeme()),
|
||||||
Unary { name, arg, .. } => {
|
Unary { name, arg, .. } => {
|
||||||
@ -171,20 +157,33 @@ impl<'src> Node<'src> for Expression<'src> {
|
|||||||
tree.push_mut(c.tree());
|
tree.push_mut(c.tree());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tree
|
tree
|
||||||
}
|
}
|
||||||
Self::Variable { name } => Tree::atom(name.lexeme()),
|
Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||||
Self::StringLiteral {
|
Self::Conditional {
|
||||||
string_literal: StringLiteral { cooked, .. },
|
condition: Condition { lhs, rhs, operator },
|
||||||
} => Tree::string(cooked),
|
then,
|
||||||
Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
|
otherwise,
|
||||||
|
} => {
|
||||||
|
let mut tree = Tree::atom(Keyword::If.lexeme());
|
||||||
|
tree.push_mut(lhs.tree());
|
||||||
|
tree.push_mut(operator.to_string());
|
||||||
|
tree.push_mut(rhs.tree());
|
||||||
|
tree.push_mut(then.tree());
|
||||||
|
tree.push_mut(otherwise.tree());
|
||||||
|
tree
|
||||||
|
}
|
||||||
Self::Group { contents } => Tree::List(vec![contents.tree()]),
|
Self::Group { contents } => Tree::List(vec![contents.tree()]),
|
||||||
Self::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()),
|
Self::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()),
|
||||||
Self::Join {
|
Self::Join {
|
||||||
lhs: Some(lhs),
|
lhs: Some(lhs),
|
||||||
rhs,
|
rhs,
|
||||||
} => Tree::atom("/").push(lhs.tree()).push(rhs.tree()),
|
} => Tree::atom("/").push(lhs.tree()).push(rhs.tree()),
|
||||||
|
Self::Or { lhs, rhs } => Tree::atom("||").push(lhs.tree()).push(rhs.tree()),
|
||||||
|
Self::StringLiteral {
|
||||||
|
string_literal: StringLiteral { cooked, .. },
|
||||||
|
} => Tree::string(cooked),
|
||||||
|
Self::Variable { name } => Tree::atom(name.lexeme()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ pub(crate) struct Parser<'run, 'src> {
|
|||||||
next_token: usize,
|
next_token: usize,
|
||||||
recursion_depth: usize,
|
recursion_depth: usize,
|
||||||
tokens: &'run [Token<'src>],
|
tokens: &'run [Token<'src>],
|
||||||
|
unstable_features: BTreeSet<UnstableFeature>,
|
||||||
working_directory: &'run Path,
|
working_directory: &'run Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
next_token: 0,
|
next_token: 0,
|
||||||
recursion_depth: 0,
|
recursion_depth: 0,
|
||||||
tokens,
|
tokens,
|
||||||
|
unstable_features: BTreeSet::new(),
|
||||||
working_directory,
|
working_directory,
|
||||||
}
|
}
|
||||||
.parse_ast()
|
.parse_ast()
|
||||||
@ -442,18 +444,19 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.next_token == self.tokens.len() {
|
if self.next_token != self.tokens.len() {
|
||||||
|
return Err(self.internal_error(format!(
|
||||||
|
"Parse completed with {} unparsed tokens",
|
||||||
|
self.tokens.len() - self.next_token,
|
||||||
|
))?);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Ast {
|
Ok(Ast {
|
||||||
items,
|
items,
|
||||||
|
unstable_features: self.unstable_features,
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
working_directory: self.working_directory.into(),
|
working_directory: self.working_directory.into(),
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
Err(self.internal_error(format!(
|
|
||||||
"Parse completed with {} unparsed tokens",
|
|
||||||
self.tokens.len() - self.next_token,
|
|
||||||
))?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an alias, e.g `alias name := target`
|
/// Parse an alias, e.g `alias name := target`
|
||||||
@ -517,26 +520,17 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
|
|
||||||
self.recursion_depth += 1;
|
self.recursion_depth += 1;
|
||||||
|
|
||||||
let expression = if self.accepted_keyword(Keyword::If)? {
|
let disjunct = self.parse_disjunct()?;
|
||||||
self.parse_conditional()?
|
|
||||||
} else if self.accepted(Slash)? {
|
|
||||||
let lhs = None;
|
|
||||||
let rhs = self.parse_expression()?.into();
|
|
||||||
Expression::Join { lhs, rhs }
|
|
||||||
} else {
|
|
||||||
let value = self.parse_value()?;
|
|
||||||
|
|
||||||
if self.accepted(Slash)? {
|
let expression = if self.accepted(BarBar)? {
|
||||||
let lhs = Some(Box::new(value));
|
self
|
||||||
|
.unstable_features
|
||||||
|
.insert(UnstableFeature::LogicalOperators);
|
||||||
|
let lhs = disjunct.into();
|
||||||
let rhs = self.parse_expression()?.into();
|
let rhs = self.parse_expression()?.into();
|
||||||
Expression::Join { lhs, rhs }
|
Expression::Or { lhs, rhs }
|
||||||
} else if self.accepted(Plus)? {
|
|
||||||
let lhs = value.into();
|
|
||||||
let rhs = self.parse_expression()?.into();
|
|
||||||
Expression::Concatenation { lhs, rhs }
|
|
||||||
} else {
|
} else {
|
||||||
value
|
disjunct
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.recursion_depth -= 1;
|
self.recursion_depth -= 1;
|
||||||
@ -544,6 +538,47 @@ impl<'run, 'src> Parser<'run, 'src> {
|
|||||||
Ok(expression)
|
Ok(expression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_disjunct(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
|
let conjunct = self.parse_conjunct()?;
|
||||||
|
|
||||||
|
let disjunct = if self.accepted(AmpersandAmpersand)? {
|
||||||
|
self
|
||||||
|
.unstable_features
|
||||||
|
.insert(UnstableFeature::LogicalOperators);
|
||||||
|
let lhs = conjunct.into();
|
||||||
|
let rhs = self.parse_disjunct()?.into();
|
||||||
|
Expression::And { lhs, rhs }
|
||||||
|
} else {
|
||||||
|
conjunct
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(disjunct)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_conjunct(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
|
if self.accepted_keyword(Keyword::If)? {
|
||||||
|
self.parse_conditional()
|
||||||
|
} else if self.accepted(Slash)? {
|
||||||
|
let lhs = None;
|
||||||
|
let rhs = self.parse_conjunct()?.into();
|
||||||
|
Ok(Expression::Join { lhs, rhs })
|
||||||
|
} else {
|
||||||
|
let value = self.parse_value()?;
|
||||||
|
|
||||||
|
if self.accepted(Slash)? {
|
||||||
|
let lhs = Some(Box::new(value));
|
||||||
|
let rhs = self.parse_conjunct()?.into();
|
||||||
|
Ok(Expression::Join { lhs, rhs })
|
||||||
|
} else if self.accepted(Plus)? {
|
||||||
|
let lhs = value.into();
|
||||||
|
let rhs = self.parse_conjunct()?.into();
|
||||||
|
Ok(Expression::Concatenation { lhs, rhs })
|
||||||
|
} else {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
||||||
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
let condition = self.parse_condition()?;
|
let condition = self.parse_condition()?;
|
||||||
|
@ -183,6 +183,10 @@ impl Assignment {
|
|||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||||
pub enum Expression {
|
pub enum Expression {
|
||||||
|
And {
|
||||||
|
lhs: Box<Expression>,
|
||||||
|
rhs: Box<Expression>,
|
||||||
|
},
|
||||||
Assert {
|
Assert {
|
||||||
condition: Condition,
|
condition: Condition,
|
||||||
error: Box<Expression>,
|
error: Box<Expression>,
|
||||||
@ -209,6 +213,10 @@ pub enum Expression {
|
|||||||
lhs: Option<Box<Expression>>,
|
lhs: Option<Box<Expression>>,
|
||||||
rhs: Box<Expression>,
|
rhs: Box<Expression>,
|
||||||
},
|
},
|
||||||
|
Or {
|
||||||
|
lhs: Box<Expression>,
|
||||||
|
rhs: Box<Expression>,
|
||||||
|
},
|
||||||
String {
|
String {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
@ -221,6 +229,10 @@ impl Expression {
|
|||||||
fn new(expression: &full::Expression) -> Self {
|
fn new(expression: &full::Expression) -> Self {
|
||||||
use full::Expression::*;
|
use full::Expression::*;
|
||||||
match expression {
|
match expression {
|
||||||
|
And { lhs, rhs } => Self::And {
|
||||||
|
lhs: Self::new(lhs).into(),
|
||||||
|
rhs: Self::new(rhs).into(),
|
||||||
|
},
|
||||||
Assert {
|
Assert {
|
||||||
condition: full::Condition { lhs, rhs, operator },
|
condition: full::Condition { lhs, rhs, operator },
|
||||||
error,
|
error,
|
||||||
@ -250,11 +262,9 @@ impl Expression {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut arguments = Vec::new();
|
let mut arguments = Vec::new();
|
||||||
|
|
||||||
if let Some(b) = opt_b.as_ref() {
|
if let Some(b) = opt_b.as_ref() {
|
||||||
arguments.push(Self::new(b));
|
arguments.push(Self::new(b));
|
||||||
}
|
}
|
||||||
|
|
||||||
arguments.push(Self::new(a));
|
arguments.push(Self::new(a));
|
||||||
Self::Call {
|
Self::Call {
|
||||||
name: name.lexeme().to_owned(),
|
name: name.lexeme().to_owned(),
|
||||||
@ -308,10 +318,6 @@ impl Expression {
|
|||||||
lhs: Self::new(lhs).into(),
|
lhs: Self::new(lhs).into(),
|
||||||
rhs: Self::new(rhs).into(),
|
rhs: Self::new(rhs).into(),
|
||||||
},
|
},
|
||||||
Join { lhs, rhs } => Self::Join {
|
|
||||||
lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()),
|
|
||||||
rhs: Self::new(rhs).into(),
|
|
||||||
},
|
|
||||||
Conditional {
|
Conditional {
|
||||||
condition: full::Condition { lhs, rhs, operator },
|
condition: full::Condition { lhs, rhs, operator },
|
||||||
otherwise,
|
otherwise,
|
||||||
@ -323,13 +329,21 @@ impl Expression {
|
|||||||
rhs: Self::new(rhs).into(),
|
rhs: Self::new(rhs).into(),
|
||||||
then: Self::new(then).into(),
|
then: Self::new(then).into(),
|
||||||
},
|
},
|
||||||
|
Group { contents } => Self::new(contents),
|
||||||
|
Join { lhs, rhs } => Self::Join {
|
||||||
|
lhs: lhs.as_ref().map(|lhs| Self::new(lhs).into()),
|
||||||
|
rhs: Self::new(rhs).into(),
|
||||||
|
},
|
||||||
|
Or { lhs, rhs } => Self::Or {
|
||||||
|
lhs: Self::new(lhs).into(),
|
||||||
|
rhs: Self::new(rhs).into(),
|
||||||
|
},
|
||||||
StringLiteral { string_literal } => Self::String {
|
StringLiteral { string_literal } => Self::String {
|
||||||
text: string_literal.cooked.clone(),
|
text: string_literal.cooked.clone(),
|
||||||
},
|
},
|
||||||
Variable { name, .. } => Self::Variable {
|
Variable { name, .. } => Self::Variable {
|
||||||
name: name.lexeme().to_owned(),
|
name: name.lexeme().to_owned(),
|
||||||
},
|
},
|
||||||
Group { contents } => Self::new(contents),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ pub(crate) enum TokenKind {
|
|||||||
At,
|
At,
|
||||||
Backtick,
|
Backtick,
|
||||||
BangEquals,
|
BangEquals,
|
||||||
|
BarBar,
|
||||||
BraceL,
|
BraceL,
|
||||||
BraceR,
|
BraceR,
|
||||||
BracketL,
|
BracketL,
|
||||||
@ -50,6 +51,7 @@ impl Display for TokenKind {
|
|||||||
At => "'@'",
|
At => "'@'",
|
||||||
Backtick => "backtick",
|
Backtick => "backtick",
|
||||||
BangEquals => "'!='",
|
BangEquals => "'!='",
|
||||||
|
BarBar => "'||'",
|
||||||
BraceL => "'{'",
|
BraceL => "'{'",
|
||||||
BraceR => "'}'",
|
BraceR => "'}'",
|
||||||
BracketL => "'['",
|
BracketL => "'['",
|
||||||
|
@ -3,6 +3,7 @@ use super::*;
|
|||||||
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
|
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
|
||||||
pub(crate) enum UnstableFeature {
|
pub(crate) enum UnstableFeature {
|
||||||
FormatSubcommand,
|
FormatSubcommand,
|
||||||
|
LogicalOperators,
|
||||||
ScriptAttribute,
|
ScriptAttribute,
|
||||||
ScriptInterpreterSetting,
|
ScriptInterpreterSetting,
|
||||||
}
|
}
|
||||||
@ -11,6 +12,10 @@ impl Display for UnstableFeature {
|
|||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
|
Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
|
||||||
|
Self::LogicalOperators => write!(
|
||||||
|
f,
|
||||||
|
"The logical operators `&&` and `||` are currently unstable."
|
||||||
|
),
|
||||||
Self::ScriptAttribute => write!(f, "The `[script]` attribute is currently unstable."),
|
Self::ScriptAttribute => write!(f, "The `[script]` attribute is currently unstable."),
|
||||||
Self::ScriptInterpreterSetting => {
|
Self::ScriptInterpreterSetting => {
|
||||||
write!(f, "The `script-interpreter` setting is currently unstable.")
|
write!(f, "The `script-interpreter` setting is currently unstable.")
|
||||||
|
@ -16,7 +16,24 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
fn next(&mut self) -> Option<Token<'src>> {
|
fn next(&mut self) -> Option<Token<'src>> {
|
||||||
loop {
|
loop {
|
||||||
match self.stack.pop()? {
|
match self.stack.pop()? {
|
||||||
Expression::StringLiteral { .. } | Expression::Backtick { .. } => {}
|
Expression::And { lhs, rhs } | Expression::Or { lhs, rhs } => {
|
||||||
|
self.stack.push(lhs);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
}
|
||||||
|
Expression::Assert {
|
||||||
|
condition:
|
||||||
|
Condition {
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
operator: _,
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
} => {
|
||||||
|
self.stack.push(error);
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.stack.push(lhs);
|
||||||
|
}
|
||||||
|
Expression::Backtick { .. } | Expression::StringLiteral { .. } => {}
|
||||||
Expression::Call { thunk } => match thunk {
|
Expression::Call { thunk } => match thunk {
|
||||||
Thunk::Nullary { .. } => {}
|
Thunk::Nullary { .. } => {}
|
||||||
Thunk::Unary { arg, .. } => self.stack.push(arg),
|
Thunk::Unary { arg, .. } => self.stack.push(arg),
|
||||||
@ -56,6 +73,10 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Expression::Concatenation { lhs, rhs } => {
|
||||||
|
self.stack.push(rhs);
|
||||||
|
self.stack.push(lhs);
|
||||||
|
}
|
||||||
Expression::Conditional {
|
Expression::Conditional {
|
||||||
condition:
|
condition:
|
||||||
Condition {
|
Condition {
|
||||||
@ -71,10 +92,8 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
}
|
}
|
||||||
Expression::Variable { name, .. } => return Some(name.token),
|
Expression::Group { contents } => {
|
||||||
Expression::Concatenation { lhs, rhs } => {
|
self.stack.push(contents);
|
||||||
self.stack.push(rhs);
|
|
||||||
self.stack.push(lhs);
|
|
||||||
}
|
}
|
||||||
Expression::Join { lhs, rhs } => {
|
Expression::Join { lhs, rhs } => {
|
||||||
self.stack.push(rhs);
|
self.stack.push(rhs);
|
||||||
@ -82,22 +101,7 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
|||||||
self.stack.push(lhs);
|
self.stack.push(lhs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expression::Group { contents } => {
|
Expression::Variable { name, .. } => return Some(name.token),
|
||||||
self.stack.push(contents);
|
|
||||||
}
|
|
||||||
Expression::Assert {
|
|
||||||
condition:
|
|
||||||
Condition {
|
|
||||||
lhs,
|
|
||||||
rhs,
|
|
||||||
operator: _,
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
} => {
|
|
||||||
self.stack.push(error);
|
|
||||||
self.stack.push(rhs);
|
|
||||||
self.stack.push(lhs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#[track_caller]
|
||||||
pub(crate) fn assert_success(output: &std::process::Output) {
|
pub(crate) fn assert_success(output: &std::process::Output) {
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
@ -136,7 +136,7 @@ test! {
|
|||||||
",
|
",
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "
|
stderr: "
|
||||||
error: Expected '!=', '==', '=~', '+', or '/', but found identifier
|
error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier
|
||||||
——▶ justfile:1:12
|
——▶ justfile:1:12
|
||||||
│
|
│
|
||||||
1 │ a := if '' a '' { '' } else { b }
|
1 │ a := if '' a '' { '' } else { b }
|
||||||
|
@ -125,7 +125,7 @@ fn comments_still_must_be_parsable_when_ignored() {
|
|||||||
)
|
)
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Expected '}}', '(', '+', or '/', but found identifier
|
error: Expected '&&', '||', '}}', '(', '+', or '/', but found identifier
|
||||||
——▶ justfile:4:12
|
——▶ justfile:4:12
|
||||||
│
|
│
|
||||||
4 │ # {{ foo bar }}
|
4 │ # {{ foo bar }}
|
||||||
|
@ -73,6 +73,7 @@ mod invocation_directory;
|
|||||||
mod json;
|
mod json;
|
||||||
mod line_prefixes;
|
mod line_prefixes;
|
||||||
mod list;
|
mod list;
|
||||||
|
mod logical_operators;
|
||||||
mod man;
|
mod man;
|
||||||
mod misc;
|
mod misc;
|
||||||
mod modules;
|
mod modules;
|
||||||
|
83
tests/logical_operators.rs
Normal file
83
tests/logical_operators.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn evaluate(expression: &str, expected: &str) {
|
||||||
|
Test::new()
|
||||||
|
.justfile(format!("x := {expression}"))
|
||||||
|
.env("JUST_UNSTABLE", "1")
|
||||||
|
.args(["--evaluate", "x"])
|
||||||
|
.stdout(expected)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_operators_are_unstable() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("x := 'foo' && 'bar'")
|
||||||
|
.args(["--evaluate", "x"])
|
||||||
|
.stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
Test::new()
|
||||||
|
.justfile("x := 'foo' || 'bar'")
|
||||||
|
.args(["--evaluate", "x"])
|
||||||
|
.stderr_regex(r"error: The logical operators `&&` and `\|\|` are currently unstable. .*")
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn and_returns_empty_string_if_lhs_is_empty() {
|
||||||
|
evaluate("'' && 'hello'", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn and_returns_rhs_if_lhs_is_non_empty() {
|
||||||
|
evaluate("'hello' && 'goodbye'", "goodbye");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn and_has_lower_precedence_than_plus() {
|
||||||
|
evaluate("'' && 'goodbye' + 'foo'", "");
|
||||||
|
|
||||||
|
evaluate("'foo' + 'hello' && 'goodbye'", "goodbye");
|
||||||
|
|
||||||
|
evaluate("'foo' + '' && 'goodbye'", "goodbye");
|
||||||
|
|
||||||
|
evaluate("'foo' + 'hello' && 'goodbye' + 'bar'", "goodbyebar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn or_returns_rhs_if_lhs_is_empty() {
|
||||||
|
evaluate("'' || 'hello'", "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn or_returns_lhs_if_lhs_is_non_empty() {
|
||||||
|
evaluate("'hello' || 'goodbye'", "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn or_has_lower_precedence_than_plus() {
|
||||||
|
evaluate("'' || 'goodbye' + 'foo'", "goodbyefoo");
|
||||||
|
|
||||||
|
evaluate("'foo' + 'hello' || 'goodbye'", "foohello");
|
||||||
|
|
||||||
|
evaluate("'foo' + '' || 'goodbye'", "foo");
|
||||||
|
|
||||||
|
evaluate("'foo' + 'hello' || 'goodbye' + 'bar'", "foohello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn and_has_higher_precedence_than_or() {
|
||||||
|
evaluate("('' && 'foo') || 'bar'", "bar");
|
||||||
|
evaluate("'' && 'foo' || 'bar'", "bar");
|
||||||
|
evaluate("'a' && 'b' || 'c'", "b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nesting() {
|
||||||
|
evaluate("'' || '' || '' || '' || 'foo'", "foo");
|
||||||
|
evaluate("'foo' && 'foo' && 'foo' && 'foo' && 'bar'", "bar");
|
||||||
|
}
|
@ -25,7 +25,7 @@ fn shell_expanded_strings_must_not_have_whitespace() {
|
|||||||
.status(1)
|
.status(1)
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Expected comment, end of file, end of line, '(', '+', or '/', but found string
|
error: Expected '&&', '||', comment, end of file, end of line, '(', '+', or '/', but found string
|
||||||
——▶ justfile:1:8
|
——▶ justfile:1:8
|
||||||
│
|
│
|
||||||
1 │ x := x '$JUST_TEST_VARIABLE'
|
1 │ x := x '$JUST_TEST_VARIABLE'
|
||||||
|
Loading…
Reference in New Issue
Block a user