Add /
operator (#1237)
This commit is contained in:
parent
9c719abf91
commit
a46be41699
@ -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
|
||||||
|
|
||||||
|
38
README.md
38
README.md
@ -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}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3
justfile
3
justfile
@ -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:
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
| ^
|
| ^
|
||||||
|
@ -43,7 +43,7 @@ test! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: evaluate_single,
|
name: evaluate_single_free,
|
||||||
justfile: "
|
justfile: "
|
||||||
a := 'x'
|
a := 'x'
|
||||||
b := 'y'
|
b := 'y'
|
||||||
|
@ -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
54
tests/slash_operator.rs
Normal 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();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user