diff --git a/GRAMMAR.md b/GRAMMAR.md
index e5847f50..00721f15 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -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
diff --git a/README.md b/README.md
index ca596854..bccf9a13 100644
--- a/README.md
+++ b/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
+valuesmaster, 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:
+
+```mf
+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:
+
+```mf
+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"
diff --git a/src/analyzer.rs b/src/analyzer.rs
index 9a26ef04..0929294c 100644
--- a/src/analyzer.rs
+++ b/src/analyzer.rs
@@ -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 {
diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs
index 53863fc9..511464d4 100644
--- a/src/assignment_resolver.rs
+++ b/src/assignment_resolver.rs
@@ -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)]
diff --git a/src/ast.rs b/src/ast.rs
index f9dd10c9..1ad7a8aa 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -6,6 +6,7 @@ use super::*;
#[derive(Debug, Clone)]
pub(crate) struct Ast<'src> {
pub(crate) items: Vec- >,
+ pub(crate) unstable_features: BTreeSet,
pub(crate) warnings: Vec,
pub(crate) working_directory: PathBuf,
}
diff --git a/src/evaluator.rs b/src/evaluator.rs
index 4ed00036..d48cece8 100644
--- a/src/evaluator.rs
+++ b/src/evaluator.rs
@@ -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}`"),
})
}
}
diff --git a/src/expression.rs b/src/expression.rs
index c1e4b5e0..3d5c3392 100644
--- a/src/expression.rs
+++ b/src/expression.rs
@@ -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>,
+ rhs: Box>,
+ },
/// `assert(condition, error)`
Assert {
condition: Condition<'src>,
@@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> {
lhs: Option>>,
rhs: Box>,
},
+ /// `lhs || rhs`
+ Or {
+ lhs: Box>,
+ rhs: Box>,
+ },
/// `"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:
::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)?;
diff --git a/src/lexer.rs b/src/lexer.rs
index 4d28a446..2c56db9d 100644
--- a/src/lexer.rs
+++ b/src/lexer.rs
@@ -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 => "[",
diff --git a/src/node.rs b/src/node.rs
index 3ccf862d..f8788c4d 100644
--- a/src/node.rs
+++ b/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()),
}
}
}
diff --git a/src/parser.rs b/src/parser.rs
index 99544acd..e03ab783 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -31,6 +31,7 @@ pub(crate) struct Parser<'run, 'src> {
next_token: usize,
recursion_depth: usize,
tokens: &'run [Token<'src>],
+ unstable_features: BTreeSet,
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() {
- Ok(Ast {
- items,
- warnings: Vec::new(),
- working_directory: self.working_directory.into(),
- })
- } else {
- Err(self.internal_error(format!(
+ 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(),
+ })
}
/// 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 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 }
- } else {
- value
- }
+ let expression = if self.accepted(BarBar)? {
+ self
+ .unstable_features
+ .insert(UnstableFeature::LogicalOperators);
+ let lhs = disjunct.into();
+ let rhs = self.parse_expression()?.into();
+ Expression::Or { lhs, rhs }
+ } else {
+ 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()?;
diff --git a/src/summary.rs b/src/summary.rs
index ee3a8d11..76483d63 100644
--- a/src/summary.rs
+++ b/src/summary.rs
@@ -183,6 +183,10 @@ impl Assignment {
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
pub enum Expression {
+ And {
+ lhs: Box,
+ rhs: Box,
+ },
Assert {
condition: Condition,
error: Box,
@@ -209,6 +213,10 @@ pub enum Expression {
lhs: Option>,
rhs: Box,
},
+ Or {
+ lhs: Box,
+ rhs: Box,
+ },
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),
}
}
}
diff --git a/src/token_kind.rs b/src/token_kind.rs
index 0db15d2d..850afa96 100644
--- a/src/token_kind.rs
+++ b/src/token_kind.rs
@@ -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 => "'['",
diff --git a/src/unstable_feature.rs b/src/unstable_feature.rs
index 07d99540..70e26fab 100644
--- a/src/unstable_feature.rs
+++ b/src/unstable_feature.rs
@@ -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.")
diff --git a/src/variables.rs b/src/variables.rs
index 57979563..9de1c98c 100644
--- a/src/variables.rs
+++ b/src/variables.rs
@@ -16,7 +16,24 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
fn next(&mut self) -> Option> {
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),
}
}
}
diff --git a/tests/assert_success.rs b/tests/assert_success.rs
index bcb364f8..f9202b7f 100644
--- a/tests/assert_success.rs
+++ b/tests/assert_success.rs
@@ -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));
diff --git a/tests/conditional.rs b/tests/conditional.rs
index c1739240..4eab2f4d 100644
--- a/tests/conditional.rs
+++ b/tests/conditional.rs
@@ -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 }
diff --git a/tests/ignore_comments.rs b/tests/ignore_comments.rs
index c3028a57..3f068227 100644
--- a/tests/ignore_comments.rs
+++ b/tests/ignore_comments.rs
@@ -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 }}
diff --git a/tests/lib.rs b/tests/lib.rs
index ec71c665..7c85460b 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -73,6 +73,7 @@ mod invocation_directory;
mod json;
mod line_prefixes;
mod list;
+mod logical_operators;
mod man;
mod misc;
mod modules;
diff --git a/tests/logical_operators.rs b/tests/logical_operators.rs
new file mode 100644
index 00000000..5baa0f52
--- /dev/null
+++ b/tests/logical_operators.rs
@@ -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");
+}
diff --git a/tests/shell_expansion.rs b/tests/shell_expansion.rs
index 67fdc07d..954e2a33 100644
--- a/tests/shell_expansion.rs
+++ b/tests/shell_expansion.rs
@@ -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'