From 154930cc8a14191154d9cc67b0c42a1feffc04ac Mon Sep 17 00:00:00 2001 From: Erik Krieg Date: Sun, 11 Sep 2022 03:48:02 -0400 Subject: [PATCH] Allow constructing absolute paths with `/` operator (#1320) --- README.md | 11 ++++++ src/assignment_resolver.rs | 8 ++++- src/evaluator.rs | 8 +++-- src/expression.rs | 8 +++-- src/node.rs | 6 +++- src/parser.rs | 8 ++++- src/summary.rs | 4 +-- src/variables.rs | 8 ++++- tests/slash_operator.rs | 74 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 124 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8eb95f40..61ff9211 100644 --- a/README.md +++ b/README.md @@ -924,6 +924,17 @@ $ just --evaluate bar a//b ``` +Absolute paths can also be constructed: + +```make +foo := / "b" +``` + +``` +$ just --evaluate foo +/b +``` + The `/` operator uses the `/` character, even on Windows. Thus, using the `/` operator should be avoided with paths that use universal naming convention (UNC), i.e., those that start with `\?`, since forward slashes are not supported with UNC paths. #### Escaping `{{` diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs index 87e458ad..b1fcd6e8 100644 --- a/src/assignment_resolver.rs +++ b/src/assignment_resolver.rs @@ -101,10 +101,16 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> { self.resolve_expression(c) } }, - Expression::Concatenation { lhs, rhs } | Expression::Join { lhs, rhs } => { + Expression::Concatenation { lhs, rhs } => { self.resolve_expression(lhs)?; self.resolve_expression(rhs) } + Expression::Join { lhs, rhs } => { + if let Some(lhs) = lhs { + self.resolve_expression(lhs)?; + } + self.resolve_expression(rhs) + } Expression::Conditional { lhs, rhs, diff --git a/src/evaluator.rs b/src/evaluator.rs index 7f45c91d..2d4cb757 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -176,9 +176,11 @@ impl<'src, 'run> Evaluator<'src, 'run> { } } Expression::Group { contents } => self.evaluate_expression(contents), - Expression::Join { lhs, rhs } => { - Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?) - } + Expression::Join { lhs: None, rhs } => Ok("/".to_string() + &self.evaluate_expression(rhs)?), + Expression::Join { + lhs: Some(lhs), + rhs, + } => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?), } } diff --git a/src/expression.rs b/src/expression.rs index 0393159d..4c0be54f 100644 --- a/src/expression.rs +++ b/src/expression.rs @@ -32,7 +32,7 @@ pub(crate) enum Expression<'src> { Group { contents: Box> }, /// `lhs / rhs` Join { - lhs: Box>, + lhs: Option>>, rhs: Box>, }, /// `"string_literal"` or `'string_literal'` @@ -51,7 +51,11 @@ impl<'src> Display for Expression<'src> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { match self { Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()), - Expression::Join { lhs, rhs } => write!(f, "{} / {}", lhs, rhs), + Expression::Join { lhs: None, rhs } => write!(f, "/ {}", rhs), + Expression::Join { + lhs: Some(lhs), + rhs, + } => write!(f, "{} / {}", lhs, rhs), Expression::Concatenation { lhs, rhs } => write!(f, "{} + {}", lhs, rhs), Expression::Conditional { lhs, diff --git a/src/node.rs b/src/node.rs index 6afc0a43..60eb0422 100644 --- a/src/node.rs +++ b/src/node.rs @@ -118,7 +118,11 @@ impl<'src> Node<'src> for Expression<'src> { } => Tree::string(cooked), Expression::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), Expression::Group { contents } => Tree::List(vec![contents.tree()]), - Expression::Join { lhs, rhs } => Tree::atom("/").push(lhs.tree()).push(rhs.tree()), + Expression::Join { lhs: None, rhs } => Tree::atom("/").push(rhs.tree()), + Expression::Join { + lhs: Some(lhs), + rhs, + } => Tree::atom("/").push(lhs.tree()).push(rhs.tree()), } } } diff --git a/src/parser.rs b/src/parser.rs index e04c1da9..effb9ca3 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -406,11 +406,15 @@ impl<'tokens, 'src> Parser<'tokens, 'src> { let expression = if self.accepted_keyword(Keyword::If)? { self.parse_conditional()? + } else if self.accepted(Slash)? { + let lhs = None; + let rhs = Box::new(self.parse_expression()?); + Expression::Join { lhs, rhs } } else { let value = self.parse_value()?; if self.accepted(Slash)? { - let lhs = Box::new(value); + let lhs = Some(Box::new(value)); let rhs = Box::new(self.parse_expression()?); Expression::Join { lhs, rhs } } else if self.accepted(Plus)? { @@ -1991,6 +1995,7 @@ mod tests { Identifier, ParenL, ParenR, + Slash, StringToken, ], found: Eof, @@ -2010,6 +2015,7 @@ mod tests { Identifier, ParenL, ParenR, + Slash, StringToken, ], found: InterpolationEnd, diff --git a/src/summary.rs b/src/summary.rs index 7d467ddb..89578b5a 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -197,7 +197,7 @@ pub enum Expression { operator: ConditionalOperator, }, Join { - lhs: Box, + lhs: Option>, rhs: Box, }, String { @@ -258,7 +258,7 @@ impl Expression { rhs: Box::new(Expression::new(rhs)), }, Join { lhs, rhs } => Expression::Join { - lhs: Box::new(Expression::new(lhs)), + lhs: lhs.as_ref().map(|lhs| Box::new(Expression::new(lhs))), rhs: Box::new(Expression::new(rhs)), }, Conditional { diff --git a/src/variables.rs b/src/variables.rs index f1bd506f..9d620a23 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -53,10 +53,16 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> { self.stack.push(lhs); } Expression::Variable { name, .. } => return Some(name.token()), - Expression::Concatenation { lhs, rhs } | Expression::Join { lhs, rhs } => { + Expression::Concatenation { lhs, rhs } => { self.stack.push(rhs); self.stack.push(lhs); } + Expression::Join { lhs, rhs } => { + self.stack.push(rhs); + if let Some(lhs) = lhs { + self.stack.push(lhs); + } + } Expression::Group { contents } => { self.stack.push(contents); } diff --git a/tests/slash_operator.rs b/tests/slash_operator.rs index 5995a32e..31859e3b 100644 --- a/tests/slash_operator.rs +++ b/tests/slash_operator.rs @@ -18,6 +18,45 @@ fn twice() { .run(); } +#[test] +fn no_lhs_once() { + Test::new() + .justfile("x := / 'a'") + .args(&["--evaluate", "x"]) + .stdout("/a") + .run(); +} + +#[test] +fn no_lhs_twice() { + Test::new() + .justfile("x := / 'a' / 'b'") + .args(&["--evaluate", "x"]) + .stdout("/a/b") + .run(); + Test::new() + .justfile("x := // 'a'") + .args(&["--evaluate", "x"]) + .stdout("//a") + .run(); +} + +#[test] +fn no_rhs_once() { + Test::new() + .justfile("x := 'a' /") + .stderr( + " + error: Expected backtick, identifier, '(', '/', or string, but found end of file + | + 1 | x := 'a' / + | ^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + #[test] fn default_un_parenthesized() { Test::new() @@ -39,6 +78,27 @@ fn default_un_parenthesized() { .run(); } +#[test] +fn no_lhs_un_parenthesized() { + Test::new() + .justfile( + " + foo x=/ 'a' / 'b': + echo {{x}} + ", + ) + .stderr( + " + error: Expected backtick, identifier, '(', or string, but found '/' + | + 1 | foo x=/ 'a' / 'b': + | ^ + ", + ) + .status(EXIT_FAILURE) + .run(); +} + #[test] fn default_parenthesized() { Test::new() @@ -52,3 +112,17 @@ fn default_parenthesized() { .stdout("a/b\n") .run(); } + +#[test] +fn no_lhs_parenthesized() { + Test::new() + .justfile( + " + foo x=(/ 'a' / 'b'): + echo {{x}} + ", + ) + .stderr("echo /a/b\n") + .stdout("/a/b\n") + .run(); +}