Allow constructing absolute paths with / operator (#1320)

This commit is contained in:
Erik Krieg 2022-09-11 03:48:02 -04:00 committed by GitHub
parent baaa8cb194
commit 154930cc8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 124 additions and 11 deletions

View File

@ -924,6 +924,17 @@ $ just --evaluate bar
a//b 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. 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 `{{` #### Escaping `{{`

View File

@ -101,10 +101,16 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
self.resolve_expression(c) self.resolve_expression(c)
} }
}, },
Expression::Concatenation { lhs, rhs } | Expression::Join { lhs, rhs } => { Expression::Concatenation { lhs, rhs } => {
self.resolve_expression(lhs)?; self.resolve_expression(lhs)?;
self.resolve_expression(rhs) self.resolve_expression(rhs)
} }
Expression::Join { lhs, rhs } => {
if let Some(lhs) = lhs {
self.resolve_expression(lhs)?;
}
self.resolve_expression(rhs)
}
Expression::Conditional { Expression::Conditional {
lhs, lhs,
rhs, rhs,

View File

@ -176,9 +176,11 @@ impl<'src, 'run> Evaluator<'src, 'run> {
} }
} }
Expression::Group { contents } => self.evaluate_expression(contents), Expression::Group { contents } => self.evaluate_expression(contents),
Expression::Join { lhs, rhs } => { Expression::Join { lhs: None, rhs } => Ok("/".to_string() + &self.evaluate_expression(rhs)?),
Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?) Expression::Join {
} lhs: Some(lhs),
rhs,
} => Ok(self.evaluate_expression(lhs)? + "/" + &self.evaluate_expression(rhs)?),
} }
} }

View File

@ -32,7 +32,7 @@ pub(crate) enum Expression<'src> {
Group { contents: Box<Expression<'src>> }, Group { contents: Box<Expression<'src>> },
/// `lhs / rhs` /// `lhs / rhs`
Join { Join {
lhs: Box<Expression<'src>>, lhs: Option<Box<Expression<'src>>>,
rhs: Box<Expression<'src>>, rhs: Box<Expression<'src>>,
}, },
/// `"string_literal"` or `'string_literal'` /// `"string_literal"` or `'string_literal'`
@ -51,7 +51,11 @@ impl<'src> Display for Expression<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
match self { match self {
Expression::Backtick { token, .. } => write!(f, "{}", token.lexeme()), 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::Concatenation { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
Expression::Conditional { Expression::Conditional {
lhs, lhs,

View File

@ -118,7 +118,11 @@ impl<'src> Node<'src> for Expression<'src> {
} => Tree::string(cooked), } => Tree::string(cooked),
Expression::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)), Expression::Backtick { contents, .. } => Tree::atom("backtick").push(Tree::string(contents)),
Expression::Group { contents } => Tree::List(vec![contents.tree()]), 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()),
} }
} }
} }

View File

@ -406,11 +406,15 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
let expression = if self.accepted_keyword(Keyword::If)? { let expression = if self.accepted_keyword(Keyword::If)? {
self.parse_conditional()? self.parse_conditional()?
} else if self.accepted(Slash)? {
let lhs = None;
let rhs = Box::new(self.parse_expression()?);
Expression::Join { lhs, rhs }
} else { } else {
let value = self.parse_value()?; let value = self.parse_value()?;
if self.accepted(Slash)? { if self.accepted(Slash)? {
let lhs = Box::new(value); let lhs = Some(Box::new(value));
let rhs = Box::new(self.parse_expression()?); let rhs = Box::new(self.parse_expression()?);
Expression::Join { lhs, rhs } Expression::Join { lhs, rhs }
} else if self.accepted(Plus)? { } else if self.accepted(Plus)? {
@ -1991,6 +1995,7 @@ mod tests {
Identifier, Identifier,
ParenL, ParenL,
ParenR, ParenR,
Slash,
StringToken, StringToken,
], ],
found: Eof, found: Eof,
@ -2010,6 +2015,7 @@ mod tests {
Identifier, Identifier,
ParenL, ParenL,
ParenR, ParenR,
Slash,
StringToken, StringToken,
], ],
found: InterpolationEnd, found: InterpolationEnd,

View File

@ -197,7 +197,7 @@ pub enum Expression {
operator: ConditionalOperator, operator: ConditionalOperator,
}, },
Join { Join {
lhs: Box<Expression>, lhs: Option<Box<Expression>>,
rhs: Box<Expression>, rhs: Box<Expression>,
}, },
String { String {
@ -258,7 +258,7 @@ impl Expression {
rhs: Box::new(Expression::new(rhs)), rhs: Box::new(Expression::new(rhs)),
}, },
Join { lhs, rhs } => Expression::Join { 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)), rhs: Box::new(Expression::new(rhs)),
}, },
Conditional { Conditional {

View File

@ -53,10 +53,16 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
self.stack.push(lhs); self.stack.push(lhs);
} }
Expression::Variable { name, .. } => return Some(name.token()), 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(rhs);
self.stack.push(lhs); self.stack.push(lhs);
} }
Expression::Join { lhs, rhs } => {
self.stack.push(rhs);
if let Some(lhs) = lhs {
self.stack.push(lhs);
}
}
Expression::Group { contents } => { Expression::Group { contents } => {
self.stack.push(contents); self.stack.push(contents);
} }

View File

@ -18,6 +18,45 @@ fn twice() {
.run(); .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] #[test]
fn default_un_parenthesized() { fn default_un_parenthesized() {
Test::new() Test::new()
@ -39,6 +78,27 @@ fn default_un_parenthesized() {
.run(); .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] #[test]
fn default_parenthesized() { fn default_parenthesized() {
Test::new() Test::new()
@ -52,3 +112,17 @@ fn default_parenthesized() {
.stdout("a/b\n") .stdout("a/b\n")
.run(); .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();
}