Add / operator (#1237)

This commit is contained in:
Casey Rodarmor 2022-06-25 02:39:06 -07:00 committed by GitHub
parent 9c719abf91
commit a46be41699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 127 additions and 13 deletions

View File

@ -66,6 +66,7 @@ setting : 'set' 'dotenv-load' boolean?
boolean : ':=' ('true' | 'false') boolean : ':=' ('true' | 'false')
expression : 'if' condition '{' expression '}' 'else' '{' expression '}' expression : 'if' condition '{' expression '}' 'else' '{' expression '}'
| value '/' expression
| value '+' expression | value '+' expression
| value | value

View File

@ -803,11 +803,12 @@ Starting server with database localhost:6379 on port 1337…
### Variables and Substitution ### Variables and Substitution
Variables, strings, concatenation, and substitution using `{{…}}` are supported: Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported:
```make ```make
tmpdir := `mktemp`
version := "0.2.7" version := "0.2.7"
tardir := "awesomesauce-" + version tardir := tmpdir / "awesomesauce-" + version
tarball := tardir + ".tar.gz" tarball := tardir + ".tar.gz"
publish: publish:
@ -819,6 +820,33 @@ publish:
rm -rf {{tarball}} {{tardir}} rm -rf {{tarball}} {{tardir}}
``` ```
#### Joining Paths
The `/` operator can be used to join two strings with a slash:
```make
foo := "a" / "b"
```
```
$ just --evaluate foo
a/b
```
Note that a `/` is added even if one is already present:
```make
foo := "a/"
bar := foo / "b"
```
```
$ just --evaluate bar
a//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 `{{` #### Escaping `{{`
To write a recipe containing `{{`, use `{{{{`: To write a recipe containing `{{`, use `{{{{`:
@ -1064,7 +1092,7 @@ These functions can fail, for example if a path does not have an extension, whic
##### Infallible ##### Infallible
- `join(a, b…)` - Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments. - `join(a, b…)` - *This function uses `/` on Unix and `\` on Windows, which can be lead to unwanted behavior. The `/` operator, e.g., `a / b`, which always uses `/`, should be considered as a replacement unless `\`s are specifically desired on Windows.* Join path `a` with path `b`. `join("foo/bar", "baz")` is `foo/bar/baz`. Accepts two or more arguments.
- `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`. - `clean(path)` - Simplify `path` by removing extra path separators, intermediate `.` components, and `..` where possible. `clean("foo//bar")` is `foo/bar`, `clean("foo/..")` is `.`, `clean("foo/./bar")` is `foo/bar`.
@ -1360,12 +1388,12 @@ Testing server:unit…
./test --tests unit server ./test --tests unit server
``` ```
Default values may be arbitrary expressions, but concatenations must be parenthesized: Default values may be arbitrary expressions, but concatenations or path joins must be parenthesized:
```make ```make
arch := "wasm" arch := "wasm"
test triple=(arch + "-unknown-unknown"): test triple=(arch + "-unknown-unknown") input=(arch / "input.dat"):
./test {{triple}} ./test {{triple}}
``` ```

View File

@ -58,9 +58,6 @@ check: fmt clippy test forbid
git diff --no-ext-diff --quiet --exit-code git diff --no-ext-diff --quiet --exit-code
VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1` VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1`
grep "^\[$VERSION\]" CHANGELOG.md grep "^\[$VERSION\]" CHANGELOG.md
cargo +nightly generate-lockfile -Z minimal-versions
cargo test
git checkout Cargo.lock
# publish current GitHub master branch # publish current GitHub master branch
publish: publish:

View File

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

View File

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

View File

@ -30,6 +30,11 @@ pub(crate) enum Expression<'src> {
}, },
/// `(contents)` /// `(contents)`
Group { contents: Box<Expression<'src>> }, Group { contents: Box<Expression<'src>> },
/// `lhs / rhs`
Join {
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`
@ -46,6 +51,7 @@ 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::Concatenation { lhs, rhs } => write!(f, "{} + {}", lhs, rhs), Expression::Concatenation { lhs, rhs } => write!(f, "{} + {}", lhs, rhs),
Expression::Conditional { Expression::Conditional {
lhs, lhs,
@ -86,6 +92,13 @@ 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 {
lhs, lhs,
rhs, rhs,

View File

@ -485,6 +485,7 @@ impl<'src> Lexer<'src> {
'*' => self.lex_single(Asterisk), '*' => self.lex_single(Asterisk),
'+' => self.lex_single(Plus), '+' => self.lex_single(Plus),
',' => self.lex_single(Comma), ',' => self.lex_single(Comma),
'/' => self.lex_single(Slash),
':' => self.lex_colon(), ':' => self.lex_colon(),
'=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals), '=' => self.lex_choices('=', &[('=', EqualsEquals), ('~', EqualsTilde)], Equals),
'@' => self.lex_single(At), '@' => self.lex_single(At),
@ -942,6 +943,7 @@ mod tests {
ParenL => "(", ParenL => "(",
ParenR => ")", ParenR => ")",
Plus => "+", Plus => "+",
Slash => "/",
Whitespace => " ", Whitespace => " ",
// Empty lexemes // Empty lexemes

View File

@ -118,6 +118,7 @@ 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()),
} }
} }
} }

View File

@ -408,7 +408,11 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
} else { } else {
let value = self.parse_value()?; let value = self.parse_value()?;
if self.accepted(Plus)? { if self.accepted(Slash)? {
let lhs = Box::new(value);
let rhs = Box::new(self.parse_expression()?);
Expression::Join { lhs, rhs }
} else if self.accepted(Plus)? {
let lhs = Box::new(value); let lhs = Box::new(value);
let rhs = Box::new(self.parse_expression()?); let rhs = Box::new(self.parse_expression()?);
Expression::Concatenation { lhs, rhs } Expression::Concatenation { lhs, rhs }

View File

@ -196,6 +196,10 @@ pub enum Expression {
otherwise: Box<Expression>, otherwise: Box<Expression>,
operator: ConditionalOperator, operator: ConditionalOperator,
}, },
Join {
lhs: Box<Expression>,
rhs: Box<Expression>,
},
String { String {
text: String, text: String,
}, },
@ -253,6 +257,10 @@ impl Expression {
lhs: Box::new(Expression::new(lhs)), lhs: Box::new(Expression::new(lhs)),
rhs: Box::new(Expression::new(rhs)), rhs: Box::new(Expression::new(rhs)),
}, },
Join { lhs, rhs } => Expression::Join {
lhs: Box::new(Expression::new(lhs)),
rhs: Box::new(Expression::new(rhs)),
},
Conditional { Conditional {
lhs, lhs,
operator, operator,

View File

@ -30,6 +30,7 @@ pub(crate) enum TokenKind {
ParenL, ParenL,
ParenR, ParenR,
Plus, Plus,
Slash,
StringToken, StringToken,
Text, Text,
Unspecified, Unspecified,
@ -71,6 +72,7 @@ impl Display for TokenKind {
ParenL => "'('", ParenL => "'('",
ParenR => "')'", ParenR => "')'",
Plus => "'+'", Plus => "'+'",
Slash => "'/'",
StringToken => "string", StringToken => "string",
Text => "command text", Text => "command text",
Unspecified => "unspecified", Unspecified => "unspecified",

View File

@ -53,7 +53,7 @@ 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::Concatenation { lhs, rhs } | Expression::Join { lhs, rhs } => {
self.stack.push(rhs); self.stack.push(rhs);
self.stack.push(lhs); self.stack.push(lhs);
} }

View File

@ -132,7 +132,7 @@ test! {
", ",
stdout: "", stdout: "",
stderr: " stderr: "
error: Expected '!=', '==', '=~', or '+', but found identifier error: Expected '!=', '==', '=~', '+', or '/', but found identifier
| |
1 | a := if '' a '' { '' } else { b } 1 | a := if '' a '' { '' } else { b }
| ^ | ^

View File

@ -43,7 +43,7 @@ test! {
} }
test! { test! {
name: evaluate_single, name: evaluate_single_free,
justfile: " justfile: "
a := 'x' a := 'x'
b := 'y' b := 'y'

View File

@ -69,6 +69,7 @@ mod search;
mod shebang; mod shebang;
mod shell; mod shell;
mod show; mod show;
mod slash_operator;
mod string; mod string;
mod sublime_syntax; mod sublime_syntax;
mod subsequents; mod subsequents;

54
tests/slash_operator.rs Normal file
View File

@ -0,0 +1,54 @@
use super::*;
#[test]
fn once() {
Test::new()
.justfile("x := 'a' / 'b'")
.args(&["--evaluate", "x"])
.stdout("a/b")
.run();
}
#[test]
fn twice() {
Test::new()
.justfile("x := 'a' / 'b' / 'c'")
.args(&["--evaluate", "x"])
.stdout("a/b/c")
.run();
}
#[test]
fn default_un_parenthesized() {
Test::new()
.justfile(
"
foo x='a' / 'b':
echo {{x}}
",
)
.stderr(
"
error: Expected '*', ':', '$', identifier, or '+', but found '/'
|
1 | foo x='a' / 'b':
| ^
",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn default_parenthesized() {
Test::new()
.justfile(
"
foo x=('a' / 'b'):
echo {{x}}
",
)
.stderr("echo a/b\n")
.stdout("a/b\n")
.run();
}