mirror of
https://github.com/casey/just.git
synced 2024-11-22 10:26:26 +03:00
Compare commits
11 Commits
5bc728d472
...
bbc9d0cfde
Author | SHA1 | Date | |
---|---|---|---|
|
bbc9d0cfde | ||
|
225ba343f4 | ||
|
67034cb8b4 | ||
|
8cdff483bf | ||
|
7030e9cac6 | ||
|
4c6368ecfc | ||
|
528c9f0e3c | ||
|
a71f2a53be | ||
|
b063940b31 | ||
|
4683a63adc | ||
|
28c4e9a13c |
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@ -110,7 +110,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Publish Archive
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.9
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
with:
|
||||
draft: false
|
||||
@ -120,7 +120,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish Changelog
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.9
|
||||
if: >-
|
||||
${{
|
||||
startsWith(github.ref, 'refs/tags/')
|
||||
@ -157,7 +157,7 @@ jobs:
|
||||
shasum -a 256 * > ../SHA256SUMS
|
||||
|
||||
- name: Publish Checksums
|
||||
uses: softprops/action-gh-release@v2.0.8
|
||||
uses: softprops/action-gh-release@v2.0.9
|
||||
with:
|
||||
draft: false
|
||||
files: SHA256SUMS
|
||||
|
@ -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
|
||||
|
143
README.md
143
README.md
@ -161,7 +161,7 @@ most Windows users.)
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href=https://nixos.org/nix/>Nix</a></td>
|
||||
<td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/just/default.nix>just</a></td>
|
||||
<td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix>just</a></td>
|
||||
<td><code>nix-env -iA nixpkgs.just</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -268,7 +268,7 @@ most Windows users.)
|
||||
<tr>
|
||||
<td><a href=https://nixos.org/nixos/>NixOS</a></td>
|
||||
<td><a href=https://nixos.org/nix/>Nix</a></td>
|
||||
<td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/just/default.nix>just</a></td>
|
||||
<td><a href=https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix>just</a></td>
|
||||
<td><code>nix-env -iA nixos.just</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -1290,26 +1290,61 @@ Available recipes:
|
||||
test
|
||||
```
|
||||
|
||||
### Variables and Substitution
|
||||
### Expressions and Substitutions
|
||||
|
||||
Variables, strings, concatenation, path joining, and substitution using `{{…}}`
|
||||
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`
|
||||
version := "0.2.7"
|
||||
tardir := tmpdir / "awesomesauce-" + version
|
||||
tarball := tardir + ".tar.gz"
|
||||
config := quote(config_dir() / ".project-config")
|
||||
|
||||
publish:
|
||||
rm -f {{tarball}}
|
||||
mkdir {{tardir}}
|
||||
cp README.md *.c {{tardir}}
|
||||
cp README.md *.c {{ config }} {{tardir}}
|
||||
tar zcvf {{tarball}} {{tardir}}
|
||||
scp {{tarball}} me@server.com:release/
|
||||
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
|
||||
values<sup>master</sup>, 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:
|
||||
|
||||
```justfile
|
||||
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:
|
||||
|
||||
```justfile
|
||||
foo := '' || 'goodbye' # 'goodbye'
|
||||
bar := 'hello' || 'goodbye' # 'hello'
|
||||
```
|
||||
|
||||
#### Joining Paths
|
||||
|
||||
The `/` operator can be used to join two strings with a slash:
|
||||
@ -1497,8 +1532,8 @@ Done!
|
||||
|
||||
### Functions
|
||||
|
||||
`just` provides a few built-in functions that might be useful when writing
|
||||
recipes.
|
||||
`just` provides many built-in functions for use in expressions, including
|
||||
recipe body `{{…}}` substitutions, assignments, and default parameter values.
|
||||
|
||||
All functions ending in `_directory` can be abbreviated to `_dir`. So
|
||||
`home_directory()` can also be written as `home_dir()`. In addition,
|
||||
@ -2075,6 +2110,10 @@ See the [Strings](#strings) section for details on unindenting.
|
||||
Backticks may not start with `#!`. This syntax is reserved for a future
|
||||
upgrade.
|
||||
|
||||
The [`shell(…)` function](#external-commands) provides a more general mechanism
|
||||
to invoke external commands, including the ability to execute the contents of a
|
||||
variable as a command, and to pass arguments to a command.
|
||||
|
||||
### Conditional Expressions
|
||||
|
||||
`if`/`else` expressions evaluate different branches depending on if two
|
||||
@ -2379,8 +2418,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"
|
||||
@ -2752,7 +2791,7 @@ pass a Windows-style path to the interpreter.
|
||||
Recipe lines are interpreted by the shell, not `just`, so it's not possible to
|
||||
set `just` variables in the middle of a recipe:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
foo:
|
||||
x := "hello" # This doesn't work!
|
||||
echo {{x}}
|
||||
@ -2884,7 +2923,7 @@ means that multi-line constructs probably won't do what you want.
|
||||
|
||||
For example, with the following `justfile`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
conditional:
|
||||
if true; then
|
||||
echo 'True!'
|
||||
@ -3291,7 +3330,7 @@ One `justfile` can include the contents of another using `import` statements.
|
||||
|
||||
If you have the following `justfile`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
import 'foo/bar.just'
|
||||
|
||||
a: b
|
||||
@ -3331,11 +3370,37 @@ set, variables in parent modules override variables in imports.
|
||||
|
||||
Imports may be made optional by putting a `?` after the `import` keyword:
|
||||
|
||||
```mf
|
||||
```just
|
||||
import? 'foo/bar.just'
|
||||
```
|
||||
|
||||
Missing source files for optional imports do not produce an error.
|
||||
Importing the same source file multiple times is not an error<sup>master</sup>.
|
||||
This allows importing multiple justfiles, for example `foo.just` and
|
||||
`bar.just`, which both import a third justfile containing shared recipes, for
|
||||
example `baz.just`, without the duplicate import of `baz.just` being an error:
|
||||
|
||||
```justfile
|
||||
# justfile
|
||||
import 'foo.just'
|
||||
import 'bar.just'
|
||||
```
|
||||
|
||||
```justfile
|
||||
# foo.just
|
||||
import 'baz.just'
|
||||
foo: baz
|
||||
```
|
||||
|
||||
```justfile
|
||||
# bar.just
|
||||
import 'baz.just'
|
||||
bar: baz
|
||||
```
|
||||
|
||||
```just
|
||||
# baz
|
||||
baz:
|
||||
```
|
||||
|
||||
### Modules<sup>1.19.0</sup>
|
||||
|
||||
@ -3347,7 +3412,7 @@ versions, you'll need to use the `--unstable` flag, `set unstable`, or set the
|
||||
|
||||
If you have the following `justfile`:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
mod bar
|
||||
|
||||
a:
|
||||
@ -3385,7 +3450,7 @@ the module file may have any capitalization.
|
||||
|
||||
Module statements may be of the form:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
mod foo 'PATH'
|
||||
```
|
||||
|
||||
@ -3409,7 +3474,7 @@ recipes.
|
||||
|
||||
Modules may be made optional by putting a `?` after the `mod` keyword:
|
||||
|
||||
```mf
|
||||
```just
|
||||
mod? foo
|
||||
```
|
||||
|
||||
@ -3419,7 +3484,7 @@ Optional modules with no source file do not conflict, so you can have multiple
|
||||
mod statements with the same name, but with different source file paths, as
|
||||
long as at most one source file exists:
|
||||
|
||||
```mf
|
||||
```just
|
||||
mod? foo 'bar.just'
|
||||
mod? foo 'baz.just'
|
||||
```
|
||||
@ -3427,7 +3492,7 @@ mod? foo 'baz.just'
|
||||
Modules may be given doc comments which appear in `--list`
|
||||
output<sup>1.30.0</sup>:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# foo is a great module!
|
||||
mod foo
|
||||
```
|
||||
@ -3569,9 +3634,9 @@ The following command will create two files, `some` and `argument.txt`:
|
||||
$ just foo "some argument.txt"
|
||||
```
|
||||
|
||||
The users shell will parse `"some argument.txt"` as a single argument, but when
|
||||
`just` replaces `touch {{argument}}` with `touch some argument.txt`, the quotes
|
||||
are not preserved, and `touch` will receive two arguments.
|
||||
The user's shell will parse `"some argument.txt"` as a single argument, but
|
||||
when `just` replaces `touch {{argument}}` with `touch some argument.txt`, the
|
||||
quotes are not preserved, and `touch` will receive two arguments.
|
||||
|
||||
There are a few ways to avoid this: quoting, positional arguments, and exported
|
||||
arguments.
|
||||
@ -3896,6 +3961,38 @@ fetch:
|
||||
Given the above `justfile`, after running `just fetch`, the recipes in
|
||||
`foo.just` will be available.
|
||||
|
||||
### Printing Complex Strings
|
||||
|
||||
`echo` can be used to print strings, but because it processes escape sequences,
|
||||
like `\n`, and different implementations of `echo` recognize different escape
|
||||
sequences, using `printf` is often a better choice.
|
||||
|
||||
`printf` takes a C-style format string and any number of arguments, which are
|
||||
interpolated into the format string.
|
||||
|
||||
This can be combined with indented, triple quoted strings to emulate shell
|
||||
heredocs.
|
||||
|
||||
Substitution complex strings into recipe bodies with `{…}` can also lead to
|
||||
trouble as it may be split by the shell into multiple arguments depending on
|
||||
the presence of whitespace and quotes. Exporting complex strings as environment
|
||||
variables and referring to them with `"$NAME"`, note the double quotes, can
|
||||
also help.
|
||||
|
||||
Putting all this together, to print a string verbatim to standard output, with
|
||||
all its various escape sequences and quotes undisturbed:
|
||||
|
||||
```just
|
||||
export FOO := '''
|
||||
a complicated string with
|
||||
some dis\tur\bi\ng escape sequences
|
||||
and "quotes" of 'different' kinds
|
||||
'''
|
||||
|
||||
bar:
|
||||
printf %s "$FOO"
|
||||
```
|
||||
|
||||
### Alternatives and Prior Art
|
||||
|
||||
There is no shortage of command runners! Some more or less similar alternatives
|
||||
|
@ -144,13 +144,13 @@ list:
|
||||
<tr>
|
||||
<td><a href="https://nixos.org/download.html#download-nix">Various</a></td>
|
||||
<td><a href="https://nixos.org/nix/">Nix</a></td>
|
||||
<td><a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/just/default.nix">just</a></td>
|
||||
<td><a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix">just</a></td>
|
||||
<td><code>nix-env -iA nixpkgs.just</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://nixos.org/nixos/">NixOS</a></td>
|
||||
<td><a href="https://nixos.org/nix/">Nix</a></td>
|
||||
<td><a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/just/default.nix">just</a></td>
|
||||
<td><a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix">just</a></td>
|
||||
<td><code>nix-env -iA nixos.just</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -35,12 +35,16 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
||||
root: &Path,
|
||||
) -> 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) => {
|
||||
@ -54,9 +58,11 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
||||
Item::Comment(_) => (),
|
||||
Item::Import { absolute, .. } => {
|
||||
if let Some(absolute) = absolute {
|
||||
if imports.insert(absolute) {
|
||||
stack.push(asts.get(absolute).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
Item::Module {
|
||||
absolute,
|
||||
name,
|
||||
@ -163,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 {
|
||||
|
@ -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)]
|
||||
|
@ -6,6 +6,7 @@ use super::*;
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Ast<'src> {
|
||||
pub(crate) items: Vec<Item<'src>>,
|
||||
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
pub(crate) working_directory: PathBuf,
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ use super::*;
|
||||
/// A binding of `name` to `value`
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub(crate) struct Binding<'src, V = String> {
|
||||
#[serde(skip)]
|
||||
pub(crate) constant: bool,
|
||||
pub(crate) export: bool,
|
||||
#[serde(skip)]
|
||||
pub(crate) file_depth: u32,
|
||||
|
@ -39,7 +39,7 @@ impl CommandExt for Command {
|
||||
}
|
||||
|
||||
for binding in scope.bindings() {
|
||||
if settings.export || binding.export {
|
||||
if binding.export || (settings.export && !binding.constant) {
|
||||
self.env(binding.name.lexeme(), &binding.value);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ impl Compiler {
|
||||
let tokens = Lexer::lex(relative, src)?;
|
||||
let mut ast = Parser::parse(
|
||||
current.file_depth,
|
||||
¤t.path,
|
||||
¤t.import_offsets,
|
||||
¤t.namepath,
|
||||
&tokens,
|
||||
@ -214,14 +213,7 @@ impl Compiler {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_compile(src: &str) -> CompileResult<Justfile> {
|
||||
let tokens = Lexer::test_lex(src)?;
|
||||
let ast = Parser::parse(
|
||||
0,
|
||||
&PathBuf::new(),
|
||||
&[],
|
||||
&Namepath::default(),
|
||||
&tokens,
|
||||
&PathBuf::new(),
|
||||
)?;
|
||||
let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())?;
|
||||
let root = PathBuf::from("justfile");
|
||||
let mut asts: HashMap<PathBuf, Ast> = HashMap::new();
|
||||
asts.insert(root.clone(), ast);
|
||||
|
102
src/evaluator.rs
102
src/evaluator.rs
@ -32,12 +32,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
|
||||
for (name, value) in overrides {
|
||||
if let Some(assignment) = module.assignments.get(name) {
|
||||
scope.bind(
|
||||
assignment.export,
|
||||
assignment.name,
|
||||
assignment.private,
|
||||
value.clone(),
|
||||
);
|
||||
scope.bind(Binding {
|
||||
constant: false,
|
||||
export: assignment.export,
|
||||
file_depth: 0,
|
||||
name: assignment.name,
|
||||
private: assignment.private,
|
||||
value: value.clone(),
|
||||
});
|
||||
} else {
|
||||
unknown_overrides.push(name.clone());
|
||||
}
|
||||
@ -68,12 +70,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
|
||||
if !self.scope.bound(name) {
|
||||
let value = self.evaluate_expression(&assignment.value)?;
|
||||
self.scope.bind(
|
||||
assignment.export,
|
||||
assignment.name,
|
||||
assignment.private,
|
||||
self.scope.bind(Binding {
|
||||
constant: false,
|
||||
export: assignment.export,
|
||||
file_depth: 0,
|
||||
name: assignment.name,
|
||||
private: assignment.private,
|
||||
value,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(self.scope.value(name).unwrap())
|
||||
@ -84,24 +88,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 +129,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 +185,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 +210,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}`"),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -329,9 +344,14 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
rest = &rest[1..];
|
||||
value
|
||||
};
|
||||
evaluator
|
||||
.scope
|
||||
.bind(parameter.export, parameter.name, false, value);
|
||||
evaluator.scope.bind(Binding {
|
||||
constant: false,
|
||||
export: parameter.export,
|
||||
file_depth: 0,
|
||||
name: parameter.name,
|
||||
private: false,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((evaluator.scope, positional))
|
||||
|
@ -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<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `assert(condition, error)`
|
||||
Assert {
|
||||
condition: Condition<'src>,
|
||||
@ -38,6 +43,11 @@ pub(crate) enum Expression<'src> {
|
||||
lhs: Option<Box<Expression<'src>>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `lhs || rhs`
|
||||
Or {
|
||||
lhs: Box<Expression<'src>>,
|
||||
rhs: Box<Expression<'src>>,
|
||||
},
|
||||
/// `"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: <S as Serializer>::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)?;
|
||||
|
@ -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 => "[",
|
||||
|
43
src/node.rs
43
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
110
src/parser.rs
110
src/parser.rs
@ -26,12 +26,12 @@ use {super::*, TokenKind::*};
|
||||
pub(crate) struct Parser<'run, 'src> {
|
||||
expected_tokens: BTreeSet<TokenKind>,
|
||||
file_depth: u32,
|
||||
file_path: &'run Path,
|
||||
import_offsets: Vec<usize>,
|
||||
module_namepath: &'run Namepath<'src>,
|
||||
next_token: usize,
|
||||
recursion_depth: usize,
|
||||
tokens: &'run [Token<'src>],
|
||||
unstable_features: BTreeSet<UnstableFeature>,
|
||||
working_directory: &'run Path,
|
||||
}
|
||||
|
||||
@ -39,7 +39,6 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
/// Parse `tokens` into an `Ast`
|
||||
pub(crate) fn parse(
|
||||
file_depth: u32,
|
||||
file_path: &'run Path,
|
||||
import_offsets: &[usize],
|
||||
module_namepath: &'run Namepath<'src>,
|
||||
tokens: &'run [Token<'src>],
|
||||
@ -48,12 +47,12 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
Self {
|
||||
expected_tokens: BTreeSet::new(),
|
||||
file_depth,
|
||||
file_path,
|
||||
import_offsets: import_offsets.to_vec(),
|
||||
module_namepath,
|
||||
next_token: 0,
|
||||
recursion_depth: 0,
|
||||
tokens,
|
||||
unstable_features: BTreeSet::new(),
|
||||
working_directory,
|
||||
}
|
||||
.parse_ast()
|
||||
@ -445,18 +444,19 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.next_token == self.tokens.len() {
|
||||
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(),
|
||||
})
|
||||
} else {
|
||||
Err(self.internal_error(format!(
|
||||
"Parse completed with {} unparsed tokens",
|
||||
self.tokens.len() - self.next_token,
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an alias, e.g `alias name := target`
|
||||
@ -500,11 +500,12 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}
|
||||
|
||||
Ok(Assignment {
|
||||
file_depth: self.file_depth,
|
||||
constant: false,
|
||||
export,
|
||||
file_depth: self.file_depth,
|
||||
name,
|
||||
value,
|
||||
private: private || name.lexeme().starts_with('_'),
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
@ -520,26 +521,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 expression = if self.accepted(BarBar)? {
|
||||
self
|
||||
.unstable_features
|
||||
.insert(UnstableFeature::LogicalOperators);
|
||||
let lhs = disjunct.into();
|
||||
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 }
|
||||
Expression::Or { lhs, rhs }
|
||||
} else {
|
||||
value
|
||||
}
|
||||
disjunct
|
||||
};
|
||||
|
||||
self.recursion_depth -= 1;
|
||||
@ -547,6 +539,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()?;
|
||||
@ -910,7 +943,6 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
dependencies,
|
||||
doc,
|
||||
file_depth: self.file_depth,
|
||||
file_path: self.file_path.into(),
|
||||
import_offsets: self.import_offsets.clone(),
|
||||
name,
|
||||
namepath: self.module_namepath.join(name),
|
||||
@ -1162,14 +1194,7 @@ mod tests {
|
||||
fn test(text: &str, want: Tree) {
|
||||
let unindented = unindent(text);
|
||||
let tokens = Lexer::test_lex(&unindented).expect("lexing failed");
|
||||
let justfile = Parser::parse(
|
||||
0,
|
||||
&PathBuf::new(),
|
||||
&[],
|
||||
&Namepath::default(),
|
||||
&tokens,
|
||||
&PathBuf::new(),
|
||||
)
|
||||
let justfile = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())
|
||||
.expect("parsing failed");
|
||||
let have = justfile.tree();
|
||||
if have != want {
|
||||
@ -1208,14 +1233,7 @@ mod tests {
|
||||
) {
|
||||
let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
|
||||
|
||||
match Parser::parse(
|
||||
0,
|
||||
&PathBuf::new(),
|
||||
&[],
|
||||
&Namepath::default(),
|
||||
&tokens,
|
||||
&PathBuf::new(),
|
||||
) {
|
||||
match Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new()) {
|
||||
Ok(_) => panic!("Parsing unexpectedly succeeded"),
|
||||
Err(have) => {
|
||||
let want = CompileError {
|
||||
|
@ -26,8 +26,6 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
|
||||
#[serde(skip)]
|
||||
pub(crate) file_depth: u32,
|
||||
#[serde(skip)]
|
||||
pub(crate) file_path: PathBuf,
|
||||
#[serde(skip)]
|
||||
pub(crate) import_offsets: Vec<usize>,
|
||||
pub(crate) name: Name<'src>,
|
||||
pub(crate) namepath: Namepath<'src>,
|
||||
|
24
src/scope.rs
24
src/scope.rs
@ -21,9 +21,11 @@ impl<'src, 'run> Scope<'src, 'run> {
|
||||
};
|
||||
|
||||
for (key, value) in constants() {
|
||||
root.bind(
|
||||
false,
|
||||
Name {
|
||||
root.bind(Binding {
|
||||
constant: true,
|
||||
export: false,
|
||||
file_depth: 0,
|
||||
name: Name {
|
||||
token: Token {
|
||||
column: 0,
|
||||
kind: TokenKind::Identifier,
|
||||
@ -34,22 +36,16 @@ impl<'src, 'run> Scope<'src, 'run> {
|
||||
src: key,
|
||||
},
|
||||
},
|
||||
false,
|
||||
(*value).into(),
|
||||
);
|
||||
private: false,
|
||||
value: (*value).into(),
|
||||
});
|
||||
}
|
||||
|
||||
root
|
||||
}
|
||||
|
||||
pub(crate) fn bind(&mut self, export: bool, name: Name<'src>, private: bool, value: String) {
|
||||
self.bindings.insert(Binding {
|
||||
export,
|
||||
file_depth: 0,
|
||||
name,
|
||||
private,
|
||||
value,
|
||||
});
|
||||
pub(crate) fn bind(&mut self, binding: Binding<'src>) {
|
||||
self.bindings.insert(binding);
|
||||
}
|
||||
|
||||
pub(crate) fn bound(&self, name: &str) -> bool {
|
||||
|
@ -183,6 +183,10 @@ impl Assignment {
|
||||
|
||||
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Debug, Clone)]
|
||||
pub enum Expression {
|
||||
And {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
Assert {
|
||||
condition: Condition,
|
||||
error: Box<Expression>,
|
||||
@ -209,6 +213,10 @@ pub enum Expression {
|
||||
lhs: Option<Box<Expression>>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
Or {
|
||||
lhs: Box<Expression>,
|
||||
rhs: Box<Expression>,
|
||||
},
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,14 +59,7 @@ pub(crate) fn analysis_error(
|
||||
) {
|
||||
let tokens = Lexer::test_lex(src).expect("Lexing failed in parse test...");
|
||||
|
||||
let ast = Parser::parse(
|
||||
0,
|
||||
&PathBuf::new(),
|
||||
&[],
|
||||
&Namepath::default(),
|
||||
&tokens,
|
||||
&PathBuf::new(),
|
||||
)
|
||||
let ast = Parser::parse(0, &[], &Namepath::default(), &tokens, &PathBuf::new())
|
||||
.expect("Parsing failed in analysis test...");
|
||||
|
||||
let root = PathBuf::from("justfile");
|
||||
|
@ -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 => "'['",
|
||||
|
@ -50,7 +50,6 @@ impl<'src> UnresolvedRecipe<'src> {
|
||||
dependencies,
|
||||
doc: self.doc,
|
||||
file_depth: self.file_depth,
|
||||
file_path: self.file_path,
|
||||
import_offsets: self.import_offsets,
|
||||
name: self.name,
|
||||
namepath: self.namepath,
|
||||
|
@ -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.")
|
||||
|
@ -16,7 +16,24 @@ impl<'expression, 'src> Iterator for Variables<'expression, 'src> {
|
||||
fn next(&mut self) -> Option<Token<'src>> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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 }
|
||||
|
@ -43,3 +43,19 @@ fn constants_can_be_redefined() {
|
||||
.stdout("foo")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constants_are_not_exported() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set export
|
||||
|
||||
foo:
|
||||
echo $HEXUPPER
|
||||
",
|
||||
)
|
||||
.stderr_regex(".*HEXUPPER: unbound variable.*")
|
||||
.status(127)
|
||||
.run();
|
||||
}
|
||||
|
@ -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 }}
|
||||
|
@ -360,3 +360,51 @@ fn reused_import_are_allowed() {
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiply_imported_items_do_not_conflict() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
import 'a.just'
|
||||
import 'a.just'
|
||||
foo: bar
|
||||
",
|
||||
)
|
||||
.write(
|
||||
"a.just",
|
||||
"
|
||||
x := 'y'
|
||||
|
||||
@bar:
|
||||
echo hello
|
||||
",
|
||||
)
|
||||
.stdout("hello\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_multiply_imported_items_do_not_conflict() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
import 'a.just'
|
||||
import 'b.just'
|
||||
foo: bar
|
||||
",
|
||||
)
|
||||
.write("a.just", "import 'c.just'")
|
||||
.write("b.just", "import 'c.just'")
|
||||
.write(
|
||||
"c.just",
|
||||
"
|
||||
x := 'y'
|
||||
|
||||
@bar:
|
||||
echo hello
|
||||
",
|
||||
)
|
||||
.stdout("hello\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ mod invocation_directory;
|
||||
mod json;
|
||||
mod line_prefixes;
|
||||
mod list;
|
||||
mod logical_operators;
|
||||
mod man;
|
||||
mod misc;
|
||||
mod modules;
|
||||
|
83
tests/logical_operators.rs
Normal file
83
tests/logical_operators.rs
Normal file
@ -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");
|
||||
}
|
@ -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'
|
||||
|
@ -1,4 +1,7 @@
|
||||
use {super::*, pretty_assertions::assert_eq};
|
||||
use {
|
||||
super::*,
|
||||
pretty_assertions::{assert_eq, StrComparison},
|
||||
};
|
||||
|
||||
macro_rules! test {
|
||||
{
|
||||
@ -205,6 +208,14 @@ impl Test {
|
||||
equal
|
||||
}
|
||||
|
||||
fn compare_string(name: &str, have: &str, want: &str) -> bool {
|
||||
let equal = have == want;
|
||||
if !equal {
|
||||
eprintln!("Bad {name}: {}", StrComparison::new(&have, &want));
|
||||
}
|
||||
equal
|
||||
}
|
||||
|
||||
if let Some(justfile) = &self.justfile {
|
||||
let justfile = unindent(justfile);
|
||||
fs::write(self.justfile_path(), justfile).unwrap();
|
||||
@ -266,8 +277,8 @@ impl Test {
|
||||
}
|
||||
|
||||
if !compare("status", output.status.code(), Some(self.status))
|
||||
| (self.stdout_regex.is_none() && !compare("stdout", output_stdout, &stdout))
|
||||
| (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr))
|
||||
| (self.stdout_regex.is_none() && !compare_string("stdout", output_stdout, &stdout))
|
||||
| (self.stderr_regex.is_none() && !compare_string("stderr", output_stderr, &stderr))
|
||||
{
|
||||
panic!("Output mismatch.");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user