mirror of
https://github.com/casey/just.git
synced 2024-11-21 14:52:13 +03:00
Merge branch 'master' into feat/working-directory-attribute
This commit is contained in:
commit
e3c9a0c199
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.1.0
|
||||
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.1.0
|
||||
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.1.0
|
||||
with:
|
||||
draft: false
|
||||
files: SHA256SUMS
|
||||
|
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -237,7 +237,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -329,14 +329,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
name = "derive-where"
|
||||
version = "1.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -531,7 +531,7 @@ dependencies = [
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"ctrlc",
|
||||
"derivative",
|
||||
"derive-where",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"edit-distance",
|
||||
@ -861,7 +861,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -930,7 +930,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -958,18 +958,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1038,7 +1027,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1142,7 +1131,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1164,7 +1153,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -1396,5 +1385,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
"syn",
|
||||
]
|
||||
|
@ -26,7 +26,7 @@ clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
|
||||
clap_complete = "4.0.0"
|
||||
clap_mangen = "0.2.20"
|
||||
ctrlc = { version = "3.1.1", features = ["termination"] }
|
||||
derivative = "2.0.0"
|
||||
derive-where = "1.2.7"
|
||||
dirs = "5.0.1"
|
||||
dotenvy = "0.15"
|
||||
edit-distance = "2.0.0"
|
||||
|
@ -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
|
||||
|
182
README.md
182
README.md
@ -167,7 +167,7 @@ most Windows users.)
|
||||
<tr>
|
||||
<td><a href=https://www.npmjs.com/>npm</a></td>
|
||||
<td><a href=https://www.npmjs.com/package/rust-just>rust-just</a></td>
|
||||
<td><code>npm install rust-just</code></td>
|
||||
<td><code>npm install -g rust-just</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href=https://pypi.org/>PyPI</a></td>
|
||||
@ -401,7 +401,7 @@ Using package managers pre-installed on GitHub Actions runners on MacOS with
|
||||
With [extractions/setup-just](https://github.com/extractions/setup-just):
|
||||
|
||||
```yaml
|
||||
- uses: extractions/setup-just@v1
|
||||
- uses: extractions/setup-just@v2
|
||||
with:
|
||||
just-version: 1.5.0 # optional semver specification, otherwise latest
|
||||
```
|
||||
@ -1290,9 +1290,11 @@ Available recipes:
|
||||
test
|
||||
```
|
||||
|
||||
### Variables and Substitution
|
||||
### Expressions and Substitutions
|
||||
|
||||
Variables, strings, concatenation, path joining, substitution using `{{…}}`, and function calls 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`
|
||||
@ -1310,6 +1312,39 @@ publish:
|
||||
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:
|
||||
@ -1543,11 +1578,11 @@ file.
|
||||
and can be changed with `set shell := […]`.
|
||||
|
||||
`command` is passed as the first argument, so if the command is `'echo $@'`,
|
||||
the full command line, with the default shell command `shell -cu` and `args`
|
||||
the full command line, with the default shell command `sh -cu` and `args`
|
||||
`'foo'` and `'bar'` will be:
|
||||
|
||||
```
|
||||
'shell' '-cu' 'echo $@' 'echo $@' 'foo' 'bar'
|
||||
'sh' '-cu' 'echo $@' 'echo $@' 'foo' 'bar'
|
||||
```
|
||||
|
||||
This is so that `$@` works as expected, and `$1` refers to the first
|
||||
@ -1816,6 +1851,24 @@ for details.
|
||||
`requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"`
|
||||
otherwise.
|
||||
|
||||
#### Style
|
||||
|
||||
- `style(name)`<sup>master</sup> - Return a named terminal display attribute
|
||||
escape sequence used by `just`. Unlike terminal display attribute escape
|
||||
sequence constants, which contain standard colors and styles, `style(name)`
|
||||
returns an escape sequence used by `just` itself, and can be used to make
|
||||
recipe output match `just`'s own output.
|
||||
|
||||
Recognized values for `name` are `'command'`, for echoed recipe lines,
|
||||
`error`, and `warning`.
|
||||
|
||||
For example, to style an error message:
|
||||
|
||||
```just
|
||||
scary:
|
||||
@echo '{{ style("error") }}OH NO{{ NORMAL }}'
|
||||
```
|
||||
|
||||
##### XDG Directories<sup>1.23.0</sup>
|
||||
|
||||
These functions return paths to user-specific directories for things like
|
||||
@ -1842,6 +1895,30 @@ A number of constants are predefined:
|
||||
| `HEX`<sup>1.27.0</sup> | `"0123456789abcdef"` |
|
||||
| `HEXLOWER`<sup>1.27.0</sup> | `"0123456789abcdef"` |
|
||||
| `HEXUPPER`<sup>1.27.0</sup> | `"0123456789ABCDEF"` |
|
||||
| `CLEAR`<sup>master</sup> | `"\ec"` |
|
||||
| `NORMAL`<sup>master</sup> | `"\e[0m"` |
|
||||
| `BOLD`<sup>master</sup> | `"\e[1m"` |
|
||||
| `ITALIC`<sup>master</sup> | `"\e[3m"` |
|
||||
| `UNDERLINE`<sup>master</sup> | `"\e[4m"` |
|
||||
| `INVERT`<sup>master</sup> | `"\e[7m"` |
|
||||
| `HIDE`<sup>master</sup> | `"\e[8m"` |
|
||||
| `STRIKETHROUGH`<sup>master</sup> | `"\e[9m"` |
|
||||
| `BLACK`<sup>master</sup> | `"\e[30m"` |
|
||||
| `RED`<sup>master</sup> | `"\e[31m"` |
|
||||
| `GREEN`<sup>master</sup> | `"\e[32m"` |
|
||||
| `YELLOW`<sup>master</sup> | `"\e[33m"` |
|
||||
| `BLUE`<sup>master</sup> | `"\e[34m"` |
|
||||
| `MAGENTA`<sup>master</sup> | `"\e[35m"` |
|
||||
| `CYAN`<sup>master</sup> | `"\e[36m"` |
|
||||
| `WHITE`<sup>master</sup> | `"\e[37m"` |
|
||||
| `BG_BLACK`<sup>master</sup> | `"\e[40m"` |
|
||||
| `BG_RED`<sup>master</sup> | `"\e[41m"` |
|
||||
| `BG_GREEN`<sup>master</sup> | `"\e[42m"` |
|
||||
| `BG_YELLOW`<sup>master</sup> | `"\e[43m"` |
|
||||
| `BG_BLUE`<sup>master</sup> | `"\e[44m"` |
|
||||
| `BG_MAGENTA`<sup>master</sup> | `"\e[45m"` |
|
||||
| `BG_CYAN`<sup>master</sup> | `"\e[46m"` |
|
||||
| `BG_WHITE`<sup>master</sup> | `"\e[47m"` |
|
||||
|
||||
```just
|
||||
@foo:
|
||||
@ -1853,9 +1930,29 @@ $ just foo
|
||||
0123456789abcdef
|
||||
```
|
||||
|
||||
Constants starting with `\e` are
|
||||
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code).
|
||||
|
||||
`CLEAR` clears the screen, similar to the `clear` command. The rest are of the
|
||||
form `\e[Nm`, where `N` is an integer, and set terminal display attributes.
|
||||
|
||||
Terminal display attribute escape sequences can be combined, for example text
|
||||
weight `BOLD`, text style `STRIKETHROUGH`, foreground color `CYAN`, and
|
||||
background color `BG_BLUE`. They should be followed by `NORMAL`, to reset the
|
||||
terminal back to normal.
|
||||
|
||||
Escape sequences should be quoted, since `[` is treated as a special character
|
||||
by some shells.
|
||||
|
||||
```just
|
||||
@foo:
|
||||
echo '{{BOLD + STRIKETHROUGH + CYAN + BG_BLUE}}Hi!{{NORMAL}}'
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Recipes, `mod` statements, and aliases may be annotated with attributes that change their behavior.
|
||||
Recipes, `mod` statements, and aliases may be annotated with attributes that
|
||||
change their behavior.
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
@ -2385,8 +2482,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"
|
||||
@ -2694,8 +2791,9 @@ scripts interpreted by `COMMAND`. This avoids some of the issues with shebang
|
||||
recipes, such as the use of `cygpath` on Windows, the need to use
|
||||
`/usr/bin/env`, and inconsistences in shebang line splitting across Unix OSs.
|
||||
|
||||
Recipes with an empty `[script]` attribute are executed with the value of
|
||||
`set script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`.
|
||||
Recipes with an empty `[script]` attribute are executed with the value of `set
|
||||
script-interpreter := […]`<sup>1.33.0</sup>, defaulting to `sh -eu`, and *not*
|
||||
the value of `set shell`.
|
||||
|
||||
The body of the recipe is evaluated, written to disk in the temporary
|
||||
directory, and run by passing its path as an argument to `COMMAND`.
|
||||
@ -2758,7 +2856,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}}
|
||||
@ -2890,7 +2988,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!'
|
||||
@ -3297,7 +3395,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
|
||||
@ -3337,7 +3435,7 @@ 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'
|
||||
```
|
||||
|
||||
@ -3346,19 +3444,19 @@ 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:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# justfile
|
||||
import 'foo.just'
|
||||
import 'bar.just'
|
||||
```
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# foo.just
|
||||
import 'baz.just'
|
||||
foo: baz
|
||||
```
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
# bar.just
|
||||
import 'baz.just'
|
||||
bar: baz
|
||||
@ -3379,7 +3477,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:
|
||||
@ -3417,7 +3515,7 @@ the module file may have any capitalization.
|
||||
|
||||
Module statements may be of the form:
|
||||
|
||||
```mf
|
||||
```justfile
|
||||
mod foo 'PATH'
|
||||
```
|
||||
|
||||
@ -3441,7 +3539,7 @@ recipes.
|
||||
|
||||
Modules may be made optional by putting a `?` after the `mod` keyword:
|
||||
|
||||
```mf
|
||||
```just
|
||||
mod? foo
|
||||
```
|
||||
|
||||
@ -3451,7 +3549,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'
|
||||
```
|
||||
@ -3459,7 +3557,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
|
||||
```
|
||||
@ -3601,9 +3699,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.
|
||||
@ -3928,6 +4026,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
|
||||
|
16
README.中文.md
16
README.中文.md
@ -220,6 +220,22 @@ list:
|
||||
<code>asdf install just <version></code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://packaging.python.org/tutorials/installing-packages">Various</a></td>
|
||||
<td><a href="https://pypi.org">PyPI</a></td>
|
||||
<td><a href="https://pypi.org/project/rust-just">rust-just</a></td>
|
||||
<td>
|
||||
<code>pipx install rust-just</code><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://docs.npmjs.com/packages-and-modules/getting-packages-from-the-registry">Various</a></td>
|
||||
<td><a href="https://www.npmjs.com">npm</a></td>
|
||||
<td><a href="https://www.npmjs.com/package/rust-just">rust-just</a></td>
|
||||
<td>
|
||||
<code>npm install -g rust-just</code><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://debian.org">Debian</a> and <a href="https://ubuntu.com">Ubuntu</a> derivatives</td>
|
||||
<td><a href="https://mpr.makedeb.org">MPR</a></td>
|
||||
|
6
justfile
6
justfile
@ -29,7 +29,7 @@ fuzz:
|
||||
run:
|
||||
cargo run
|
||||
|
||||
# only run tests matching PATTERN
|
||||
# only run tests matching `PATTERN`
|
||||
[group: 'test']
|
||||
filter PATTERN:
|
||||
cargo test {{PATTERN}}
|
||||
@ -169,6 +169,10 @@ build-book:
|
||||
mdbook build book/en
|
||||
mdbook build book/zh
|
||||
|
||||
[group: 'dev']
|
||||
print-readme-constants-table:
|
||||
cargo test constants::tests::readme_table -- --nocapture
|
||||
|
||||
# run all polyglot recipes
|
||||
[group: 'demo']
|
||||
polyglot: _python _js _perl _sh _ruby
|
||||
|
@ -36,12 +36,15 @@ impl<'run, 'src> Analyzer<'run, 'src> {
|
||||
) -> 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) => {
|
||||
@ -166,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,
|
||||
|
@ -35,7 +35,6 @@ impl Color {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn always() -> Self {
|
||||
Self {
|
||||
use_color: UseColor::Always,
|
||||
@ -66,6 +65,10 @@ impl Color {
|
||||
self.restyle(Style::new().fg(Blue))
|
||||
}
|
||||
|
||||
pub(crate) fn doc_backtick(self) -> Self {
|
||||
self.restyle(Style::new().fg(Cyan))
|
||||
}
|
||||
|
||||
pub(crate) fn error(self) -> Self {
|
||||
self.restyle(Style::new().fg(Red).bold())
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,58 @@
|
||||
use super::*;
|
||||
|
||||
pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> {
|
||||
static CONSTANTS: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
const CONSTANTS: [(&str, &str, &str); 27] = [
|
||||
("HEX", "0123456789abcdef", "1.27.0"),
|
||||
("HEXLOWER", "0123456789abcdef", "1.27.0"),
|
||||
("HEXUPPER", "0123456789ABCDEF", "1.27.0"),
|
||||
("CLEAR", "\x1bc", "master"),
|
||||
("NORMAL", "\x1b[0m", "master"),
|
||||
("BOLD", "\x1b[1m", "master"),
|
||||
("ITALIC", "\x1b[3m", "master"),
|
||||
("UNDERLINE", "\x1b[4m", "master"),
|
||||
("INVERT", "\x1b[7m", "master"),
|
||||
("HIDE", "\x1b[8m", "master"),
|
||||
("STRIKETHROUGH", "\x1b[9m", "master"),
|
||||
("BLACK", "\x1b[30m", "master"),
|
||||
("RED", "\x1b[31m", "master"),
|
||||
("GREEN", "\x1b[32m", "master"),
|
||||
("YELLOW", "\x1b[33m", "master"),
|
||||
("BLUE", "\x1b[34m", "master"),
|
||||
("MAGENTA", "\x1b[35m", "master"),
|
||||
("CYAN", "\x1b[36m", "master"),
|
||||
("WHITE", "\x1b[37m", "master"),
|
||||
("BG_BLACK", "\x1b[40m", "master"),
|
||||
("BG_RED", "\x1b[41m", "master"),
|
||||
("BG_GREEN", "\x1b[42m", "master"),
|
||||
("BG_YELLOW", "\x1b[43m", "master"),
|
||||
("BG_BLUE", "\x1b[44m", "master"),
|
||||
("BG_MAGENTA", "\x1b[45m", "master"),
|
||||
("BG_CYAN", "\x1b[46m", "master"),
|
||||
("BG_WHITE", "\x1b[47m", "master"),
|
||||
];
|
||||
|
||||
CONSTANTS.get_or_init(|| {
|
||||
vec![
|
||||
("HEX", "0123456789abcdef"),
|
||||
("HEXLOWER", "0123456789abcdef"),
|
||||
("HEXUPPER", "0123456789ABCDEF"),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
pub(crate) fn constants() -> &'static HashMap<&'static str, &'static str> {
|
||||
static MAP: OnceLock<HashMap<&str, &str>> = OnceLock::new();
|
||||
MAP.get_or_init(|| {
|
||||
CONSTANTS
|
||||
.into_iter()
|
||||
.map(|(name, value, _version)| (name, value))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn readme_table() {
|
||||
println!("| Name | Value |");
|
||||
println!("|------|-------------|");
|
||||
for (name, value, version) in CONSTANTS {
|
||||
println!(
|
||||
"| `{name}`<sup>{version}</sup> | `\"{}\"` |",
|
||||
value.replace('\x1b', "\\e")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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)?;
|
||||
|
@ -98,6 +98,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
||||
"snakecase" => Unary(snakecase),
|
||||
"source_directory" => Nullary(source_directory),
|
||||
"source_file" => Nullary(source_file),
|
||||
"style" => Unary(style),
|
||||
"titlecase" => Unary(titlecase),
|
||||
"trim" => Unary(trim),
|
||||
"trim_end" => Unary(trim_end),
|
||||
@ -623,6 +624,20 @@ fn source_file(context: Context) -> FunctionResult {
|
||||
})
|
||||
}
|
||||
|
||||
fn style(context: Context, s: &str) -> FunctionResult {
|
||||
match s {
|
||||
"command" => Ok(
|
||||
Color::always()
|
||||
.command(context.evaluator.context.config.command_color)
|
||||
.prefix()
|
||||
.to_string(),
|
||||
),
|
||||
"error" => Ok(Color::always().error().prefix().to_string()),
|
||||
"warning" => Ok(Color::always().warning().prefix().to_string()),
|
||||
_ => Err(format!("unknown style: `{s}`")),
|
||||
}
|
||||
}
|
||||
|
||||
fn titlecase(_context: Context, s: &str) -> FunctionResult {
|
||||
Ok(s.to_title_case())
|
||||
}
|
||||
|
@ -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 => "[",
|
||||
|
@ -34,7 +34,7 @@ pub(crate) use {
|
||||
},
|
||||
camino::Utf8Path,
|
||||
clap::ValueEnum,
|
||||
derivative::Derivative,
|
||||
derive_where::derive_where,
|
||||
edit_distance::edit_distance,
|
||||
lexiclean::Lexiclean,
|
||||
libc::EXIT_FAILURE,
|
||||
|
45
src/node.rs
45
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -198,7 +197,7 @@ impl<'src> Node<'src> for UnresolvedRecipe<'src> {
|
||||
t.push_mut("quiet");
|
||||
}
|
||||
|
||||
if let Some(doc) = self.doc {
|
||||
if let Some(doc) = &self.doc {
|
||||
t.push_mut(Tree::string(doc));
|
||||
}
|
||||
|
||||
|
104
src/parser.rs
104
src/parser.rs
@ -31,6 +31,7 @@ pub(crate) struct Parser<'run, 'src> {
|
||||
next_token: usize,
|
||||
recursion_depth: usize,
|
||||
tokens: &'run [Token<'src>],
|
||||
unstable_features: BTreeSet<UnstableFeature>,
|
||||
working_directory: &'run Path,
|
||||
}
|
||||
|
||||
@ -51,6 +52,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
next_token: 0,
|
||||
recursion_depth: 0,
|
||||
tokens,
|
||||
unstable_features: BTreeSet::new(),
|
||||
working_directory,
|
||||
}
|
||||
.parse_ast()
|
||||
@ -442,18 +444,19 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}
|
||||
}
|
||||
|
||||
if self.next_token == self.tokens.len() {
|
||||
Ok(Ast {
|
||||
items,
|
||||
warnings: Vec::new(),
|
||||
working_directory: self.working_directory.into(),
|
||||
})
|
||||
} else {
|
||||
Err(self.internal_error(format!(
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse an alias, e.g `alias name := target`
|
||||
@ -497,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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -517,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 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 }
|
||||
} else {
|
||||
value
|
||||
}
|
||||
let expression = if self.accepted(BarBar)? {
|
||||
self
|
||||
.unstable_features
|
||||
.insert(UnstableFeature::LogicalOperators);
|
||||
let lhs = disjunct.into();
|
||||
let rhs = self.parse_expression()?.into();
|
||||
Expression::Or { lhs, rhs }
|
||||
} else {
|
||||
disjunct
|
||||
};
|
||||
|
||||
self.recursion_depth -= 1;
|
||||
@ -544,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()?;
|
||||
@ -900,6 +936,14 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
|
||||
let private = name.lexeme().starts_with('_') || attributes.contains(&Attribute::Private);
|
||||
|
||||
let mut doc = doc.map(ToOwned::to_owned);
|
||||
|
||||
for attribute in &attributes {
|
||||
if let Attribute::Doc(attribute_doc) = attribute {
|
||||
doc = attribute_doc.as_ref().map(|doc| doc.cooked.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Recipe {
|
||||
shebang: shebang || script,
|
||||
attributes,
|
||||
|
@ -22,7 +22,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
|
||||
pub(crate) attributes: BTreeSet<Attribute<'src>>,
|
||||
pub(crate) body: Vec<Line<'src>>,
|
||||
pub(crate) dependencies: Vec<D>,
|
||||
pub(crate) doc: Option<&'src str>,
|
||||
pub(crate) doc: Option<String>,
|
||||
#[serde(skip)]
|
||||
pub(crate) file_depth: u32,
|
||||
#[serde(skip)]
|
||||
@ -473,7 +473,8 @@ impl<'src, D> Recipe<'src, D> {
|
||||
return doc.as_ref().map(|s| s.cooked.as_ref());
|
||||
}
|
||||
}
|
||||
self.doc
|
||||
|
||||
self.doc.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn subsequents(&self) -> impl Iterator<Item = &D> {
|
||||
@ -483,8 +484,14 @@ impl<'src, D> Recipe<'src, D> {
|
||||
|
||||
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
||||
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||
if let Some(doc) = self.doc {
|
||||
writeln!(f, "# {doc}")?;
|
||||
if !self
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|attribute| matches!(attribute, Attribute::Doc(_)))
|
||||
{
|
||||
if let Some(doc) = &self.doc {
|
||||
writeln!(f, "# {doc}")?;
|
||||
}
|
||||
}
|
||||
|
||||
for attribute in &self.attributes {
|
||||
|
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 {
|
||||
|
@ -2,6 +2,11 @@ use {super::*, clap_mangen::Man};
|
||||
|
||||
const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n";
|
||||
|
||||
fn backtick_re() -> &'static Regex {
|
||||
static BACKTICK_RE: OnceLock<Regex> = OnceLock::new();
|
||||
BACKTICK_RE.get_or_init(|| Regex::new("(`.*?`)|(`[^`]*$)").unwrap())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub(crate) enum Subcommand {
|
||||
Changelog,
|
||||
@ -413,15 +418,31 @@ impl Subcommand {
|
||||
) {
|
||||
if let Some(doc) = doc {
|
||||
if !doc.is_empty() && doc.lines().count() <= 1 {
|
||||
let color = config.color.stdout();
|
||||
print!(
|
||||
"{:padding$}{} {}",
|
||||
"{:padding$}{} ",
|
||||
"",
|
||||
config.color.stdout().doc().paint("#"),
|
||||
config.color.stdout().doc().paint(doc),
|
||||
color.doc().paint("#"),
|
||||
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
|
||||
);
|
||||
|
||||
let mut end = 0;
|
||||
for backtick in backtick_re().find_iter(doc) {
|
||||
let prefix = &doc[end..backtick.start()];
|
||||
if !prefix.is_empty() {
|
||||
print!("{}", color.doc().paint(prefix));
|
||||
}
|
||||
print!("{}", color.doc_backtick().paint(backtick.as_str()));
|
||||
end = backtick.end();
|
||||
}
|
||||
|
||||
let suffix = &doc[end..];
|
||||
if !suffix.is_empty() {
|
||||
print!("{}", color.doc().paint(suffix));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
src/thunk.rs
18
src/thunk.rs
@ -1,46 +1,46 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug, Clone, PartialEq = "feature_allow_slow_enum")]
|
||||
#[derive_where(Debug, PartialEq)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Thunk<'src> {
|
||||
Nullary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context) -> FunctionResult,
|
||||
},
|
||||
Unary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str) -> FunctionResult,
|
||||
arg: Box<Expression<'src>>,
|
||||
},
|
||||
UnaryOpt {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str, Option<&str>) -> FunctionResult,
|
||||
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
|
||||
},
|
||||
UnaryPlus {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str, &[String]) -> FunctionResult,
|
||||
args: (Box<Expression<'src>>, Vec<Expression<'src>>),
|
||||
},
|
||||
Binary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str, &str) -> FunctionResult,
|
||||
args: [Box<Expression<'src>>; 2],
|
||||
},
|
||||
BinaryPlus {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str, &str, &[String]) -> FunctionResult,
|
||||
args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),
|
||||
},
|
||||
Ternary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
#[derive_where(skip(Debug, EqHashOrd))]
|
||||
function: fn(function::Context, &str, &str, &str) -> FunctionResult,
|
||||
args: [Box<Expression<'src>>; 3],
|
||||
},
|
||||
|
@ -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 => "'['",
|
||||
|
@ -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();
|
||||
}
|
||||
|
24
tests/fmt.rs
24
tests/fmt.rs
@ -1096,3 +1096,27 @@ fn multi_argument_attribute() {
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_attribute_suppresses_comment() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set unstable
|
||||
|
||||
# COMMENT
|
||||
[doc('ATTRIBUTE')]
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.arg("--dump")
|
||||
.stdout(
|
||||
"
|
||||
set unstable := true
|
||||
|
||||
[doc('ATTRIBUTE')]
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
@ -1183,3 +1183,78 @@ bar:
|
||||
.args(["foo", "bar"])
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_command_default() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
foo:
|
||||
@echo '{{ style("command") }}foo{{NORMAL}}'
|
||||
"#,
|
||||
)
|
||||
.stdout("\x1b[1mfoo\x1b[0m\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_command_non_default() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
foo:
|
||||
@echo '{{ style("command") }}foo{{NORMAL}}'
|
||||
"#,
|
||||
)
|
||||
.args(["--command-color", "red"])
|
||||
.stdout("\x1b[1;31mfoo\x1b[0m\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
foo:
|
||||
@echo '{{ style("error") }}foo{{NORMAL}}'
|
||||
"#,
|
||||
)
|
||||
.stdout("\x1b[1;31mfoo\x1b[0m\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_warning() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
foo:
|
||||
@echo '{{ style("warning") }}foo{{NORMAL}}'
|
||||
"#,
|
||||
)
|
||||
.stdout("\x1b[1;33mfoo\x1b[0m\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_unknown() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
foo:
|
||||
@echo '{{ style("hippo") }}foo{{NORMAL}}'
|
||||
"#,
|
||||
)
|
||||
.stderr(
|
||||
r#"
|
||||
error: Call to function `style` failed: unknown style: `hippo`
|
||||
——▶ justfile:2:13
|
||||
│
|
||||
2 │ @echo '{{ style("hippo") }}foo{{NORMAL}}'
|
||||
│ ^^^^^
|
||||
"#,
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.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 }}
|
||||
|
@ -1429,3 +1429,58 @@ fn recipes_with_private_attribute_are_private() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_attribute_overrides_comment() {
|
||||
case(
|
||||
"
|
||||
# COMMENT
|
||||
[doc('ATTRIBUTE')]
|
||||
foo:
|
||||
",
|
||||
json!({
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"groups": [],
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
"attributes": [{"doc": "ATTRIBUTE"}],
|
||||
"body": [],
|
||||
"dependencies": [],
|
||||
"doc": "ATTRIBUTE",
|
||||
"name": "foo",
|
||||
"namepath": "foo",
|
||||
"parameters": [],
|
||||
"priors": 0,
|
||||
"private": false,
|
||||
"quiet": false,
|
||||
"shebang": false,
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
"positional_arguments": false,
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
"working_directory" : null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -73,6 +73,7 @@ mod invocation_directory;
|
||||
mod json;
|
||||
mod line_prefixes;
|
||||
mod list;
|
||||
mod logical_operators;
|
||||
mod man;
|
||||
mod misc;
|
||||
mod modules;
|
||||
|
@ -438,3 +438,39 @@ fn no_space_before_submodules_not_following_groups() {
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backticks_highlighted() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
# Comment `` `with backticks` and trailing text
|
||||
recipe:
|
||||
",
|
||||
)
|
||||
.args(["--list", "--color=always"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m``\u{1b}[0m\u{1b}[34m \u{1b}[0m\u{1b}[36m`with backticks`\u{1b}[0m\u{1b}[34m and trailing text\u{1b}[0m
|
||||
")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_backticks() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
# Comment `with unclosed backick
|
||||
recipe:
|
||||
",
|
||||
)
|
||||
.args(["--list", "--color=always"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
recipe \u{1b}[34m#\u{1b}[0m \u{1b}[34mComment \u{1b}[0m\u{1b}[36m`with unclosed backick\u{1b}[0m
|
||||
")
|
||||
.run();
|
||||
}
|
||||
|
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'
|
||||
|
Loading…
Reference in New Issue
Block a user