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
|
||||
|
||||
expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
||||
expression : disjunct || expression
|
||||
| disjunct
|
||||
|
||||
disjunct : conjunct && disjunct
|
||||
| conjunct
|
||||
|
||||
conjunct : 'if' condition '{' expression '}' 'else' '{' expression '}'
|
||||
| 'assert' '(' condition ',' expression ')'
|
||||
| '/' expression
|
||||
| value '/' expression
|
||||
|
105
README.md
105
README.md
@ -1290,9 +1290,11 @@ Available recipes:
|
||||
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
|
||||
tmpdir := `mktemp -d`
|
||||
@ -1310,6 +1312,39 @@ publish:
|
||||
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
|
||||
|
||||
The `/` operator can be used to join two strings with a slash:
|
||||
@ -2367,8 +2402,8 @@ Testing server:unit…
|
||||
./test --tests unit server
|
||||
```
|
||||
|
||||
Default values may be arbitrary expressions, but concatenations or path joins
|
||||
must be parenthesized:
|
||||
Default values may be arbitrary expressions, but expressions containing the
|
||||
`+`, `&&`, `||`, or `/` operators must be parenthesized:
|
||||
|
||||
```just
|
||||
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
|
||||
set `just` variables in the middle of a recipe:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
foo:
|
||||
x := "hello" # This doesn't work!
|
||||
echo {{x}}
|
||||
@ -2872,7 +2907,7 @@ means that multi-line constructs probably won't do what you want.
|
||||
|
||||
For example, with the following `justfile`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
conditional:
|
||||
if true; then
|
||||
echo 'True!'
|
||||
@ -3279,7 +3314,7 @@ One `justfile` can include the contents of another using `import` statements.
|
||||
|
||||
If you have the following `justfile`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
import 'foo/bar.just'
|
||||
|
||||
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:
|
||||
|
||||
```mf
|
||||
```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
|
||||
example `baz.just`, without the duplicate import of `baz.just` being an error:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# justfile
|
||||
import 'foo.just'
|
||||
import 'bar.just'
|
||||
```
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# foo.just
|
||||
import 'baz.just'
|
||||
foo: baz
|
||||
```
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# bar.just
|
||||
import 'baz.just'
|
||||
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`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
mod bar
|
||||
|
||||
a:
|
||||
@ -3399,7 +3434,7 @@ the module file may have any capitalization.
|
||||
|
||||
Module statements may be of the form:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
mod foo 'PATH'
|
||||
```
|
||||
|
||||
@ -3423,7 +3458,7 @@ recipes.
|
||||
|
||||
Modules may be made optional by putting a `?` after the `mod` keyword:
|
||||
|
||||
```mf
|
||||
```just
|
||||
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
|
||||
long as at most one source file exists:
|
||||
|
||||
```mf
|
||||
```just
|
||||
mod? foo 'bar.just'
|
||||
mod? foo 'baz.just'
|
||||
```
|
||||
@ -3441,7 +3476,7 @@ mod? foo 'baz.just'
|
||||
Modules may be given doc comments which appear in `--list`
|
||||
output<sup>1.30.0</sup>:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# foo is a great module!
|
||||
mod foo
|
||||
```
|
||||
@ -3583,9 +3618,9 @@ The following command will create two files, `some` and `argument.txt`:
|
||||
$ just foo "some argument.txt"
|
||||
```
|
||||
|
||||
The users shell will parse `"some argument.txt"` as a single argument, but when
|
||||
`just` replaces `touch {{argument}}` with `touch some argument.txt`, the quotes
|
||||
are not preserved, and `touch` will receive two arguments.
|
||||
The user's shell will parse `"some argument.txt"` as a single argument, but
|
||||
when `just` replaces `touch {{argument}}` with `touch some argument.txt`, the
|
||||
quotes are not preserved, and `touch` will receive two arguments.
|
||||
|
||||
There are a few ways to avoid this: quoting, positional arguments, and exported
|
||||
arguments.
|
||||
@ -3910,6 +3945,38 @@ fetch:
|
||||
Given the above `justfile`, after running `just fetch`, the recipes in
|
||||
`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
|
||||
|
||||
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>> {
|
||||
let mut definitions = HashMap::new();
|
||||
let mut imports = HashSet::new();
|
||||
let mut unstable_features = BTreeSet::new();
|
||||
|
||||
let mut stack = Vec::new();
|
||||
let ast = asts.get(root).unwrap();
|
||||
stack.push(ast);
|
||||
|
||||
while let Some(ast) = stack.pop() {
|
||||
unstable_features.extend(&ast.unstable_features);
|
||||
|
||||
for item in &ast.items {
|
||||
match item {
|
||||
Item::Alias(alias) => {
|
||||
@ -166,8 +169,6 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
||||
aliases.insert(Self::resolve_alias(&recipes, alias)?);
|
||||
}
|
||||
|
||||
let mut unstable_features = BTreeSet::new();
|
||||
|
||||
for recipe in recipes.values() {
|
||||
for attribute in &recipe.attributes {
|
||||
if let Attribute::Script(_) = attribute {
|
||||
|
@ -31,7 +31,29 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||
self.stack.push(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);
|
||||
} else {
|
||||
let message = format!("attempted to resolve unknown assignment `{name}`");
|
||||
@ -51,112 +73,6 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||
|
||||
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)]
|
||||
|
@ -6,6 +6,7 @@ use super::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Ast<'src> {
|
||||
pub(crate) items: Vec<Item<'src>>,
|
||||
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
pub(crate) working_directory: PathBuf,
|
||||
}
|
||||
|
@ -84,24 +84,31 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
expression: &Expression<'src>,
|
||||
) -> RunResult<'src, String> {
|
||||
match expression {
|
||||
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())
|
||||
Expression::And { lhs, rhs } => {
|
||||
let lhs = self.evaluate_expression(lhs)?;
|
||||
if lhs.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
self.evaluate_expression(rhs)
|
||||
}
|
||||
Expression::Assert { condition, error } => {
|
||||
if self.evaluate_condition(condition)? {
|
||||
Ok(String::new())
|
||||
} else {
|
||||
Err(Error::Internal {
|
||||
message: format!("attempted to evaluate undefined variable `{variable}`"),
|
||||
Err(Error::Assert {
|
||||
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 } => {
|
||||
use Thunk::*;
|
||||
|
||||
let result = match thunk {
|
||||
Nullary { function, .. } => function(function::Context::new(self, thunk.name())),
|
||||
Unary { function, arg, .. } => {
|
||||
@ -118,7 +125,6 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
Some(b) => Some(self.evaluate_expression(b)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
function(function::Context::new(self, thunk.name()), &a, b.as_deref())
|
||||
}
|
||||
UnaryPlus {
|
||||
@ -175,20 +181,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
function(function::Context::new(self, thunk.name()), &a, &b, &c)
|
||||
}
|
||||
};
|
||||
|
||||
result.map_err(|message| Error::FunctionCall {
|
||||
function: thunk.name(),
|
||||
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 } => {
|
||||
Ok(self.evaluate_expression(lhs)? + &self.evaluate_expression(rhs)?)
|
||||
}
|
||||
@ -209,12 +206,26 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
lhs: Some(lhs),
|
||||
rhs,
|
||||
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
|
||||
Expression::Assert { condition, error } => {
|
||||
if self.evaluate_condition(condition)? {
|
||||
Ok(String::new())
|
||||
Expression::Or { lhs, rhs } => {
|
||||
let lhs = self.evaluate_expression(lhs)?;
|
||||
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 {
|
||||
Err(Error::Assert {
|
||||
message: self.evaluate_expression(error)?,
|
||||
Err(Error::Internal {
|
||||
message: format!("attempted to evaluate undefined variable `{variable}`"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ use super::*;
|
||||
/// The parser parses both values and expressions into `Expression`s.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub(crate) enum Expression<'src> {
|
||||
/// `lhs && rhs`
|
||||
And {
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `assert(condition, error)`
|
||||
Assert {
|
||||
condition: Condition<'src>,
|
||||
@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> {
|
||||
lhs: Option<Box<Expression<'src>>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `lhs || rhs`
|
||||
Or {
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `"string_literal"` or `'string_literal'`
|
||||
StringLiteral { string_literal: StringLiteral<'src> },
|
||||
/// `variable`
|
||||
@ -53,23 +63,25 @@ impl<'src> Expression<'src> {
|
||||
impl<'src> Display for Expression<'src> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::And { lhs, rhs } => write!(f, "{lhs} && {rhs}"),
|
||||
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
||||
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
||||
Self::Join { lhs: None, rhs } => write!(f, "/ {rhs}"),
|
||||
Self::Join {
|
||||
lhs: Some(lhs),
|
||||
rhs,
|
||||
} => write!(f, "{lhs} / {rhs}"),
|
||||
Self::Call { thunk } => write!(f, "{thunk}"),
|
||||
Self::Concatenation { lhs, rhs } => write!(f, "{lhs} + {rhs}"),
|
||||
Self::Conditional {
|
||||
condition,
|
||||
then,
|
||||
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::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,
|
||||
{
|
||||
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 } => {
|
||||
let mut seq: <S as Serializer>::SerializeSeq = serializer.serialize_seq(None)?;
|
||||
seq.serialize_element("assert")?;
|
||||
@ -101,13 +120,6 @@ impl<'src> Serialize for Expression<'src> {
|
||||
seq.serialize_element(rhs)?;
|
||||
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 {
|
||||
condition,
|
||||
then,
|
||||
@ -121,6 +133,20 @@ impl<'src> Serialize for Expression<'src> {
|
||||
seq.end()
|
||||
}
|
||||
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::Variable { name } => {
|
||||
let mut seq = serializer.serialize_seq(None)?;
|
||||
|
@ -496,6 +496,7 @@ impl<'src> Lexer<'src> {
|
||||
']' => self.lex_delimiter(BracketR),
|
||||
'`' | '"' | '\'' => self.lex_string(),
|
||||
'{' => self.lex_delimiter(BraceL),
|
||||
'|' => self.lex_digraph('|', '|', BarBar),
|
||||
'}' => self.lex_delimiter(BraceR),
|
||||
_ if Self::is_identifier_start(start) => self.lex_identifier(),
|
||||
_ => {
|
||||
@ -948,6 +949,7 @@ mod tests {
|
||||
Asterisk => "*",
|
||||
At => "@",
|
||||
BangEquals => "!=",
|
||||
BarBar => "||",
|
||||
BraceL => "{",
|
||||
BraceR => "}",
|
||||
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> {
|
||||
fn tree(&self) -> Tree<'src> {
|
||||
match self {
|
||||
Self::And { lhs, rhs } => Tree::atom("&&").push(lhs.tree()).push(rhs.tree()),
|
||||
Self::Assert {
|
||||
condition: Condition { lhs, rhs, operator },
|
||||
error,
|
||||
@ -96,25 +97,10 @@ impl<'src> Node<'src> for Expression<'src> {
|
||||
.push(operator.to_string())
|
||||
.push(rhs.tree())
|
||||
.push(error.tree()),
|
||||
Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||
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::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
|
||||
Self::Call { thunk } => {
|
||||
use Thunk::*;
|
||||
|
||||
let mut tree = Tree::atom("call");
|
||||
|
||||
match thunk {
|
||||
Nullary { name, .. } => tree.push_mut(name.lexeme()),
|
||||
Unary { name, arg, .. } => {
|
||||
@ -171,20 +157,33 @@ impl<'src> Node<'src> for Expression<'src> {
|
||||
tree.push_mut(c.tree());
|
||||
}
|
||||
}
|
||||
|
||||
tree
|
||||
}
|
||||
Self::Variable { name } => Tree::atom(name.lexeme()),
|
||||
Self::StringLiteral {
|
||||
string_literal: StringLiteral { cooked, .. },
|
||||
} => Tree::string(cooked),
|
||||
Self::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
|
||||
Self::Concatenation { lhs, rhs } => Tree::atom("+").push(lhs.tree()).push(rhs.tree()),
|
||||
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::Group { contents } => Tree::List(vec![contents.tree()]),
|
||||
Self::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()),
|
||||
Self::Join {
|
||||
lhs: Some(lhs),
|
||||
rhs,
|
||||
} => 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,
|
||||
recursion_depth: usize,
|
||||
tokens: &'run [Token<'src>],
|
||||
unstable_features: BTreeSet<UnstableFeature>,
|
||||
working_directory: &'run Path,
|
||||
}
|
||||
|
||||
@ -51,6 +52,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
next_token: 0,
|
||||
recursion_depth: 0,
|
||||
tokens,
|
||||
unstable_features: BTreeSet::new(),
|
||||
working_directory,
|
||||
}
|
||||
.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 {
|
||||
items,
|
||||
unstable_features: self.unstable_features,
|
||||
warnings: Vec::new(),
|
||||
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`
|
||||
@ -517,26 +520,17 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
|
||||
self.recursion_depth += 1;
|
||||
|
||||
let expression = if self.accepted_keyword(Keyword::If)? {
|
||||
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()?;
|
||||
let disjunct = self.parse_disjunct()?;
|
||||
|
||||
if self.accepted(Slash)? {
|
||||
let lhs = Some(Box::new(value));
|
||||
let expression = if self.accepted(BarBar)? {
|
||||
self
|
||||
.unstable_features
|
||||
.insert(UnstableFeature::LogicalOperators);
|
||||
let lhs = disjunct.into();
|
||||
let rhs = self.parse_expression()?.into();
|
||||
Expression::Join { lhs, rhs }
|
||||
} else if self.accepted(Plus)? {
|
||||
let lhs = value.into();
|
||||
let rhs = self.parse_expression()?.into();
|
||||
Expression::Concatenation { lhs, rhs }
|
||||
Expression::Or { lhs, rhs }
|
||||
} else {
|
||||
value
|
||||
}
|
||||
disjunct
|
||||
};
|
||||
|
||||
self.recursion_depth -= 1;
|
||||
@ -544,6 +538,47 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
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" }`
|
||||
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||
let condition = self.parse_condition()?;
|
||||
|
@ -183,6 +183,10 @@ impl Assignment {
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||
pub enum Expression {
|
||||
And {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
Assert {
|
||||
condition: Condition,
|
||||
error: Box<Expression>,
|
||||
@ -209,6 +213,10 @@ pub enum Expression {
|
||||
lhs: Option<Box<Expression>>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
Or {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
String {
|
||||
text: String,
|
||||
},
|
||||
@ -221,6 +229,10 @@ impl Expression {
|
||||
fn new(expression: &full::Expression) -> Self {
|
||||
use full::Expression::*;
|
||||
match expression {
|
||||
And { lhs, rhs } => Self::And {
|
||||
lhs: Self::new(lhs).into(),
|
||||
rhs: Self::new(rhs).into(),
|
||||
},
|
||||
Assert {
|
||||
condition: full::Condition { lhs, rhs, operator },
|
||||
error,
|
||||
@ -250,11 +262,9 @@ impl Expression {
|
||||
..
|
||||
} => {
|
||||
let mut arguments = Vec::new();
|
||||
|
||||
if let Some(b) = opt_b.as_ref() {
|
||||
arguments.push(Self::new(b));
|
||||
}
|
||||
|
||||
arguments.push(Self::new(a));
|
||||
Self::Call {
|
||||
name: name.lexeme().to_owned(),
|
||||
@ -308,10 +318,6 @@ impl Expression {
|
||||
lhs: Self::new(lhs).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 {
|
||||
condition: full::Condition { lhs, rhs, operator },
|
||||
otherwise,
|
||||
@ -323,13 +329,21 @@ impl Expression {
|
||||
rhs: Self::new(rhs).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 {
|
||||
text: string_literal.cooked.clone(),
|
||||
},
|
||||
Variable { name, .. } => Self::Variable {
|
||||
name: name.lexeme().to_owned(),
|
||||
},
|
||||
Group { contents } => Self::new(contents),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ pub(crate) enum TokenKind {
|
||||
At,
|
||||
Backtick,
|
||||
BangEquals,
|
||||
BarBar,
|
||||
BraceL,
|
||||
BraceR,
|
||||
BracketL,
|
||||
@ -50,6 +51,7 @@ impl Display for TokenKind {
|
||||
At => "'@'",
|
||||
Backtick => "backtick",
|
||||
BangEquals => "'!='",
|
||||
BarBar => "'||'",
|
||||
BraceL => "'{'",
|
||||
BraceR => "'}'",
|
||||
BracketL => "'['",
|
||||
|
@ -3,6 +3,7 @@ use super::*;
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
|
||||
pub(crate) enum UnstableFeature {
|
||||
FormatSubcommand,
|
||||
LogicalOperators,
|
||||
ScriptAttribute,
|
||||
ScriptInterpreterSetting,
|
||||
}
|
||||
@ -11,6 +12,10 @@ impl Display for UnstableFeature {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
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::ScriptInterpreterSetting => {
|
||||
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>> {
|
||||
loop {
|
||||
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 {
|
||||
Thunk::Nullary { .. } => {}
|
||||
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 {
|
||||
condition:
|
||||
Condition {
|
||||
@ -71,10 +92,8 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
self.stack.push(rhs);
|
||||
self.stack.push(lhs);
|
||||
}
|
||||
Expression::Variable { name, .. } => return Some(name.token),
|
||||
Expression::Concatenation { lhs, rhs } => {
|
||||
self.stack.push(rhs);
|
||||
self.stack.push(lhs);
|
||||
Expression::Group { contents } => {
|
||||
self.stack.push(contents);
|
||||
}
|
||||
Expression::Join { lhs, rhs } => {
|
||||
self.stack.push(rhs);
|
||||
@ -82,22 +101,7 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
self.stack.push(lhs);
|
||||
}
|
||||
}
|
||||
Expression::Group { contents } => {
|
||||
self.stack.push(contents);
|
||||
}
|
||||
Expression::Assert {
|
||||
condition:
|
||||
Condition {
|
||||
lhs,
|
||||
rhs,
|
||||
operator: _,
|
||||
},
|
||||
error,
|
||||
} => {
|
||||
self.stack.push(error);
|
||||
self.stack.push(rhs);
|
||||
self.stack.push(lhs);
|
||||
}
|
||||
Expression::Variable { name, .. } => return Some(name.token),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
#[track_caller]
|
||||
pub(crate) fn assert_success(output: &std::process::Output) {
|
||||
if !output.status.success() {
|
||||
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
|
@ -136,7 +136,7 @@ test! {
|
||||
",
|
||||
stdout: "",
|
||||
stderr: "
|
||||
error: Expected '!=', '==', '=~', '+', or '/', but found identifier
|
||||
error: Expected '&&', '!=', '||', '==', '=~', '+', or '/', but found identifier
|
||||
——▶ justfile:1:12
|
||||
│
|
||||
1 │ a := if '' a '' { '' } else { b }
|
||||
|
@ -125,7 +125,7 @@ fn comments_still_must_be_parsable_when_ignored() {
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Expected '}}', '(', '+', or '/', but found identifier
|
||||
error: Expected '&&', '||', '}}', '(', '+', or '/', but found identifier
|
||||
——▶ justfile:4:12
|
||||
│
|
||||
4 │ # {{ foo bar }}
|
||||
|
@ -73,6 +73,7 @@ mod invocation_directory;
|
||||
mod json;
|
||||
mod line_prefixes;
|
||||
mod list;
|
||||
mod logical_operators;
|
||||
mod man;
|
||||
mod misc;
|
||||
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)
|
||||
.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
|
||||
│
|
||||
1 │ x := x '$JUST_TEST_VARIABLE'
|
||||
|
Loading…
Reference in New Issue
Block a user