mirror of
https://github.com/casey/just
synced 2024-07-01 07:24:45 +00:00
Compare commits
28 Commits
b8a86a400f
...
95d51ab933
Author | SHA1 | Date | |
---|---|---|---|
|
95d51ab933 | ||
|
2964a62542 | ||
|
570d3058cf | ||
|
c900b6f478 | ||
|
af86a471e2 | ||
|
e4564f45a3 | ||
|
aa43a664ee | ||
|
553adc1004 | ||
|
e572b93d84 | ||
|
fcac7ee768 | ||
|
71b72c4a53 | ||
|
0e8f660d6d | ||
|
1c3c1dd3c0 | ||
|
197e1002d0 | ||
|
4a59769faa | ||
|
bf6ec6bf16 | ||
|
1547af08b5 | ||
|
b05a75d168 | ||
|
5f91b37c82 | ||
|
dd9792571b | ||
|
e6c37aacd1 | ||
|
18ec9796b9 | ||
|
e1b17fe9cf | ||
|
4b5ba8f6f5 | ||
|
1ce7a05bef | ||
|
637023e86f | ||
|
4f16428bcb | ||
|
8778972014 |
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
|
@ -95,7 +95,7 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Publish Archive
|
- name: Publish Archive
|
||||||
uses: softprops/action-gh-release@v2.0.5
|
uses: softprops/action-gh-release@v2.0.6
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
|
@ -105,7 +105,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Publish Changelog
|
- name: Publish Changelog
|
||||||
uses: softprops/action-gh-release@v2.0.5
|
uses: softprops/action-gh-release@v2.0.6
|
||||||
if: >-
|
if: >-
|
||||||
${{
|
${{
|
||||||
startsWith(github.ref, 'refs/tags/')
|
startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
951
CHANGELOG.md
951
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
68
Cargo.lock
generated
68
Cargo.lock
generated
|
@ -174,9 +174,9 @@ checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.98"
|
version = "1.0.99"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
|
@ -221,9 +221,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.4"
|
version = "4.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
|
@ -231,9 +231,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.2"
|
version = "4.5.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
|
@ -244,18 +244,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_complete"
|
name = "clap_complete"
|
||||||
version = "4.5.2"
|
version = "4.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
|
checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.5.4",
|
"clap 4.5.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.5.4"
|
version = "4.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
|
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
@ -265,17 +265,17 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_lex"
|
name = "clap_lex"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_mangen"
|
name = "clap_mangen"
|
||||||
version = "0.2.20"
|
version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e"
|
checksum = "74b70fc13e60c0e1d490dc50eb73a749be6d81f4ef03783df1d9b7b0c62bc937"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap 4.5.4",
|
"clap 4.5.7",
|
||||||
"roff",
|
"roff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -306,15 +306,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cradle"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7096122c1023d53de7298f322590170540ad3eba46bbc2750b495f098c27c09a"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
|
@ -600,16 +591,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "just"
|
name = "just"
|
||||||
version = "1.28.0"
|
version = "1.29.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ansi_term",
|
"ansi_term",
|
||||||
"blake3",
|
"blake3",
|
||||||
"camino",
|
"camino",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap 4.5.4",
|
"clap 4.5.7",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
"cradle",
|
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"derivative",
|
"derivative",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
@ -685,9 +675,9 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.2"
|
version = "2.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memmap2"
|
name = "memmap2"
|
||||||
|
@ -898,13 +888,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.4"
|
version = "1.10.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.4.6",
|
"regex-automata 0.4.7",
|
||||||
"regex-syntax",
|
"regex-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -916,9 +906,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.6"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
@ -927,9 +917,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.3"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roff"
|
name = "roff"
|
||||||
|
@ -1244,9 +1234,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "just"
|
name = "just"
|
||||||
version = "1.28.0"
|
version = "1.29.1"
|
||||||
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
||||||
autotests = false
|
autotests = false
|
||||||
categories = ["command-line-utilities", "development-tools"]
|
categories = ["command-line-utilities", "development-tools"]
|
||||||
|
@ -54,7 +54,6 @@ unicode-width = "0.1.0"
|
||||||
uuid = { version = "1.0.0", features = ["v4"] }
|
uuid = { version = "1.0.0", features = ["v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
cradle = "0.2.0"
|
|
||||||
executable-path = "1.0.0"
|
executable-path = "1.0.0"
|
||||||
pretty_assertions = "1.0.0"
|
pretty_assertions = "1.0.0"
|
||||||
temptree = "0.2.0"
|
temptree = "0.2.0"
|
||||||
|
|
69
README.md
69
README.md
|
@ -656,8 +656,8 @@ Available recipes:
|
||||||
lint
|
lint
|
||||||
```
|
```
|
||||||
|
|
||||||
Recipes in submodules can be listed with `just --list PATH`, where `PATH` is a
|
Recipes in [submodules](#modules1190) can be listed with `just --list PATH`,
|
||||||
space- or `::`-separated module path:
|
where `PATH` is a space- or `::`-separated module path:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cat justfile
|
$ cat justfile
|
||||||
|
@ -996,6 +996,20 @@ $ just test foo "bar baz"
|
||||||
- bar baz
|
- bar baz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Positional arguments may also be turned on on a per-recipe basis with the
|
||||||
|
`[positional-arguments]` attribute<sup>1.29.0</sup>:
|
||||||
|
|
||||||
|
```just
|
||||||
|
[positional-arguments]
|
||||||
|
@foo bar:
|
||||||
|
echo $0
|
||||||
|
echo $1
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that PowerShell does not handle positional arguments in the same way as
|
||||||
|
other shells, so turning on positional arguments will likely break recipes that
|
||||||
|
use PowerShell.
|
||||||
|
|
||||||
#### Shell
|
#### Shell
|
||||||
|
|
||||||
The `shell` setting controls the command used to invoke recipe lines and
|
The `shell` setting controls the command used to invoke recipe lines and
|
||||||
|
@ -1300,6 +1314,7 @@ foobar := x'~/$FOO/${BAR}'
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `$VAR` | value of environment variable `VAR` |
|
| `$VAR` | value of environment variable `VAR` |
|
||||||
| `${VAR}` | value of environment variable `VAR` |
|
| `${VAR}` | value of environment variable `VAR` |
|
||||||
|
| `${VAR:-DEFAULT}` | value of environment variable `VAR`, or `DEFAULT` if `VAR` is not set |
|
||||||
| Leading `~` | path to current user's home directory |
|
| Leading `~` | path to current user's home directory |
|
||||||
| Leading `~USER` | path to `USER`'s home directory |
|
| Leading `~USER` | path to `USER`'s home directory |
|
||||||
|
|
||||||
|
@ -1627,6 +1642,16 @@ which will halt execution.
|
||||||
characters. For example, `choose('64', HEX)` will generate a random
|
characters. For example, `choose('64', HEX)` will generate a random
|
||||||
64-character lowercase hex string.
|
64-character lowercase hex string.
|
||||||
|
|
||||||
|
#### Datetime
|
||||||
|
|
||||||
|
- `datetime(format)`<sup>master</sup> - Return local time with `format`.
|
||||||
|
- `datetime_utc(format)`<sup>master</sup> - Return UTC time with `format`.
|
||||||
|
|
||||||
|
The arguments to `datetime` and `datetime_utc` are `strftime`-style format
|
||||||
|
strings, see the
|
||||||
|
[`chrono` library docs](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
|
||||||
|
for details.
|
||||||
|
|
||||||
#### Semantic Versions
|
#### Semantic Versions
|
||||||
|
|
||||||
- `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a
|
- `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a
|
||||||
|
@ -1686,6 +1711,7 @@ Recipes may be annotated with attributes that change their behavior.
|
||||||
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
| `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
|
||||||
| `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
|
| `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
|
||||||
| `[no-quiet]`<sup>1.23.0</sup> | Override globally quiet recipes and always echo out the recipe. |
|
| `[no-quiet]`<sup>1.23.0</sup> | Override globally quiet recipes and always echo out the recipe. |
|
||||||
|
| `[positional-arguments]`<sup>1.29.0</sup> | Turn on [positional arguments](#positional-arguments) for this recipe. |
|
||||||
| `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
|
| `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
|
||||||
| `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
|
| `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
|
||||||
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |
|
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |
|
||||||
|
@ -1845,6 +1871,8 @@ Recipe groups:
|
||||||
rust recipes
|
rust recipes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `just --groups --unsorted` to print groups in their justfile order.
|
||||||
|
|
||||||
### Command Evaluation Using Backticks
|
### Command Evaluation Using Backticks
|
||||||
|
|
||||||
Backticks can be used to store the result of commands:
|
Backticks can be used to store the result of commands:
|
||||||
|
@ -2059,7 +2087,7 @@ a $A $B=`echo $A`:
|
||||||
When [export](#export) is set, all `just` variables are exported as environment
|
When [export](#export) is set, all `just` variables are exported as environment
|
||||||
variables.
|
variables.
|
||||||
|
|
||||||
#### Unexporting Environment Variables<sup>master</sup>
|
#### Unexporting Environment Variables<sup>1.29.0</sup>
|
||||||
|
|
||||||
Environment variables can be unexported with the `unexport keyword`:
|
Environment variables can be unexported with the `unexport keyword`:
|
||||||
|
|
||||||
|
@ -3117,7 +3145,7 @@ import? 'foo/bar.just'
|
||||||
|
|
||||||
Missing source files for optional imports do not produce an error.
|
Missing source files for optional imports do not produce an error.
|
||||||
|
|
||||||
### Modules <sup>1.19.0</sup>
|
### Modules<sup>1.19.0</sup>
|
||||||
|
|
||||||
A `justfile` can declare modules using `mod` statements. `mod` statements are
|
A `justfile` can declare modules using `mod` statements. `mod` statements are
|
||||||
currently unstable, so you'll need to use the `--unstable` flag, or set the
|
currently unstable, so you'll need to use the `--unstable` flag, or set the
|
||||||
|
@ -3362,9 +3390,9 @@ foo argument:
|
||||||
touch "$1"
|
touch "$1"
|
||||||
```
|
```
|
||||||
|
|
||||||
This defeats `just`'s ability to catch typos, for example if you type `$2`, but
|
This defeats `just`'s ability to catch typos, for example if you type `$2`
|
||||||
works for all possible values of `argument`, including those with double
|
instead of `$1`, but works for all possible values of `argument`, including
|
||||||
quotes.
|
those with double quotes.
|
||||||
|
|
||||||
#### Exported Arguments
|
#### Exported Arguments
|
||||||
|
|
||||||
|
@ -3629,6 +3657,33 @@ Node.js `package.json` files:
|
||||||
export PATH := "./node_modules/.bin:" + env_var('PATH')
|
export PATH := "./node_modules/.bin:" + env_var('PATH')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Paths on Windows
|
||||||
|
|
||||||
|
On Windows, functions that return paths will return `\`-separated paths. When
|
||||||
|
not using PowerShell or `cmd.exe` these paths should be quoted to prevent the
|
||||||
|
`\`s from being intepreted as character escapes:
|
||||||
|
|
||||||
|
```just
|
||||||
|
ls:
|
||||||
|
echo '{{absolute_path(".")}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remote Justfiles
|
||||||
|
|
||||||
|
If you wish to include a `mod` or `import` source file in many `justfiles`
|
||||||
|
without needing to duplicate it, you can use an optional `mod` or `import`,
|
||||||
|
along with a recipe to fetch the module source:
|
||||||
|
|
||||||
|
```just
|
||||||
|
import? 'foo.just'
|
||||||
|
|
||||||
|
fetch:
|
||||||
|
curl https://raw.githubusercontent.com/casey/just/master/justfile > foo.just
|
||||||
|
```
|
||||||
|
|
||||||
|
Given the above `justfile`, after running `just fetch`, the recipes in
|
||||||
|
`foo.just` will be available.
|
||||||
|
|
||||||
### Alternatives and Prior Art
|
### Alternatives and Prior Art
|
||||||
|
|
||||||
There is no shortage of command runners! Some more or less similar alternatives
|
There is no shortage of command runners! Some more or less similar alternatives
|
||||||
|
|
|
@ -30,13 +30,9 @@ fn main() {
|
||||||
.replace_all(
|
.replace_all(
|
||||||
&fs::read_to_string("CHANGELOG.md").unwrap(),
|
&fs::read_to_string("CHANGELOG.md").unwrap(),
|
||||||
|captures: &Captures| {
|
|captures: &Captures| {
|
||||||
let pr = captures[1].parse().unwrap();
|
let pr = captures[1].parse().unwrap();
|
||||||
match author(pr).as_str() {
|
let contributor = author(pr);
|
||||||
"casey" => format!("([#{pr}](https://github.com/casey/just/pull/{pr}))"),
|
format!("([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))")
|
||||||
contributor => {
|
|
||||||
format!("([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -149,7 +149,7 @@ impl<'src> Analyzer<'src> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let recipes = RecipeResolver::resolve_recipes(recipe_table, &self.assignments)?;
|
let recipes = RecipeResolver::resolve_recipes(&self.assignments, &settings, recipe_table)?;
|
||||||
|
|
||||||
let mut aliases = Table::new();
|
let mut aliases = Table::new();
|
||||||
while let Some(alias) = self.aliases.pop() {
|
while let Some(alias) = self.aliases.pop() {
|
||||||
|
|
403
src/argument_parser.rs
Normal file
403
src/argument_parser.rs
Normal file
|
@ -0,0 +1,403 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::doc_markdown)]
|
||||||
|
/// The argument parser is responsible for grouping positional arguments into
|
||||||
|
/// argument groups, which consist of a path to a recipe and its arguments.
|
||||||
|
///
|
||||||
|
/// Argument parsing is substantially complicated by the fact that recipe paths
|
||||||
|
/// can be given on the command line as multiple arguments, i.e., "foo" "bar"
|
||||||
|
/// baz", or as a single "::"-separated argument.
|
||||||
|
///
|
||||||
|
/// Error messages produced by the argument parser should use the format of the
|
||||||
|
/// recipe path as passed on the command line.
|
||||||
|
///
|
||||||
|
/// Additionally, if a recipe is specified with a "::"-separated path, extra
|
||||||
|
/// components of that path after a valid recipe must not be used as arguments,
|
||||||
|
/// whereas arguments after multiple argument path may be used as arguments. As
|
||||||
|
/// an example, `foo bar baz` may refer to recipe `foo::bar` with argument
|
||||||
|
/// `baz`, but `foo::bar::baz` is an error, since `bar` is a recipe, not a
|
||||||
|
/// module.
|
||||||
|
pub(crate) struct ArgumentParser<'src: 'run, 'run> {
|
||||||
|
arguments: &'run [&'run str],
|
||||||
|
next: usize,
|
||||||
|
root: &'run Justfile<'src>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub(crate) struct ArgumentGroup<'run> {
|
||||||
|
pub(crate) arguments: Vec<&'run str>,
|
||||||
|
pub(crate) path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
|
||||||
|
pub(crate) fn parse_arguments(
|
||||||
|
root: &'run Justfile<'src>,
|
||||||
|
arguments: &'run [&'run str],
|
||||||
|
) -> RunResult<'src, Vec<ArgumentGroup<'run>>> {
|
||||||
|
let mut groups = Vec::new();
|
||||||
|
|
||||||
|
let mut invocation_parser = Self {
|
||||||
|
arguments,
|
||||||
|
next: 0,
|
||||||
|
root,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
groups.push(invocation_parser.parse_group()?);
|
||||||
|
|
||||||
|
if invocation_parser.next == arguments.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_group(&mut self) -> RunResult<'src, ArgumentGroup<'run>> {
|
||||||
|
let (recipe, path) = if let Some(next) = self.next() {
|
||||||
|
if next.contains(':') {
|
||||||
|
let module_path =
|
||||||
|
ModulePath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {
|
||||||
|
recipe: next.into(),
|
||||||
|
suggestion: None,
|
||||||
|
})?;
|
||||||
|
let (recipe, path, _) = self.resolve_recipe(true, &module_path.path)?;
|
||||||
|
self.next += 1;
|
||||||
|
(recipe, path)
|
||||||
|
} else {
|
||||||
|
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||||
|
self.next += consumed;
|
||||||
|
(recipe, path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||||
|
assert_eq!(consumed, 0);
|
||||||
|
(recipe, path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let rest = self.rest();
|
||||||
|
|
||||||
|
let argument_range = recipe.argument_range();
|
||||||
|
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
||||||
|
if !argument_range.range_contains(&argument_count) {
|
||||||
|
return Err(Error::ArgumentCountMismatch {
|
||||||
|
recipe: recipe.name(),
|
||||||
|
parameters: recipe.parameters.clone(),
|
||||||
|
found: rest.len(),
|
||||||
|
min: recipe.min_arguments(),
|
||||||
|
max: recipe.max_arguments(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let arguments = rest[..argument_count].to_vec();
|
||||||
|
|
||||||
|
self.next += argument_count;
|
||||||
|
|
||||||
|
Ok(ArgumentGroup { arguments, path })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_recipe(
|
||||||
|
&self,
|
||||||
|
module_path: bool,
|
||||||
|
args: &[impl AsRef<str>],
|
||||||
|
) -> RunResult<'src, (&'run Recipe<'src>, Vec<String>, usize)> {
|
||||||
|
let mut current = self.root;
|
||||||
|
let mut path = Vec::new();
|
||||||
|
|
||||||
|
for (i, arg) in args.iter().enumerate() {
|
||||||
|
let arg = arg.as_ref();
|
||||||
|
|
||||||
|
path.push(arg.to_string());
|
||||||
|
|
||||||
|
if let Some(module) = current.modules.get(arg) {
|
||||||
|
current = module;
|
||||||
|
} else if let Some(recipe) = current.get_recipe(arg) {
|
||||||
|
if module_path && i + 1 < args.len() {
|
||||||
|
return Err(Error::ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path: if module_path {
|
||||||
|
path.join("::")
|
||||||
|
} else {
|
||||||
|
path.join(" ")
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Ok((recipe, path, i + 1));
|
||||||
|
} else {
|
||||||
|
if module_path && i + 1 < args.len() {
|
||||||
|
return Err(Error::UnknownSubmodule {
|
||||||
|
path: path.join("::"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::UnknownRecipe {
|
||||||
|
recipe: if module_path {
|
||||||
|
path.join("::")
|
||||||
|
} else {
|
||||||
|
path.join(" ")
|
||||||
|
},
|
||||||
|
suggestion: current.suggest_recipe(arg),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(recipe) = ¤t.default {
|
||||||
|
recipe.check_can_be_default_recipe()?;
|
||||||
|
path.push(recipe.name().into());
|
||||||
|
Ok((recipe, path, args.len()))
|
||||||
|
} else if current.recipes.is_empty() {
|
||||||
|
Err(Error::NoRecipes)
|
||||||
|
} else {
|
||||||
|
Err(Error::NoDefaultRecipe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Option<&'run str> {
|
||||||
|
self.arguments.get(self.next).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rest(&self) -> &[&'run str] {
|
||||||
|
&self.arguments[self.next..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {super::*, tempfile::TempDir};
|
||||||
|
|
||||||
|
trait TempDirExt {
|
||||||
|
fn write(&self, path: &str, content: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempDirExt for TempDir {
|
||||||
|
fn write(&self, path: &str, content: &str) {
|
||||||
|
let path = self.path().join(path);
|
||||||
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
|
fs::write(path, content).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_no_arguments() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into()],
|
||||||
|
arguments: Vec::new()
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_with_argument() {
|
||||||
|
let justfile = testing::compile("foo bar:");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo", "baz"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into()],
|
||||||
|
arguments: vec!["baz"],
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_argument_count_mismatch() {
|
||||||
|
let justfile = testing::compile("foo bar:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap_err(),
|
||||||
|
Error::ArgumentCountMismatch {
|
||||||
|
recipe: "foo",
|
||||||
|
found: 0,
|
||||||
|
min: 1,
|
||||||
|
max: 1,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_unknown() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["bar"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_unknown() {
|
||||||
|
let justfile = testing::compile("foo:");
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&justfile, &["bar", "baz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule() {
|
||||||
|
let loader = Loader::new();
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let path = tempdir.path().join("justfile");
|
||||||
|
fs::write(&path, "mod foo").unwrap();
|
||||||
|
fs::create_dir(tempdir.path().join("foo")).unwrap();
|
||||||
|
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "bar"]).unwrap(),
|
||||||
|
vec![ArgumentGroup {
|
||||||
|
path: vec!["foo".into(), "bar".into()],
|
||||||
|
arguments: Vec::new()
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule_unknown() {
|
||||||
|
let loader = Loader::new();
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
let path = tempdir.path().join("justfile");
|
||||||
|
fs::write(&path, "mod foo").unwrap();
|
||||||
|
fs::create_dir(tempdir.path().join("foo")).unwrap();
|
||||||
|
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &path).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo", "zzz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "foo zzz",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_in_submodule_path_unknown() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "mod foo");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::zzz"]).unwrap_err(),
|
||||||
|
Error::UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion: None
|
||||||
|
} if recipe == "foo::zzz",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn module_path_not_consumed() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "mod foo");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &["foo::bar::baz"]).unwrap_err(),
|
||||||
|
Error::ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path,
|
||||||
|
} if path == "foo::bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_recipes() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::NoRecipes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_recipe_requires_arguments() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "foo bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::DefaultRecipeRequiresArguments {
|
||||||
|
recipe: "foo",
|
||||||
|
min_arguments: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_default_recipe() {
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
tempdir.write("justfile", "import 'foo.just'");
|
||||||
|
tempdir.write("foo.just", "bar:");
|
||||||
|
|
||||||
|
let loader = Loader::new();
|
||||||
|
let compilation = Compiler::compile(true, &loader, &tempdir.path().join("justfile")).unwrap();
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||||
|
Error::NoDefaultRecipe,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_grouping() {
|
||||||
|
let justfile = testing::compile(
|
||||||
|
"
|
||||||
|
FOO A B='blarg':
|
||||||
|
echo foo: {{A}} {{B}}
|
||||||
|
|
||||||
|
BAR X:
|
||||||
|
echo bar: {{X}}
|
||||||
|
|
||||||
|
BAZ +Z:
|
||||||
|
echo baz: {{Z}}
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ArgumentParser::parse_arguments(
|
||||||
|
&justfile,
|
||||||
|
&["BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"]
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
vec![
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["BAR".into()],
|
||||||
|
arguments: vec!["0"],
|
||||||
|
},
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["FOO".into()],
|
||||||
|
arguments: vec!["1", "2"],
|
||||||
|
},
|
||||||
|
ArgumentGroup {
|
||||||
|
path: vec!["BAZ".into()],
|
||||||
|
arguments: vec!["3", "4", "5"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ use super::*;
|
||||||
pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;
|
pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;
|
||||||
|
|
||||||
impl<'src> Display for Assignment<'src> {
|
impl<'src> Display for Assignment<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
if self.export {
|
if self.export {
|
||||||
write!(f, "export ")?;
|
write!(f, "export ")?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ pub(crate) struct Ast<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Ast<'src> {
|
impl<'src> Display for Ast<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
let mut iter = self.items.iter().peekable();
|
let mut iter = self.items.iter().peekable();
|
||||||
|
|
||||||
while let Some(item) = iter.next() {
|
while let Some(item) = iter.next() {
|
||||||
|
|
|
@ -17,6 +17,7 @@ pub(crate) enum Attribute<'src> {
|
||||||
NoCd,
|
NoCd,
|
||||||
NoExitMessage,
|
NoExitMessage,
|
||||||
NoQuiet,
|
NoQuiet,
|
||||||
|
PositionalArguments,
|
||||||
Private,
|
Private,
|
||||||
Unix,
|
Unix,
|
||||||
Windows,
|
Windows,
|
||||||
|
@ -32,6 +33,7 @@ impl AttributeDiscriminant {
|
||||||
| Self::NoCd
|
| Self::NoCd
|
||||||
| Self::NoExitMessage
|
| Self::NoExitMessage
|
||||||
| Self::NoQuiet
|
| Self::NoQuiet
|
||||||
|
| Self::PositionalArguments
|
||||||
| Self::Private
|
| Self::Private
|
||||||
| Self::Unix
|
| Self::Unix
|
||||||
| Self::Windows => 0..=0,
|
| Self::Windows => 0..=0,
|
||||||
|
@ -78,6 +80,7 @@ impl<'src> Attribute<'src> {
|
||||||
NoCd => Self::NoCd,
|
NoCd => Self::NoCd,
|
||||||
NoExitMessage => Self::NoExitMessage,
|
NoExitMessage => Self::NoExitMessage,
|
||||||
NoQuiet => Self::NoQuiet,
|
NoQuiet => Self::NoQuiet,
|
||||||
|
PositionalArguments => Self::PositionalArguments,
|
||||||
Private => Self::Private,
|
Private => Self::Private,
|
||||||
Unix => Self::Unix,
|
Unix => Self::Unix,
|
||||||
Windows => Self::Windows,
|
Windows => Self::Windows,
|
||||||
|
@ -98,6 +101,7 @@ impl<'src> Attribute<'src> {
|
||||||
| Self::NoCd
|
| Self::NoCd
|
||||||
| Self::NoExitMessage
|
| Self::NoExitMessage
|
||||||
| Self::NoQuiet
|
| Self::NoQuiet
|
||||||
|
| Self::PositionalArguments
|
||||||
| Self::Private
|
| Self::Private
|
||||||
| Self::Unix
|
| Self::Unix
|
||||||
| Self::Windows => None,
|
| Self::Windows => None,
|
||||||
|
@ -106,7 +110,7 @@ impl<'src> Attribute<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Attribute<'src> {
|
impl<'src> Display for Attribute<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.name())?;
|
write!(f, "{}", self.name())?;
|
||||||
if let Some(argument) = self.argument() {
|
if let Some(argument) = self.argument() {
|
||||||
write!(f, "({argument})")?;
|
write!(f, "({argument})")?;
|
||||||
|
|
|
@ -28,7 +28,7 @@ fn capitalize(s: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for CompileError<'_> {
|
impl Display for CompileError<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
use CompileErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
|
|
||||||
match &*self.kind {
|
match &*self.kind {
|
||||||
|
|
|
@ -94,6 +94,9 @@ export extern "just" [
|
||||||
]"#;
|
]"#;
|
||||||
|
|
||||||
const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
|
const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
|
||||||
|
if string match -rq '(-f|--justfile)\s*=?(?<justfile>[^\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]
|
||||||
|
set -fx JUST_JUSTFILE "$justfile"
|
||||||
|
end
|
||||||
just --list 2> /dev/null | tail -n +2 | awk '{
|
just --list 2> /dev/null | tail -n +2 | awk '{
|
||||||
command = $1;
|
command = $1;
|
||||||
args = $0;
|
args = $0;
|
||||||
|
@ -134,7 +137,7 @@ complete -c just -a '(__fish_just_complete_recipes)'
|
||||||
|
|
||||||
const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
|
const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
|
||||||
(
|
(
|
||||||
r#" _arguments "${_arguments_options[@]}" \"#,
|
r#" _arguments "${_arguments_options[@]}" : \"#,
|
||||||
r" local common=(",
|
r" local common=(",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub(crate) struct Condition<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Condition<'src> {
|
impl<'src> Display for Condition<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "{} {} {}", self.lhs, self.operator, self.rhs)
|
write!(f, "{} {} {}", self.lhs, self.operator, self.rhs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
353
src/config.rs
353
src/config.rs
|
@ -7,10 +7,6 @@ use {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHOOSE_HELP: &str = "Select one or more recipes to run using a binary chooser. \
|
|
||||||
If `--chooser` is not passed the chooser defaults to the \
|
|
||||||
value of $JUST_CHOOSER, falling back to `fzf`";
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) struct Config {
|
pub(crate) struct Config {
|
||||||
pub(crate) check: bool,
|
pub(crate) check: bool,
|
||||||
|
@ -154,17 +150,20 @@ impl Config {
|
||||||
.trailing_var_arg(true)
|
.trailing_var_arg(true)
|
||||||
.styles(
|
.styles(
|
||||||
Styles::styled()
|
Styles::styled()
|
||||||
.header(AnsiColor::Yellow.on_default())
|
.header(AnsiColor::Yellow.on_default())
|
||||||
.usage(AnsiColor::Yellow.on_default())
|
.literal(AnsiColor::Green.on_default())
|
||||||
.literal(AnsiColor::Green.on_default())
|
.placeholder(AnsiColor::Green.on_default())
|
||||||
.placeholder(AnsiColor::Green.on_default())
|
.usage(AnsiColor::Yellow.on_default()),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::CHECK)
|
Arg::new(arg::CHECK)
|
||||||
.long("check")
|
.long("check")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.requires(cmd::FORMAT)
|
.requires(cmd::FORMAT)
|
||||||
.help("Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required."),
|
.help(
|
||||||
|
"Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. \
|
||||||
|
Exits with 1 and prints a diff if formatting is required.",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::CHOOSER)
|
Arg::new(arg::CHOOSER)
|
||||||
|
@ -173,6 +172,13 @@ impl Config {
|
||||||
.action(ArgAction::Set)
|
.action(ArgAction::Set)
|
||||||
.help("Override binary invoked by `--choose`"),
|
.help("Override binary invoked by `--choose`"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::CLEAR_SHELL_ARGS)
|
||||||
|
.long("clear-shell-args")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.overrides_with(arg::SHELL_ARG)
|
||||||
|
.help("Clear shell arguments"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::COLOR)
|
Arg::new(arg::COLOR)
|
||||||
.long("color")
|
.long("color")
|
||||||
|
@ -190,7 +196,21 @@ impl Config {
|
||||||
.value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES))
|
.value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES))
|
||||||
.help("Echo recipe lines in <COMMAND-COLOR>"),
|
.help("Echo recipe lines in <COMMAND-COLOR>"),
|
||||||
)
|
)
|
||||||
.arg(Arg::new(arg::YES).long("yes").action(ArgAction::SetTrue).help("Automatically confirm all recipes."))
|
.arg(
|
||||||
|
Arg::new(arg::DOTENV_FILENAME)
|
||||||
|
.long("dotenv-filename")
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.help("Search for environment file named <DOTENV-FILENAME> instead of `.env`")
|
||||||
|
.conflicts_with(arg::DOTENV_PATH),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::DOTENV_PATH)
|
||||||
|
.short('E')
|
||||||
|
.long("dotenv-path")
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.help("Load <DOTENV-PATH> as environment file instead of searching for one"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::DRY_RUN)
|
Arg::new(arg::DRY_RUN)
|
||||||
.short('n')
|
.short('n')
|
||||||
|
@ -203,66 +223,30 @@ impl Config {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::DUMP_FORMAT)
|
Arg::new(arg::DUMP_FORMAT)
|
||||||
.long("dump-format")
|
.long("dump-format")
|
||||||
|
.env("JUST_DUMP_FORMAT")
|
||||||
.action(ArgAction::Set)
|
.action(ArgAction::Set)
|
||||||
.value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES))
|
.value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES))
|
||||||
.default_value(arg::DUMP_FORMAT_JUST)
|
.default_value(arg::DUMP_FORMAT_JUST)
|
||||||
.value_name("FORMAT")
|
.value_name("FORMAT")
|
||||||
.help("Dump justfile as <FORMAT>"),
|
.help("Dump justfile as <FORMAT>"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::GLOBAL_JUSTFILE)
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.long("global-justfile")
|
||||||
|
.short('g')
|
||||||
|
.conflicts_with(arg::JUSTFILE)
|
||||||
|
.conflicts_with(arg::WORKING_DIRECTORY)
|
||||||
|
.help("Use global justfile"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::HIGHLIGHT)
|
Arg::new(arg::HIGHLIGHT)
|
||||||
.long("highlight")
|
.long("highlight")
|
||||||
|
.env("JUST_HIGHLIGHT")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Highlight echoed recipe lines in bold")
|
.help("Highlight echoed recipe lines in bold")
|
||||||
.overrides_with(arg::NO_HIGHLIGHT),
|
.overrides_with(arg::NO_HIGHLIGHT),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new(arg::LIST_HEADING)
|
|
||||||
.long("list-heading")
|
|
||||||
.help("Print <TEXT> before list")
|
|
||||||
.value_name("TEXT")
|
|
||||||
.action(ArgAction::Set),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::LIST_PREFIX)
|
|
||||||
.long("list-prefix")
|
|
||||||
.help("Print <TEXT> before each list item")
|
|
||||||
.value_name("TEXT")
|
|
||||||
.action(ArgAction::Set),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::LIST_SUBMODULES)
|
|
||||||
.long("list-submodules")
|
|
||||||
.help("List recipes in submodules")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.env("JUST_LIST_SUBMODULES"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::NO_ALIASES)
|
|
||||||
.long("no-aliases")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.help("Don't show aliases in list"),
|
|
||||||
)
|
|
||||||
.arg (
|
|
||||||
Arg::new(arg::NO_DEPS)
|
|
||||||
.long("no-deps")
|
|
||||||
.alias("no-dependencies")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.help("Don't run recipe dependencies")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::NO_DOTENV)
|
|
||||||
.long("no-dotenv")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.help("Don't load `.env` file"),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::NO_HIGHLIGHT)
|
|
||||||
.long("no-highlight")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.help("Don't highlight echoed recipe lines in bold")
|
|
||||||
.overrides_with(arg::HIGHLIGHT),
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::JUSTFILE)
|
Arg::new(arg::JUSTFILE)
|
||||||
.short('f')
|
.short('f')
|
||||||
|
@ -272,6 +256,60 @@ impl Config {
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
.help("Use <JUSTFILE> as justfile"),
|
.help("Use <JUSTFILE> as justfile"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::LIST_HEADING)
|
||||||
|
.long("list-heading")
|
||||||
|
.env("JUST_LIST_HEADING")
|
||||||
|
.help("Print <TEXT> before list")
|
||||||
|
.value_name("TEXT")
|
||||||
|
.action(ArgAction::Set),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::LIST_PREFIX)
|
||||||
|
.long("list-prefix")
|
||||||
|
.env("JUST_LIST_PREFIX")
|
||||||
|
.help("Print <TEXT> before each list item")
|
||||||
|
.value_name("TEXT")
|
||||||
|
.action(ArgAction::Set),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::LIST_SUBMODULES)
|
||||||
|
.long("list-submodules")
|
||||||
|
.env("JUST_LIST_SUBMODULES")
|
||||||
|
.help("List recipes in submodules")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.env("JUST_LIST_SUBMODULES"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::NO_ALIASES)
|
||||||
|
.long("no-aliases")
|
||||||
|
.env("JUST_NO_ALIASES")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Don't show aliases in list"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::NO_DEPS)
|
||||||
|
.long("no-deps")
|
||||||
|
.env("JUST_NO_DEPS")
|
||||||
|
.alias("no-dependencies")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Don't run recipe dependencies"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::NO_DOTENV)
|
||||||
|
.long("no-dotenv")
|
||||||
|
.env("JUST_NO_DOTENV")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Don't load `.env` file"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::NO_HIGHLIGHT)
|
||||||
|
.long("no-highlight")
|
||||||
|
.env("JUST_NO_HIGHLIGHT")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Don't highlight echoed recipe lines in bold")
|
||||||
|
.overrides_with(arg::HIGHLIGHT),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::QUIET)
|
Arg::new(arg::QUIET)
|
||||||
.short('q')
|
.short('q')
|
||||||
|
@ -311,15 +349,24 @@ impl Config {
|
||||||
.help("Invoke <COMMAND> with the shell used to run recipe lines and backticks"),
|
.help("Invoke <COMMAND> with the shell used to run recipe lines and backticks"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::CLEAR_SHELL_ARGS)
|
Arg::new(arg::TIMESTAMP)
|
||||||
.long("clear-shell-args")
|
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.overrides_with(arg::SHELL_ARG)
|
.long("timestamp")
|
||||||
.help("Clear shell arguments"),
|
.env("JUST_TIMESTAMP")
|
||||||
|
.help("Print recipe command timestamps"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::TIMESTAMP_FORMAT)
|
||||||
|
.action(ArgAction::Set)
|
||||||
|
.long("timestamp-format")
|
||||||
|
.env("JUST_TIMESTAMP_FORMAT")
|
||||||
|
.default_value("%H:%M:%S")
|
||||||
|
.help("Timestamp format string"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::UNSORTED)
|
Arg::new(arg::UNSORTED)
|
||||||
.long("unsorted")
|
.long("unsorted")
|
||||||
|
.env("JUST_UNSORTED")
|
||||||
.short('u')
|
.short('u')
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Return list and summary entries in source order"),
|
.help("Return list and summary entries in source order"),
|
||||||
|
@ -350,13 +397,28 @@ impl Config {
|
||||||
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
||||||
.requires(arg::JUSTFILE),
|
.requires(arg::JUSTFILE),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(arg::YES)
|
||||||
|
.long("yes")
|
||||||
|
.env("JUST_YES")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("Automatically confirm all recipes."),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::CHANGELOG)
|
Arg::new(cmd::CHANGELOG)
|
||||||
.long("changelog")
|
.long("changelog")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Print changelog"),
|
.help("Print changelog"),
|
||||||
)
|
)
|
||||||
.arg(Arg::new(cmd::CHOOSE).long("choose").action(ArgAction::SetTrue).help(CHOOSE_HELP))
|
.arg(
|
||||||
|
Arg::new(cmd::CHOOSE)
|
||||||
|
.long("choose")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help(
|
||||||
|
"Select one or more recipes to run using a binary chooser. If `--chooser` is not \
|
||||||
|
passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::COMMAND)
|
Arg::new(cmd::COMMAND)
|
||||||
.long("command")
|
.long("command")
|
||||||
|
@ -395,6 +457,7 @@ impl Config {
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::EVALUATE)
|
Arg::new(cmd::EVALUATE)
|
||||||
.long("evaluate")
|
.long("evaluate")
|
||||||
|
.alias("eval")
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help(
|
.help(
|
||||||
"Evaluate and print all variables. If a variable name is given as an argument, only \
|
"Evaluate and print all variables. If a variable name is given as an argument, only \
|
||||||
|
@ -408,6 +471,12 @@ impl Config {
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("Format and overwrite justfile"),
|
.help("Format and overwrite justfile"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new(cmd::GROUPS)
|
||||||
|
.long("groups")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.help("List recipe groups"),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::INIT)
|
Arg::new(cmd::INIT)
|
||||||
.long("init")
|
.long("init")
|
||||||
|
@ -425,12 +494,6 @@ impl Config {
|
||||||
.conflicts_with(arg::ARGUMENTS)
|
.conflicts_with(arg::ARGUMENTS)
|
||||||
.help("List available recipes"),
|
.help("List available recipes"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new(cmd::GROUPS)
|
|
||||||
.long("groups")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.help("List recipe groups")
|
|
||||||
)
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(cmd::MAN)
|
Arg::new(cmd::MAN)
|
||||||
.long("man")
|
.long("man")
|
||||||
|
@ -459,21 +522,6 @@ impl Config {
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)
|
||||||
.help("List names of variables"),
|
.help("List names of variables"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new(arg::DOTENV_FILENAME)
|
|
||||||
.long("dotenv-filename")
|
|
||||||
.action(ArgAction::Set)
|
|
||||||
.help("Search for environment file named <DOTENV-FILENAME> instead of `.env`")
|
|
||||||
.conflicts_with(arg::DOTENV_PATH),
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::DOTENV_PATH)
|
|
||||||
.short('E')
|
|
||||||
.long("dotenv-path")
|
|
||||||
.action(ArgAction::Set)
|
|
||||||
.value_parser(value_parser!(PathBuf))
|
|
||||||
.help("Load <DOTENV-PATH> as environment file instead of searching for one")
|
|
||||||
)
|
|
||||||
.group(ArgGroup::new("SUBCOMMAND").args(cmd::ALL))
|
.group(ArgGroup::new("SUBCOMMAND").args(cmd::ALL))
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new(arg::ARGUMENTS)
|
Arg::new(arg::ARGUMENTS)
|
||||||
|
@ -481,30 +529,6 @@ impl Config {
|
||||||
.action(ArgAction::Append)
|
.action(ArgAction::Append)
|
||||||
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
|
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
|
||||||
)
|
)
|
||||||
.arg(
|
|
||||||
Arg::new(arg::GLOBAL_JUSTFILE)
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.long("global-justfile")
|
|
||||||
.short('g')
|
|
||||||
.conflicts_with(arg::JUSTFILE)
|
|
||||||
.conflicts_with(arg::WORKING_DIRECTORY)
|
|
||||||
.help("Use global justfile")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::TIMESTAMP)
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.long("timestamp")
|
|
||||||
.env("JUST_TIMESTAMP")
|
|
||||||
.help("Print recipe command timestamps")
|
|
||||||
)
|
|
||||||
.arg(
|
|
||||||
Arg::new(arg::TIMESTAMP_FORMAT)
|
|
||||||
.action(ArgAction::Set)
|
|
||||||
.long("timestamp-format")
|
|
||||||
.env("JUST_TIMESTAMP_FORMAT")
|
|
||||||
.default_value("%H:%M:%S")
|
|
||||||
.help("Timestamp format string")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_from_matches(matches: &ArgMatches) -> ConfigResult<Color> {
|
fn color_from_matches(matches: &ArgMatches) -> ConfigResult<Color> {
|
||||||
|
@ -611,17 +635,6 @@ impl Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult<Self> {
|
pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult<Self> {
|
||||||
let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?;
|
|
||||||
|
|
||||||
let verbosity = if matches.get_flag(arg::QUIET) {
|
|
||||||
Verbosity::Quiet
|
|
||||||
} else {
|
|
||||||
Verbosity::from_flag_occurrences(matches.get_count(arg::VERBOSE))
|
|
||||||
};
|
|
||||||
|
|
||||||
let color = Self::color_from_matches(matches)?;
|
|
||||||
let command_color = Self::command_color_from_matches(matches)?;
|
|
||||||
|
|
||||||
let mut overrides = BTreeMap::new();
|
let mut overrides = BTreeMap::new();
|
||||||
if let Some(mut values) = matches.get_many::<String>(arg::SET) {
|
if let Some(mut values) = matches.get_many::<String>(arg::SET) {
|
||||||
while let (Some(k), Some(v)) = (values.next(), values.next()) {
|
while let (Some(k), Some(v)) = (values.next(), values.next()) {
|
||||||
|
@ -684,28 +697,10 @@ impl Config {
|
||||||
}
|
}
|
||||||
} else if let Some(&shell) = matches.get_one::<completions::Shell>(cmd::COMPLETIONS) {
|
} else if let Some(&shell) = matches.get_one::<completions::Shell>(cmd::COMPLETIONS) {
|
||||||
Subcommand::Completions { shell }
|
Subcommand::Completions { shell }
|
||||||
} else if matches.get_flag(cmd::EDIT) {
|
|
||||||
Subcommand::Edit
|
|
||||||
} else if matches.get_flag(cmd::SUMMARY) {
|
|
||||||
Subcommand::Summary
|
|
||||||
} else if matches.get_flag(cmd::DUMP) {
|
} else if matches.get_flag(cmd::DUMP) {
|
||||||
Subcommand::Dump
|
Subcommand::Dump
|
||||||
} else if matches.get_flag(cmd::FORMAT) {
|
} else if matches.get_flag(cmd::EDIT) {
|
||||||
Subcommand::Format
|
Subcommand::Edit
|
||||||
} else if matches.get_flag(cmd::INIT) {
|
|
||||||
Subcommand::Init
|
|
||||||
} else if let Some(path) = matches.get_many::<String>(cmd::LIST) {
|
|
||||||
Subcommand::List {
|
|
||||||
path: Self::parse_module_path(path)?,
|
|
||||||
}
|
|
||||||
} else if matches.get_flag(cmd::GROUPS) {
|
|
||||||
Subcommand::Groups
|
|
||||||
} else if matches.get_flag(cmd::MAN) {
|
|
||||||
Subcommand::Man
|
|
||||||
} else if let Some(path) = matches.get_many::<String>(cmd::SHOW) {
|
|
||||||
Subcommand::Show {
|
|
||||||
path: Self::parse_module_path(path)?,
|
|
||||||
}
|
|
||||||
} else if matches.get_flag(cmd::EVALUATE) {
|
} else if matches.get_flag(cmd::EVALUATE) {
|
||||||
if positional.arguments.len() > 1 {
|
if positional.arguments.len() > 1 {
|
||||||
return Err(ConfigError::SubcommandArguments {
|
return Err(ConfigError::SubcommandArguments {
|
||||||
|
@ -722,6 +717,24 @@ impl Config {
|
||||||
variable: positional.arguments.into_iter().next(),
|
variable: positional.arguments.into_iter().next(),
|
||||||
overrides,
|
overrides,
|
||||||
}
|
}
|
||||||
|
} else if matches.get_flag(cmd::FORMAT) {
|
||||||
|
Subcommand::Format
|
||||||
|
} else if matches.get_flag(cmd::GROUPS) {
|
||||||
|
Subcommand::Groups
|
||||||
|
} else if matches.get_flag(cmd::INIT) {
|
||||||
|
Subcommand::Init
|
||||||
|
} else if let Some(path) = matches.get_many::<String>(cmd::LIST) {
|
||||||
|
Subcommand::List {
|
||||||
|
path: Self::parse_module_path(path)?,
|
||||||
|
}
|
||||||
|
} else if matches.get_flag(cmd::MAN) {
|
||||||
|
Subcommand::Man
|
||||||
|
} else if let Some(path) = matches.get_many::<String>(cmd::SHOW) {
|
||||||
|
Subcommand::Show {
|
||||||
|
path: Self::parse_module_path(path)?,
|
||||||
|
}
|
||||||
|
} else if matches.get_flag(cmd::SUMMARY) {
|
||||||
|
Subcommand::Summary
|
||||||
} else if matches.get_flag(cmd::VARIABLES) {
|
} else if matches.get_flag(cmd::VARIABLES) {
|
||||||
Subcommand::Variables
|
Subcommand::Variables
|
||||||
} else {
|
} else {
|
||||||
|
@ -731,20 +744,10 @@ impl Config {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let shell_args = if matches.get_flag(arg::CLEAR_SHELL_ARGS) {
|
|
||||||
Some(Vec::new())
|
|
||||||
} else {
|
|
||||||
matches
|
|
||||||
.get_many::<String>(arg::SHELL_ARG)
|
|
||||||
.map(|s| s.map(Into::into).collect())
|
|
||||||
};
|
|
||||||
|
|
||||||
let unstable = matches.get_flag(arg::UNSTABLE);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
check: matches.get_flag(arg::CHECK),
|
check: matches.get_flag(arg::CHECK),
|
||||||
color,
|
color: Self::color_from_matches(matches)?,
|
||||||
command_color,
|
command_color: Self::command_color_from_matches(matches)?,
|
||||||
dotenv_filename: matches
|
dotenv_filename: matches
|
||||||
.get_one::<String>(arg::DOTENV_FILENAME)
|
.get_one::<String>(arg::DOTENV_FILENAME)
|
||||||
.map(Into::into),
|
.map(Into::into),
|
||||||
|
@ -752,7 +755,7 @@ impl Config {
|
||||||
dry_run: matches.get_flag(arg::DRY_RUN),
|
dry_run: matches.get_flag(arg::DRY_RUN),
|
||||||
dump_format: Self::dump_format_from_matches(matches)?,
|
dump_format: Self::dump_format_from_matches(matches)?,
|
||||||
highlight: !matches.get_flag(arg::NO_HIGHLIGHT),
|
highlight: !matches.get_flag(arg::NO_HIGHLIGHT),
|
||||||
invocation_directory,
|
invocation_directory: env::current_dir().context(config_error::CurrentDirContext)?,
|
||||||
list_heading: matches
|
list_heading: matches
|
||||||
.get_one::<String>(arg::LIST_HEADING)
|
.get_one::<String>(arg::LIST_HEADING)
|
||||||
.map_or_else(|| "Available recipes:\n".into(), Into::into),
|
.map_or_else(|| "Available recipes:\n".into(), Into::into),
|
||||||
|
@ -765,7 +768,13 @@ impl Config {
|
||||||
no_dependencies: matches.get_flag(arg::NO_DEPS),
|
no_dependencies: matches.get_flag(arg::NO_DEPS),
|
||||||
search_config,
|
search_config,
|
||||||
shell: matches.get_one::<String>(arg::SHELL).map(Into::into),
|
shell: matches.get_one::<String>(arg::SHELL).map(Into::into),
|
||||||
shell_args,
|
shell_args: if matches.get_flag(arg::CLEAR_SHELL_ARGS) {
|
||||||
|
Some(Vec::new())
|
||||||
|
} else {
|
||||||
|
matches
|
||||||
|
.get_many::<String>(arg::SHELL_ARG)
|
||||||
|
.map(|s| s.map(Into::into).collect())
|
||||||
|
},
|
||||||
shell_command: matches.get_flag(arg::SHELL_COMMAND),
|
shell_command: matches.get_flag(arg::SHELL_COMMAND),
|
||||||
subcommand,
|
subcommand,
|
||||||
timestamp: matches.get_flag(arg::TIMESTAMP),
|
timestamp: matches.get_flag(arg::TIMESTAMP),
|
||||||
|
@ -774,13 +783,17 @@ impl Config {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.into(),
|
.into(),
|
||||||
unsorted: matches.get_flag(arg::UNSORTED),
|
unsorted: matches.get_flag(arg::UNSORTED),
|
||||||
unstable,
|
unstable: matches.get_flag(arg::UNSTABLE),
|
||||||
verbosity,
|
verbosity: if matches.get_flag(arg::QUIET) {
|
||||||
|
Verbosity::Quiet
|
||||||
|
} else {
|
||||||
|
Verbosity::from_flag_occurrences(matches.get_count(arg::VERBOSE))
|
||||||
|
},
|
||||||
yes: matches.get_flag(arg::YES),
|
yes: matches.get_flag(arg::YES),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> {
|
pub(crate) fn require_unstable(&self, message: &str) -> RunResult<'static> {
|
||||||
if self.unstable {
|
if self.unstable {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
|
@ -790,7 +803,7 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(self, loader: &Loader) -> Result<(), Error> {
|
pub(crate) fn run(self, loader: &Loader) -> RunResult {
|
||||||
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
||||||
warn!("Failed to set CTRL-C handler: {error}");
|
warn!("Failed to set CTRL-C handler: {error}");
|
||||||
}
|
}
|
||||||
|
@ -1469,6 +1482,22 @@ mod tests {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_from_working_directory_and_justfile_long_stdin,
|
||||||
|
args: ["--working-directory", "foo", "--justfile", "-"],
|
||||||
|
search_config: SearchConfig::WithStdinAndWorkingDirectory {
|
||||||
|
working_directory: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_from_working_directory_and_justfile_short_stdin,
|
||||||
|
args: ["--working-directory", "foo", "-f", "-"],
|
||||||
|
search_config: SearchConfig::WithStdinAndWorkingDirectory {
|
||||||
|
working_directory: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: search_config_justfile_long,
|
name: search_config_justfile_long,
|
||||||
args: ["--justfile", "foo"],
|
args: ["--justfile", "foo"],
|
||||||
|
@ -1477,6 +1506,12 @@ mod tests {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_justfile_long_stdin,
|
||||||
|
args: ["--justfile", "-"],
|
||||||
|
search_config: SearchConfig::WithStdin,
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: search_config_justfile_short,
|
name: search_config_justfile_short,
|
||||||
args: ["-f", "foo"],
|
args: ["-f", "foo"],
|
||||||
|
@ -1485,6 +1520,12 @@ mod tests {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_justfile_short_stdin,
|
||||||
|
args: ["-f", "-"],
|
||||||
|
search_config: SearchConfig::WithStdin,
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: search_directory_parent,
|
name: search_directory_parent,
|
||||||
args: ["../"],
|
args: ["../"],
|
||||||
|
|
|
@ -8,7 +8,7 @@ pub(crate) struct Dependency<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Dependency<'src> {
|
impl<'src> Display for Dependency<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
if self.arguments.is_empty() {
|
if self.arguments.is_empty() {
|
||||||
write!(f, "{}", self.recipe.name())
|
write!(f, "{}", self.recipe.name())
|
||||||
} else {
|
} else {
|
||||||
|
|
18
src/error.rs
18
src/error.rs
|
@ -95,6 +95,9 @@ pub(crate) enum Error<'src> {
|
||||||
variable: String,
|
variable: String,
|
||||||
suggestion: Option<Suggestion<'src>>,
|
suggestion: Option<Suggestion<'src>>,
|
||||||
},
|
},
|
||||||
|
ExpectedSubmoduleButFoundRecipe {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
FormatCheckFoundDiff,
|
FormatCheckFoundDiff,
|
||||||
FunctionCall {
|
FunctionCall {
|
||||||
function: Name<'src>,
|
function: Name<'src>,
|
||||||
|
@ -162,13 +165,13 @@ pub(crate) enum Error<'src> {
|
||||||
line_number: Option<usize>,
|
line_number: Option<usize>,
|
||||||
},
|
},
|
||||||
UnknownSubmodule {
|
UnknownSubmodule {
|
||||||
path: ModulePath,
|
path: String,
|
||||||
},
|
},
|
||||||
UnknownOverrides {
|
UnknownOverrides {
|
||||||
overrides: Vec<String>,
|
overrides: Vec<String>,
|
||||||
},
|
},
|
||||||
UnknownRecipes {
|
UnknownRecipe {
|
||||||
recipes: Vec<String>,
|
recipe: String,
|
||||||
suggestion: Option<Suggestion<'src>>,
|
suggestion: Option<Suggestion<'src>>,
|
||||||
},
|
},
|
||||||
Unstable {
|
Unstable {
|
||||||
|
@ -365,6 +368,9 @@ impl<'src> ColorDisplay for Error<'src> {
|
||||||
write!(f, "\n{suggestion}")?;
|
write!(f, "\n{suggestion}")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ExpectedSubmoduleButFoundRecipe { path } => {
|
||||||
|
write!(f, "Expected submodule at `{path}` but found recipe.")?;
|
||||||
|
},
|
||||||
FormatCheckFoundDiff => {
|
FormatCheckFoundDiff => {
|
||||||
write!(f, "Formatted justfile differs from original.")?;
|
write!(f, "Formatted justfile differs from original.")?;
|
||||||
}
|
}
|
||||||
|
@ -447,10 +453,8 @@ impl<'src> ColorDisplay for Error<'src> {
|
||||||
let overrides = List::and_ticked(overrides);
|
let overrides = List::and_ticked(overrides);
|
||||||
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
|
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
|
||||||
}
|
}
|
||||||
UnknownRecipes { recipes, suggestion } => {
|
UnknownRecipe { recipe, suggestion } => {
|
||||||
let count = Count("recipe", recipes.len());
|
write!(f, "Justfile does not contain recipe `{recipe}`.")?;
|
||||||
let recipes = List::or_ticked(recipes);
|
|
||||||
write!(f, "Justfile does not contain {count} {recipes}.")?;
|
|
||||||
if let Some(suggestion) = suggestion {
|
if let Some(suggestion) = suggestion {
|
||||||
write!(f, "\n{suggestion}")?;
|
write!(f, "\n{suggestion}")?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ impl<'src> Expression<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Expression<'src> {
|
impl<'src> Display for Expression<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
||||||
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
||||||
|
|
151
src/function.rs
151
src/function.rs
|
@ -11,13 +11,13 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) enum Function {
|
pub(crate) enum Function {
|
||||||
Nullary(fn(Context) -> Result<String, String>),
|
Nullary(fn(Context) -> FunctionResult),
|
||||||
Unary(fn(Context, &str) -> Result<String, String>),
|
Unary(fn(Context, &str) -> FunctionResult),
|
||||||
UnaryOpt(fn(Context, &str, Option<&str>) -> Result<String, String>),
|
UnaryOpt(fn(Context, &str, Option<&str>) -> FunctionResult),
|
||||||
UnaryPlus(fn(Context, &str, &[String]) -> Result<String, String>),
|
UnaryPlus(fn(Context, &str, &[String]) -> FunctionResult),
|
||||||
Binary(fn(Context, &str, &str) -> Result<String, String>),
|
Binary(fn(Context, &str, &str) -> FunctionResult),
|
||||||
BinaryPlus(fn(Context, &str, &str, &[String]) -> Result<String, String>),
|
BinaryPlus(fn(Context, &str, &str, &[String]) -> FunctionResult),
|
||||||
Ternary(fn(Context, &str, &str, &str) -> Result<String, String>),
|
Ternary(fn(Context, &str, &str, &str) -> FunctionResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Context<'src: 'run, 'run> {
|
pub(crate) struct Context<'src: 'run, 'run> {
|
||||||
|
@ -47,6 +47,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
||||||
"config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
|
"config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
|
||||||
"data_directory" => Nullary(|_| dir("data", dirs::data_dir)),
|
"data_directory" => Nullary(|_| dir("data", dirs::data_dir)),
|
||||||
"data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)),
|
"data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)),
|
||||||
|
"datetime" => Unary(datetime),
|
||||||
|
"datetime_utc" => Unary(datetime_utc),
|
||||||
"encode_uri_component" => Unary(encode_uri_component),
|
"encode_uri_component" => Unary(encode_uri_component),
|
||||||
"env" => UnaryOpt(env),
|
"env" => UnaryOpt(env),
|
||||||
"env_var" => Unary(env_var),
|
"env_var" => Unary(env_var),
|
||||||
|
@ -119,7 +121,7 @@ impl Function {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn absolute_path(context: Context, path: &str) -> Result<String, String> {
|
fn absolute_path(context: Context, path: &str) -> FunctionResult {
|
||||||
let abs_path_unchecked = context
|
let abs_path_unchecked = context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -136,7 +138,7 @@ fn absolute_path(context: Context, path: &str) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn append(_context: Context, suffix: &str, s: &str) -> Result<String, String> {
|
fn append(_context: Context, suffix: &str, s: &str) -> FunctionResult {
|
||||||
Ok(
|
Ok(
|
||||||
s.split_whitespace()
|
s.split_whitespace()
|
||||||
.map(|s| format!("{s}{suffix}"))
|
.map(|s| format!("{s}{suffix}"))
|
||||||
|
@ -145,15 +147,15 @@ fn append(_context: Context, suffix: &str, s: &str) -> Result<String, String> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn arch(_context: Context) -> Result<String, String> {
|
fn arch(_context: Context) -> FunctionResult {
|
||||||
Ok(target::arch().to_owned())
|
Ok(target::arch().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blake3(_context: Context, s: &str) -> Result<String, String> {
|
fn blake3(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(blake3::hash(s.as_bytes()).to_string())
|
Ok(blake3::hash(s.as_bytes()).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn blake3_file(context: Context, path: &str) -> Result<String, String> {
|
fn blake3_file(context: Context, path: &str) -> FunctionResult {
|
||||||
let path = context
|
let path = context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -167,7 +169,7 @@ fn blake3_file(context: Context, path: &str) -> Result<String, String> {
|
||||||
Ok(hasher.finalize().to_string())
|
Ok(hasher.finalize().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn canonicalize(_context: Context, path: &str) -> Result<String, String> {
|
fn canonicalize(_context: Context, path: &str) -> FunctionResult {
|
||||||
let canonical =
|
let canonical =
|
||||||
std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
|
std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
|
||||||
|
|
||||||
|
@ -179,7 +181,7 @@ fn canonicalize(_context: Context, path: &str) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn capitalize(_context: Context, s: &str) -> Result<String, String> {
|
fn capitalize(_context: Context, s: &str) -> FunctionResult {
|
||||||
let mut capitalized = String::new();
|
let mut capitalized = String::new();
|
||||||
for (i, c) in s.chars().enumerate() {
|
for (i, c) in s.chars().enumerate() {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
@ -191,7 +193,7 @@ fn capitalize(_context: Context, s: &str) -> Result<String, String> {
|
||||||
Ok(capitalized)
|
Ok(capitalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose(_context: Context, n: &str, alphabet: &str) -> Result<String, String> {
|
fn choose(_context: Context, n: &str, alphabet: &str) -> FunctionResult {
|
||||||
if alphabet.is_empty() {
|
if alphabet.is_empty() {
|
||||||
return Err("empty alphabet".into());
|
return Err("empty alphabet".into());
|
||||||
}
|
}
|
||||||
|
@ -215,11 +217,11 @@ fn choose(_context: Context, n: &str, alphabet: &str) -> Result<String, String>
|
||||||
Ok((0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect())
|
Ok((0..n).map(|_| alphabet.choose(&mut rng).unwrap()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clean(_context: Context, path: &str) -> Result<String, String> {
|
fn clean(_context: Context, path: &str) -> FunctionResult {
|
||||||
Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())
|
Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> Result<String, String> {
|
fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> FunctionResult {
|
||||||
match f() {
|
match f() {
|
||||||
Some(path) => path
|
Some(path) => path
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
|
@ -235,7 +237,15 @@ fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> Result<String, String>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_uri_component(_context: Context, s: &str) -> Result<String, String> {
|
fn datetime(_context: Context, format: &str) -> FunctionResult {
|
||||||
|
Ok(chrono::Local::now().format(format).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn datetime_utc(_context: Context, format: &str) -> FunctionResult {
|
||||||
|
Ok(chrono::Utc::now().format(format).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_uri_component(_context: Context, s: &str) -> FunctionResult {
|
||||||
static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
|
static PERCENT_ENCODE: percent_encoding::AsciiSet = percent_encoding::NON_ALPHANUMERIC
|
||||||
.remove(b'-')
|
.remove(b'-')
|
||||||
.remove(b'_')
|
.remove(b'_')
|
||||||
|
@ -249,7 +259,7 @@ fn encode_uri_component(_context: Context, s: &str) -> Result<String, String> {
|
||||||
Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string())
|
Ok(percent_encoding::utf8_percent_encode(s, &PERCENT_ENCODE).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_var(context: Context, key: &str) -> Result<String, String> {
|
fn env_var(context: Context, key: &str) -> FunctionResult {
|
||||||
use std::env::VarError::*;
|
use std::env::VarError::*;
|
||||||
|
|
||||||
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
||||||
|
@ -265,7 +275,7 @@ fn env_var(context: Context, key: &str) -> Result<String, String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_var_or_default(context: Context, key: &str, default: &str) -> Result<String, String> {
|
fn env_var_or_default(context: Context, key: &str, default: &str) -> FunctionResult {
|
||||||
use std::env::VarError::*;
|
use std::env::VarError::*;
|
||||||
|
|
||||||
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
||||||
|
@ -281,39 +291,39 @@ fn env_var_or_default(context: Context, key: &str, default: &str) -> Result<Stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env(context: Context, key: &str, default: Option<&str>) -> Result<String, String> {
|
fn env(context: Context, key: &str, default: Option<&str>) -> FunctionResult {
|
||||||
match default {
|
match default {
|
||||||
Some(val) => env_var_or_default(context, key, val),
|
Some(val) => env_var_or_default(context, key, val),
|
||||||
None => env_var(context, key),
|
None => env_var(context, key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error(_context: Context, message: &str) -> Result<String, String> {
|
fn error(_context: Context, message: &str) -> FunctionResult {
|
||||||
Err(message.to_owned())
|
Err(message.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extension(_context: Context, path: &str) -> Result<String, String> {
|
fn extension(_context: Context, path: &str) -> FunctionResult {
|
||||||
Utf8Path::new(path)
|
Utf8Path::new(path)
|
||||||
.extension()
|
.extension()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.ok_or_else(|| format!("Could not extract extension from `{path}`"))
|
.ok_or_else(|| format!("Could not extract extension from `{path}`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_name(_context: Context, path: &str) -> Result<String, String> {
|
fn file_name(_context: Context, path: &str) -> FunctionResult {
|
||||||
Utf8Path::new(path)
|
Utf8Path::new(path)
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.ok_or_else(|| format!("Could not extract file name from `{path}`"))
|
.ok_or_else(|| format!("Could not extract file name from `{path}`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_stem(_context: Context, path: &str) -> Result<String, String> {
|
fn file_stem(_context: Context, path: &str) -> FunctionResult {
|
||||||
Utf8Path::new(path)
|
Utf8Path::new(path)
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.ok_or_else(|| format!("Could not extract file stem from `{path}`"))
|
.ok_or_else(|| format!("Could not extract file stem from `{path}`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invocation_directory(context: Context) -> Result<String, String> {
|
fn invocation_directory(context: Context) -> FunctionResult {
|
||||||
Platform::convert_native_path(
|
Platform::convert_native_path(
|
||||||
&context.evaluator.context.search.working_directory,
|
&context.evaluator.context.search.working_directory,
|
||||||
&context.evaluator.context.config.invocation_directory,
|
&context.evaluator.context.config.invocation_directory,
|
||||||
|
@ -321,7 +331,7 @@ fn invocation_directory(context: Context) -> Result<String, String> {
|
||||||
.map_err(|e| format!("Error getting shell path: {e}"))
|
.map_err(|e| format!("Error getting shell path: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn invocation_directory_native(context: Context) -> Result<String, String> {
|
fn invocation_directory_native(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -342,11 +352,11 @@ fn invocation_directory_native(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_dependency(context: Context) -> Result<String, String> {
|
fn is_dependency(context: Context) -> FunctionResult {
|
||||||
Ok(context.evaluator.is_dependency.to_string())
|
Ok(context.evaluator.is_dependency.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepend(_context: Context, prefix: &str, s: &str) -> Result<String, String> {
|
fn prepend(_context: Context, prefix: &str, s: &str) -> FunctionResult {
|
||||||
Ok(
|
Ok(
|
||||||
s.split_whitespace()
|
s.split_whitespace()
|
||||||
.map(|s| format!("{prefix}{s}"))
|
.map(|s| format!("{prefix}{s}"))
|
||||||
|
@ -355,7 +365,7 @@ fn prepend(_context: Context, prefix: &str, s: &str) -> Result<String, String> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn join(_context: Context, base: &str, with: &str, and: &[String]) -> Result<String, String> {
|
fn join(_context: Context, base: &str, with: &str, and: &[String]) -> FunctionResult {
|
||||||
let mut result = Utf8Path::new(base).join(with);
|
let mut result = Utf8Path::new(base).join(with);
|
||||||
for arg in and {
|
for arg in and {
|
||||||
result.push(arg);
|
result.push(arg);
|
||||||
|
@ -363,7 +373,7 @@ fn join(_context: Context, base: &str, with: &str, and: &[String]) -> Result<Str
|
||||||
Ok(result.to_string())
|
Ok(result.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn just_executable(_context: Context) -> Result<String, String> {
|
fn just_executable(_context: Context) -> FunctionResult {
|
||||||
let exe_path =
|
let exe_path =
|
||||||
env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?;
|
env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?;
|
||||||
|
|
||||||
|
@ -375,11 +385,11 @@ fn just_executable(_context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn just_pid(_context: Context) -> Result<String, String> {
|
fn just_pid(_context: Context) -> FunctionResult {
|
||||||
Ok(std::process::id().to_string())
|
Ok(std::process::id().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn justfile(context: Context) -> Result<String, String> {
|
fn justfile(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -395,7 +405,7 @@ fn justfile(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn justfile_directory(context: Context) -> Result<String, String> {
|
fn justfile_directory(context: Context) -> FunctionResult {
|
||||||
let justfile_directory = context
|
let justfile_directory = context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -420,19 +430,19 @@ fn justfile_directory(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kebabcase(_context: Context, s: &str) -> Result<String, String> {
|
fn kebabcase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_kebab_case())
|
Ok(s.to_kebab_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lowercamelcase(_context: Context, s: &str) -> Result<String, String> {
|
fn lowercamelcase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_lower_camel_case())
|
Ok(s.to_lower_camel_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lowercase(_context: Context, s: &str) -> Result<String, String> {
|
fn lowercase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_lowercase())
|
Ok(s.to_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn module_directory(context: Context) -> Result<String, String> {
|
fn module_directory(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -459,7 +469,7 @@ fn module_directory(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn module_file(context: Context) -> Result<String, String> {
|
fn module_file(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -478,27 +488,27 @@ fn module_file(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn num_cpus(_context: Context) -> Result<String, String> {
|
fn num_cpus(_context: Context) -> FunctionResult {
|
||||||
let num = num_cpus::get();
|
let num = num_cpus::get();
|
||||||
Ok(num.to_string())
|
Ok(num.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn os(_context: Context) -> Result<String, String> {
|
fn os(_context: Context) -> FunctionResult {
|
||||||
Ok(target::os().to_owned())
|
Ok(target::os().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn os_family(_context: Context) -> Result<String, String> {
|
fn os_family(_context: Context) -> FunctionResult {
|
||||||
Ok(target::family().to_owned())
|
Ok(target::family().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parent_directory(_context: Context, path: &str) -> Result<String, String> {
|
fn parent_directory(_context: Context, path: &str) -> FunctionResult {
|
||||||
Utf8Path::new(path)
|
Utf8Path::new(path)
|
||||||
.parent()
|
.parent()
|
||||||
.map(Utf8Path::to_string)
|
.map(Utf8Path::to_string)
|
||||||
.ok_or_else(|| format!("Could not extract parent directory from `{path}`"))
|
.ok_or_else(|| format!("Could not extract parent directory from `{path}`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path_exists(context: Context, path: &str) -> Result<String, String> {
|
fn path_exists(context: Context, path: &str) -> FunctionResult {
|
||||||
Ok(
|
Ok(
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
|
@ -511,20 +521,15 @@ fn path_exists(context: Context, path: &str) -> Result<String, String> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quote(_context: Context, s: &str) -> Result<String, String> {
|
fn quote(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(format!("'{}'", s.replace('\'', "'\\''")))
|
Ok(format!("'{}'", s.replace('\'', "'\\''")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace(_context: Context, s: &str, from: &str, to: &str) -> Result<String, String> {
|
fn replace(_context: Context, s: &str, from: &str, to: &str) -> FunctionResult {
|
||||||
Ok(s.replace(from, to))
|
Ok(s.replace(from, to))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn replace_regex(
|
fn replace_regex(_context: Context, s: &str, regex: &str, replacement: &str) -> FunctionResult {
|
||||||
_context: Context,
|
|
||||||
s: &str,
|
|
||||||
regex: &str,
|
|
||||||
replacement: &str,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
Ok(
|
Ok(
|
||||||
Regex::new(regex)
|
Regex::new(regex)
|
||||||
.map_err(|err| err.to_string())?
|
.map_err(|err| err.to_string())?
|
||||||
|
@ -533,7 +538,7 @@ fn replace_regex(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sha256(_context: Context, s: &str) -> Result<String, String> {
|
fn sha256(_context: Context, s: &str) -> FunctionResult {
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(s);
|
hasher.update(s);
|
||||||
|
@ -541,7 +546,7 @@ fn sha256(_context: Context, s: &str) -> Result<String, String> {
|
||||||
Ok(format!("{hash:x}"))
|
Ok(format!("{hash:x}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sha256_file(context: Context, path: &str) -> Result<String, String> {
|
fn sha256_file(context: Context, path: &str) -> FunctionResult {
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
let path = context
|
let path = context
|
||||||
.evaluator
|
.evaluator
|
||||||
|
@ -558,7 +563,7 @@ fn sha256_file(context: Context, path: &str) -> Result<String, String> {
|
||||||
Ok(format!("{hash:x}"))
|
Ok(format!("{hash:x}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shell(context: Context, command: &str, args: &[String]) -> Result<String, String> {
|
fn shell(context: Context, command: &str, args: &[String]) -> FunctionResult {
|
||||||
let args = iter::once(command)
|
let args = iter::once(command)
|
||||||
.chain(args.iter().map(String::as_str))
|
.chain(args.iter().map(String::as_str))
|
||||||
.collect::<Vec<&str>>();
|
.collect::<Vec<&str>>();
|
||||||
|
@ -569,19 +574,19 @@ fn shell(context: Context, command: &str, args: &[String]) -> Result<String, Str
|
||||||
.map_err(|output_error| output_error.to_string())
|
.map_err(|output_error| output_error.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shoutykebabcase(_context: Context, s: &str) -> Result<String, String> {
|
fn shoutykebabcase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_shouty_kebab_case())
|
Ok(s.to_shouty_kebab_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn shoutysnakecase(_context: Context, s: &str) -> Result<String, String> {
|
fn shoutysnakecase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_shouty_snake_case())
|
Ok(s.to_shouty_snake_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn snakecase(_context: Context, s: &str) -> Result<String, String> {
|
fn snakecase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_snake_case())
|
Ok(s.to_snake_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_directory(context: Context) -> Result<String, String> {
|
fn source_directory(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -602,7 +607,7 @@ fn source_directory(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_file(context: Context) -> Result<String, String> {
|
fn source_file(context: Context) -> FunctionResult {
|
||||||
context
|
context
|
||||||
.evaluator
|
.evaluator
|
||||||
.context
|
.context
|
||||||
|
@ -621,51 +626,51 @@ fn source_file(context: Context) -> Result<String, String> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn titlecase(_context: Context, s: &str) -> Result<String, String> {
|
fn titlecase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_title_case())
|
Ok(s.to_title_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim(_context: Context, s: &str) -> Result<String, String> {
|
fn trim(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.trim().to_owned())
|
Ok(s.trim().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_end(_context: Context, s: &str) -> Result<String, String> {
|
fn trim_end(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.trim_end().to_owned())
|
Ok(s.trim_end().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_end_match(_context: Context, s: &str, pat: &str) -> Result<String, String> {
|
fn trim_end_match(_context: Context, s: &str, pat: &str) -> FunctionResult {
|
||||||
Ok(s.strip_suffix(pat).unwrap_or(s).to_owned())
|
Ok(s.strip_suffix(pat).unwrap_or(s).to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_end_matches(_context: Context, s: &str, pat: &str) -> Result<String, String> {
|
fn trim_end_matches(_context: Context, s: &str, pat: &str) -> FunctionResult {
|
||||||
Ok(s.trim_end_matches(pat).to_owned())
|
Ok(s.trim_end_matches(pat).to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_start(_context: Context, s: &str) -> Result<String, String> {
|
fn trim_start(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.trim_start().to_owned())
|
Ok(s.trim_start().to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_start_match(_context: Context, s: &str, pat: &str) -> Result<String, String> {
|
fn trim_start_match(_context: Context, s: &str, pat: &str) -> FunctionResult {
|
||||||
Ok(s.strip_prefix(pat).unwrap_or(s).to_owned())
|
Ok(s.strip_prefix(pat).unwrap_or(s).to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn trim_start_matches(_context: Context, s: &str, pat: &str) -> Result<String, String> {
|
fn trim_start_matches(_context: Context, s: &str, pat: &str) -> FunctionResult {
|
||||||
Ok(s.trim_start_matches(pat).to_owned())
|
Ok(s.trim_start_matches(pat).to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uppercamelcase(_context: Context, s: &str) -> Result<String, String> {
|
fn uppercamelcase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_upper_camel_case())
|
Ok(s.to_upper_camel_case())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uppercase(_context: Context, s: &str) -> Result<String, String> {
|
fn uppercase(_context: Context, s: &str) -> FunctionResult {
|
||||||
Ok(s.to_uppercase())
|
Ok(s.to_uppercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn uuid(_context: Context) -> Result<String, String> {
|
fn uuid(_context: Context) -> FunctionResult {
|
||||||
Ok(uuid::Uuid::new_v4().to_string())
|
Ok(uuid::Uuid::new_v4().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn without_extension(_context: Context, path: &str) -> Result<String, String> {
|
fn without_extension(_context: Context, path: &str) -> FunctionResult {
|
||||||
let parent = Utf8Path::new(path)
|
let parent = Utf8Path::new(path)
|
||||||
.parent()
|
.parent()
|
||||||
.ok_or_else(|| format!("Could not extract parent from `{path}`"))?;
|
.ok_or_else(|| format!("Could not extract parent from `{path}`"))?;
|
||||||
|
@ -679,7 +684,7 @@ fn without_extension(_context: Context, path: &str) -> Result<String, String> {
|
||||||
|
|
||||||
/// Check whether a string processes properly as semver (e.x. "0.1.0")
|
/// Check whether a string processes properly as semver (e.x. "0.1.0")
|
||||||
/// and matches a given semver requirement (e.x. ">=0.1.0")
|
/// and matches a given semver requirement (e.x. ">=0.1.0")
|
||||||
fn semver_matches(_context: Context, version: &str, requirement: &str) -> Result<String, String> {
|
fn semver_matches(_context: Context, version: &str, requirement: &str) -> FunctionResult {
|
||||||
Ok(
|
Ok(
|
||||||
requirement
|
requirement
|
||||||
.parse::<VersionReq>()
|
.parse::<VersionReq>()
|
||||||
|
|
244
src/justfile.rs
244
src/justfile.rs
|
@ -173,66 +173,26 @@ impl<'src> Justfile<'src> {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut remaining: Vec<&str> = if !arguments.is_empty() {
|
let arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
|
||||||
arguments.iter().map(String::as_str).collect()
|
|
||||||
} else if let Some(recipe) = &self.default {
|
let groups = ArgumentParser::parse_arguments(self, &arguments)?;
|
||||||
recipe.check_can_be_default_recipe()?;
|
|
||||||
vec![recipe.name()]
|
|
||||||
} else if self.recipes.is_empty() {
|
|
||||||
return Err(Error::NoRecipes);
|
|
||||||
} else {
|
|
||||||
return Err(Error::NoDefaultRecipe);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut missing = Vec::new();
|
|
||||||
let mut invocations = Vec::new();
|
|
||||||
let mut scopes = BTreeMap::new();
|
|
||||||
let arena: Arena<Scope> = Arena::new();
|
let arena: Arena<Scope> = Arena::new();
|
||||||
|
let mut invocations = Vec::<Invocation>::new();
|
||||||
|
let mut scopes = BTreeMap::new();
|
||||||
|
|
||||||
while let Some(first) = remaining.first().copied() {
|
for group in &groups {
|
||||||
if first.contains("::")
|
invocations.push(self.invocation(
|
||||||
&& !(first.starts_with(':') || first.ends_with(':') || first.contains(":::"))
|
|
||||||
{
|
|
||||||
remaining = first
|
|
||||||
.split("::")
|
|
||||||
.chain(remaining[1..].iter().copied())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rest = &remaining[1..];
|
|
||||||
|
|
||||||
if let Some((invocation, consumed)) = self.invocation(
|
|
||||||
0,
|
|
||||||
&mut Vec::new(),
|
|
||||||
&arena,
|
&arena,
|
||||||
&mut scopes,
|
&group.arguments,
|
||||||
config,
|
config,
|
||||||
&dotenv,
|
&dotenv,
|
||||||
search,
|
|
||||||
&scope,
|
&scope,
|
||||||
first,
|
&group.path,
|
||||||
rest,
|
0,
|
||||||
)? {
|
&mut scopes,
|
||||||
remaining = rest[consumed..].to_vec();
|
search,
|
||||||
invocations.push(invocation);
|
)?);
|
||||||
} else {
|
|
||||||
missing.push(first.to_string());
|
|
||||||
remaining = rest.to_vec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
|
||||||
let suggestion = if missing.len() == 1 {
|
|
||||||
self.suggest_recipe(missing.first().unwrap())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
return Err(Error::UnknownRecipes {
|
|
||||||
recipes: missing,
|
|
||||||
suggestion,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ran = Ran::default();
|
let mut ran = Ran::default();
|
||||||
|
@ -278,21 +238,29 @@ impl<'src> Justfile<'src> {
|
||||||
|
|
||||||
fn invocation<'run>(
|
fn invocation<'run>(
|
||||||
&'run self,
|
&'run self,
|
||||||
depth: usize,
|
|
||||||
path: &mut Vec<&'run str>,
|
|
||||||
arena: &'run Arena<Scope<'src, 'run>>,
|
arena: &'run Arena<Scope<'src, 'run>>,
|
||||||
scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
|
arguments: &[&'run str],
|
||||||
config: &'run Config,
|
config: &'run Config,
|
||||||
dotenv: &'run BTreeMap<String, String>,
|
dotenv: &'run BTreeMap<String, String>,
|
||||||
search: &'run Search,
|
|
||||||
parent: &'run Scope<'src, 'run>,
|
parent: &'run Scope<'src, 'run>,
|
||||||
first: &'run str,
|
path: &'run [String],
|
||||||
rest: &[&'run str],
|
position: usize,
|
||||||
) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
|
scopes: &mut BTreeMap<&'run [String], &'run Scope<'src, 'run>>,
|
||||||
if let Some(module) = self.modules.get(first) {
|
search: &'run Search,
|
||||||
path.push(first);
|
) -> RunResult<'src, Invocation<'src, 'run>> {
|
||||||
|
if position + 1 == path.len() {
|
||||||
|
let recipe = self.get_recipe(&path[position]).unwrap();
|
||||||
|
Ok(Invocation {
|
||||||
|
recipe,
|
||||||
|
module_source: &self.source,
|
||||||
|
arguments: arguments.into(),
|
||||||
|
settings: &self.settings,
|
||||||
|
scope: parent,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let module = self.modules.get(&path[position]).unwrap();
|
||||||
|
|
||||||
let scope = if let Some(scope) = scopes.get(path) {
|
let scope = if let Some(scope) = scopes.get(&path[..position]) {
|
||||||
scope
|
scope
|
||||||
} else {
|
} else {
|
||||||
let scope = Evaluator::evaluate_assignments(
|
let scope = Evaluator::evaluate_assignments(
|
||||||
|
@ -304,76 +272,21 @@ impl<'src> Justfile<'src> {
|
||||||
search,
|
search,
|
||||||
)?;
|
)?;
|
||||||
let scope = arena.alloc(scope);
|
let scope = arena.alloc(scope);
|
||||||
scopes.insert(path.clone(), scope);
|
scopes.insert(path, scope);
|
||||||
scopes.get(path).unwrap()
|
scopes.get(path).unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
if rest.is_empty() {
|
module.invocation(
|
||||||
if let Some(recipe) = &module.default {
|
arena,
|
||||||
recipe.check_can_be_default_recipe()?;
|
arguments,
|
||||||
return Ok(Some((
|
config,
|
||||||
Invocation {
|
dotenv,
|
||||||
settings: &module.settings,
|
scope,
|
||||||
recipe,
|
path,
|
||||||
arguments: Vec::new(),
|
position + 1,
|
||||||
scope,
|
scopes,
|
||||||
module_source: &self.source,
|
search,
|
||||||
},
|
)
|
||||||
depth,
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Err(Error::NoDefaultRecipe)
|
|
||||||
} else {
|
|
||||||
module.invocation(
|
|
||||||
depth + 1,
|
|
||||||
path,
|
|
||||||
arena,
|
|
||||||
scopes,
|
|
||||||
config,
|
|
||||||
dotenv,
|
|
||||||
search,
|
|
||||||
scope,
|
|
||||||
rest[0],
|
|
||||||
&rest[1..],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else if let Some(recipe) = self.get_recipe(first) {
|
|
||||||
if recipe.parameters.is_empty() {
|
|
||||||
Ok(Some((
|
|
||||||
Invocation {
|
|
||||||
arguments: Vec::new(),
|
|
||||||
recipe,
|
|
||||||
scope: parent,
|
|
||||||
settings: &self.settings,
|
|
||||||
module_source: &self.source,
|
|
||||||
},
|
|
||||||
depth,
|
|
||||||
)))
|
|
||||||
} else {
|
|
||||||
let argument_range = recipe.argument_range();
|
|
||||||
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
|
||||||
if !argument_range.range_contains(&argument_count) {
|
|
||||||
return Err(Error::ArgumentCountMismatch {
|
|
||||||
recipe: recipe.name(),
|
|
||||||
parameters: recipe.parameters.clone(),
|
|
||||||
found: rest.len(),
|
|
||||||
min: recipe.min_arguments(),
|
|
||||||
max: recipe.max_arguments(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Some((
|
|
||||||
Invocation {
|
|
||||||
arguments: rest[..argument_count].to_vec(),
|
|
||||||
recipe,
|
|
||||||
scope: parent,
|
|
||||||
settings: &self.settings,
|
|
||||||
module_source: &self.source,
|
|
||||||
},
|
|
||||||
depth + argument_count,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,13 +365,13 @@ impl<'src> Justfile<'src> {
|
||||||
modules
|
modules
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe<'src, Dependency>> {
|
pub(crate) fn public_recipes(&self, config: &Config) -> Vec<&Recipe> {
|
||||||
let mut recipes = self
|
let mut recipes = self
|
||||||
.recipes
|
.recipes
|
||||||
.values()
|
.values()
|
||||||
.map(AsRef::as_ref)
|
.map(AsRef::as_ref)
|
||||||
.filter(|recipe| recipe.is_public())
|
.filter(|recipe| recipe.is_public())
|
||||||
.collect::<Vec<&Recipe<Dependency>>>();
|
.collect::<Vec<&Recipe>>();
|
||||||
|
|
||||||
if config.unsorted {
|
if config.unsorted {
|
||||||
recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
|
recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
|
||||||
|
@ -467,19 +380,33 @@ impl<'src> Justfile<'src> {
|
||||||
recipes
|
recipes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn public_groups(&self) -> BTreeSet<String> {
|
pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {
|
||||||
self
|
let mut groups = Vec::new();
|
||||||
.recipes
|
|
||||||
.values()
|
for recipe in self.recipes.values() {
|
||||||
.map(AsRef::as_ref)
|
if recipe.is_public() {
|
||||||
.filter(|recipe| recipe.is_public())
|
for group in recipe.groups() {
|
||||||
.flat_map(Recipe::groups)
|
groups.push((&recipe.import_offsets, recipe.name.offset, group));
|
||||||
.collect()
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.unsorted {
|
||||||
|
groups.sort();
|
||||||
|
} else {
|
||||||
|
groups.sort_by(|(_, _, a), (_, _, b)| a.cmp(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
|
||||||
|
groups.retain(|(_, _, group)| seen.insert(group.clone()));
|
||||||
|
|
||||||
|
groups.into_iter().map(|(_, _, group)| group).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> ColorDisplay for Justfile<'src> {
|
impl<'src> ColorDisplay for Justfile<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||||
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
|
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
|
||||||
for (name, assignment) in &self.assignments {
|
for (name, assignment) in &self.assignments {
|
||||||
if assignment.export {
|
if assignment.export {
|
||||||
|
@ -523,21 +450,38 @@ mod tests {
|
||||||
use Error::*;
|
use Error::*;
|
||||||
|
|
||||||
run_error! {
|
run_error! {
|
||||||
name: unknown_recipes,
|
name: unknown_recipe_no_suggestion,
|
||||||
src: "a:\nb:\nc:",
|
src: "a:\nb:\nc:",
|
||||||
args: ["a", "x", "y", "z"],
|
args: ["a", "xyz", "y", "z"],
|
||||||
error: UnknownRecipes {
|
error: UnknownRecipe {
|
||||||
recipes,
|
recipe,
|
||||||
suggestion,
|
suggestion,
|
||||||
},
|
},
|
||||||
check: {
|
check: {
|
||||||
assert_eq!(recipes, &["x", "y", "z"]);
|
assert_eq!(recipe, "xyz");
|
||||||
assert_eq!(suggestion, None);
|
assert_eq!(suggestion, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run_error! {
|
run_error! {
|
||||||
name: unknown_recipes_show_alias_suggestion,
|
name: unknown_recipe_with_suggestion,
|
||||||
|
src: "a:\nb:\nc:",
|
||||||
|
args: ["a", "x", "y", "z"],
|
||||||
|
error: UnknownRecipe {
|
||||||
|
recipe,
|
||||||
|
suggestion,
|
||||||
|
},
|
||||||
|
check: {
|
||||||
|
assert_eq!(recipe, "x");
|
||||||
|
assert_eq!(suggestion, Some(Suggestion {
|
||||||
|
name: "a",
|
||||||
|
target: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run_error! {
|
||||||
|
name: unknown_recipe_show_alias_suggestion,
|
||||||
src: "
|
src: "
|
||||||
foo:
|
foo:
|
||||||
echo foo
|
echo foo
|
||||||
|
@ -545,12 +489,12 @@ mod tests {
|
||||||
alias z := foo
|
alias z := foo
|
||||||
",
|
",
|
||||||
args: ["zz"],
|
args: ["zz"],
|
||||||
error: UnknownRecipes {
|
error: UnknownRecipe {
|
||||||
recipes,
|
recipe,
|
||||||
suggestion,
|
suggestion,
|
||||||
},
|
},
|
||||||
check: {
|
check: {
|
||||||
assert_eq!(recipes, &["zz"]);
|
assert_eq!(recipe, "zz");
|
||||||
assert_eq!(suggestion, Some(Suggestion {
|
assert_eq!(suggestion, Some(Suggestion {
|
||||||
name: "z",
|
name: "z",
|
||||||
target: Some("foo"),
|
target: Some("foo"),
|
||||||
|
|
18
src/lib.rs
18
src/lib.rs
|
@ -13,9 +13,15 @@
|
||||||
overlapping_range_endpoints
|
overlapping_range_endpoints
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
//! `just` is primarily used as a command-line binary, but does provide a
|
||||||
|
//! limited public library interface.
|
||||||
|
//!
|
||||||
|
//! Please keep in mind that there are no semantic version guarantees for the
|
||||||
|
//! library interface. It may break or change at any time.
|
||||||
|
|
||||||
pub(crate) use {
|
pub(crate) use {
|
||||||
crate::{
|
crate::{
|
||||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment,
|
||||||
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
||||||
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||||
|
@ -86,10 +92,11 @@ pub use crate::run::run;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use unindent::unindent;
|
pub use unindent::unindent;
|
||||||
|
|
||||||
pub(crate) type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
|
type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
|
||||||
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
type ConfigResult<T> = Result<T, ConfigError>;
|
||||||
pub(crate) type RunResult<'a, T = ()> = Result<T, Error<'a>>;
|
type FunctionResult = Result<String, String>;
|
||||||
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
type RunResult<'a, T = ()> = Result<T, Error<'a>>;
|
||||||
|
type SearchResult<T> = Result<T, SearchError>;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
@ -113,6 +120,7 @@ pub mod summary;
|
||||||
|
|
||||||
mod alias;
|
mod alias;
|
||||||
mod analyzer;
|
mod analyzer;
|
||||||
|
mod argument_parser;
|
||||||
mod assignment;
|
mod assignment;
|
||||||
mod assignment_resolver;
|
mod assignment_resolver;
|
||||||
mod ast;
|
mod ast;
|
||||||
|
|
|
@ -24,8 +24,9 @@ pub(crate) fn load_dotenv(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(path) = dotenv_path {
|
if let Some(path) = dotenv_path {
|
||||||
|
let path = working_directory.join(path);
|
||||||
if path.is_file() {
|
if path.is_file() {
|
||||||
return load_from_file(&working_directory.join(path));
|
return load_from_file(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(code) = just::run() {
|
if let Err(code) = just::run(std::env::args_os()) {
|
||||||
std::process::exit(code);
|
std::process::exit(code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub(crate) enum OutputError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for OutputError {
|
impl Display for OutputError {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match *self {
|
match *self {
|
||||||
Self::Code(code) => write!(f, "Process exited with status code {code}"),
|
Self::Code(code) => write!(f, "Process exited with status code {code}"),
|
||||||
Self::Io(ref io_error) => write!(f, "Error executing process: {io_error}"),
|
Self::Io(ref io_error) => write!(f, "Error executing process: {io_error}"),
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub(crate) struct Parameter<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> ColorDisplay for Parameter<'src> {
|
impl<'src> ColorDisplay for Parameter<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||||
if let Some(prefix) = self.kind.prefix() {
|
if let Some(prefix) = self.kind.prefix() {
|
||||||
write!(f, "{}", color.annotation().paint(prefix))?;
|
write!(f, "{}", color.annotation().paint(prefix))?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -340,9 +340,13 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||||
self.presume_keyword(Keyword::Export)?;
|
self.presume_keyword(Keyword::Export)?;
|
||||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||||
}
|
}
|
||||||
Some(Keyword::Unexport) => {
|
Some(Keyword::Unexport)
|
||||||
|
if self.next_are(&[Identifier, Identifier, Eof])
|
||||||
|
|| self.next_are(&[Identifier, Identifier, Eol]) =>
|
||||||
|
{
|
||||||
self.presume_keyword(Keyword::Unexport)?;
|
self.presume_keyword(Keyword::Unexport)?;
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
|
self.expect_eol()?;
|
||||||
items.push(Item::Unexport { name });
|
items.push(Item::Unexport { name });
|
||||||
}
|
}
|
||||||
Some(Keyword::Import)
|
Some(Keyword::Import)
|
||||||
|
|
|
@ -19,7 +19,7 @@ impl PlatformInterface for Platform {
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_execute_permission(path: &Path) -> Result<(), io::Error> {
|
fn set_execute_permission(path: &Path) -> io::Result<()> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
// get current permissions
|
// get current permissions
|
||||||
|
@ -38,7 +38,7 @@ impl PlatformInterface for Platform {
|
||||||
exit_status.signal()
|
exit_status.signal()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_native_path(_working_directory: &Path, path: &Path) -> Result<String, String> {
|
fn convert_native_path(_working_directory: &Path, path: &Path) -> FunctionResult {
|
||||||
path
|
path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
|
@ -85,7 +85,7 @@ impl PlatformInterface for Platform {
|
||||||
Ok(cmd)
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_execute_permission(_path: &Path) -> Result<(), io::Error> {
|
fn set_execute_permission(_path: &Path) -> io::Result<()> {
|
||||||
// it is not necessary to set an execute permission on a script on windows, so
|
// it is not necessary to set an execute permission on a script on windows, so
|
||||||
// this is a nop
|
// this is a nop
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -97,7 +97,7 @@ impl PlatformInterface for Platform {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_native_path(working_directory: &Path, path: &Path) -> Result<String, String> {
|
fn convert_native_path(working_directory: &Path, path: &Path) -> FunctionResult {
|
||||||
// Translate path from windows style to unix style
|
// Translate path from windows style to unix style
|
||||||
let mut cygpath = Command::new("cygpath");
|
let mut cygpath = Command::new("cygpath");
|
||||||
cygpath.current_dir(working_directory);
|
cygpath.current_dir(working_directory);
|
||||||
|
|
|
@ -10,12 +10,12 @@ pub(crate) trait PlatformInterface {
|
||||||
) -> Result<Command, OutputError>;
|
) -> Result<Command, OutputError>;
|
||||||
|
|
||||||
/// Set the execute permission on the file pointed to by `path`
|
/// Set the execute permission on the file pointed to by `path`
|
||||||
fn set_execute_permission(path: &Path) -> Result<(), io::Error>;
|
fn set_execute_permission(path: &Path) -> io::Result<()>;
|
||||||
|
|
||||||
/// Extract the signal from a process exit status, if it was terminated by a
|
/// Extract the signal from a process exit status, if it was terminated by a
|
||||||
/// signal
|
/// signal
|
||||||
fn signal_from_exit_status(exit_status: ExitStatus) -> Option<i32>;
|
fn signal_from_exit_status(exit_status: ExitStatus) -> Option<i32>;
|
||||||
|
|
||||||
/// Translate a path from a "native" path to a path the interpreter expects
|
/// Translate a path from a "native" path to a path the interpreter expects
|
||||||
fn convert_native_path(working_directory: &Path, path: &Path) -> Result<String, String>;
|
fn convert_native_path(working_directory: &Path, path: &Path) -> FunctionResult;
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,10 @@ impl<'src, D> Recipe<'src, D> {
|
||||||
!self.private && !self.attributes.contains(&Attribute::Private)
|
!self.private && !self.attributes.contains(&Attribute::Private)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn takes_positional_arguments(&self, settings: &Settings) -> bool {
|
||||||
|
settings.positional_arguments || self.attributes.contains(&Attribute::PositionalArguments)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn change_directory(&self) -> bool {
|
pub(crate) fn change_directory(&self) -> bool {
|
||||||
!self.attributes.contains(&Attribute::NoCd)
|
!self.attributes.contains(&Attribute::NoCd)
|
||||||
}
|
}
|
||||||
|
@ -263,7 +267,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||||
|
|
||||||
cmd.arg(command);
|
cmd.arg(command);
|
||||||
|
|
||||||
if context.settings.positional_arguments {
|
if self.takes_positional_arguments(context.settings) {
|
||||||
cmd.arg(self.name.lexeme());
|
cmd.arg(self.name.lexeme());
|
||||||
cmd.args(positional);
|
cmd.args(positional);
|
||||||
}
|
}
|
||||||
|
@ -415,7 +419,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||||
output_error,
|
output_error,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if context.settings.positional_arguments {
|
if self.takes_positional_arguments(context.settings) {
|
||||||
command.args(positional);
|
command.args(positional);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -472,7 +476,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
impl<'src, D: Display> ColorDisplay for Recipe<'src, D> {
|
||||||
fn fmt(&self, f: &mut Formatter, color: Color) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
||||||
if let Some(doc) = self.doc {
|
if let Some(doc) = self.doc {
|
||||||
writeln!(f, "# {doc}")?;
|
writeln!(f, "# {doc}")?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,9 @@ pub(crate) struct RecipeResolver<'src: 'run, 'run> {
|
||||||
|
|
||||||
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
||||||
pub(crate) fn resolve_recipes(
|
pub(crate) fn resolve_recipes(
|
||||||
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
|
||||||
assignments: &'run Table<'src, Assignment<'src>>,
|
assignments: &'run Table<'src, Assignment<'src>>,
|
||||||
|
settings: &Settings,
|
||||||
|
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||||
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
||||||
let mut resolver = Self {
|
let mut resolver = Self {
|
||||||
resolved_recipes: Table::new(),
|
resolved_recipes: Table::new(),
|
||||||
|
@ -39,6 +40,10 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
||||||
}
|
}
|
||||||
|
|
||||||
for line in &recipe.body {
|
for line in &recipe.body {
|
||||||
|
if line.is_comment() && settings.ignore_comments {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for fragment in &line.fragments {
|
for fragment in &line.fragments {
|
||||||
if let Fragment::Interpolation { expression, .. } = fragment {
|
if let Fragment::Interpolation { expression, .. } = fragment {
|
||||||
for variable in expression.variables() {
|
for variable in expression.variables() {
|
||||||
|
|
12
src/run.rs
12
src/run.rs
|
@ -1,8 +1,8 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Main entry point into just binary.
|
/// Main entry point into `just`. Parse arguments from `args` and run.
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub fn run() -> Result<(), i32> {
|
pub fn run(args: impl Iterator<Item = impl Into<OsString> + Clone>) -> Result<(), i32> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
ansi_term::enable_ansi_support().ok();
|
ansi_term::enable_ansi_support().ok();
|
||||||
|
|
||||||
|
@ -11,12 +11,16 @@ pub fn run() -> Result<(), i32> {
|
||||||
.filter("JUST_LOG")
|
.filter("JUST_LOG")
|
||||||
.write_style("JUST_LOG_STYLE"),
|
.write_style("JUST_LOG_STYLE"),
|
||||||
)
|
)
|
||||||
.init();
|
.try_init()
|
||||||
|
.ok();
|
||||||
|
|
||||||
let app = Config::app();
|
let app = Config::app();
|
||||||
|
|
||||||
info!("Parsing command line arguments…");
|
info!("Parsing command line arguments…");
|
||||||
let matches = app.get_matches();
|
let matches = app.try_get_matches_from(args).map_err(|err| {
|
||||||
|
err.print().ok();
|
||||||
|
err.exit_code()
|
||||||
|
})?;
|
||||||
|
|
||||||
let config = Config::from_matches(&matches).map_err(Error::from);
|
let config = Config::from_matches(&matches).map_err(Error::from);
|
||||||
|
|
||||||
|
|
178
src/search.rs
178
src/search.rs
|
@ -1,14 +1,58 @@
|
||||||
use {super::*, std::path::Component};
|
use {
|
||||||
|
super::*,
|
||||||
|
std::{
|
||||||
|
io::{stdin, Read},
|
||||||
|
path::{Component, Display},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];
|
const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];
|
||||||
pub(crate) const JUSTFILE_NAMES: [&str; 2] = ["justfile", ".justfile"];
|
pub(crate) const JUSTFILE_NAMES: [&str; 2] = ["justfile", ".justfile"];
|
||||||
const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
|
const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
|
||||||
|
|
||||||
pub(crate) struct Search {
|
pub(crate) struct Search {
|
||||||
pub(crate) justfile: PathBuf,
|
pub(crate) justfile: JustfileKind,
|
||||||
pub(crate) working_directory: PathBuf,
|
pub(crate) working_directory: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum JustfileKind {
|
||||||
|
Path { path: PathBuf },
|
||||||
|
Stdin { data: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JustfileKind {
|
||||||
|
pub fn to_str(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
JustfileKind::Path { path } => path.to_str(),
|
||||||
|
JustfileKind::Stdin { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&self) -> Display<'_> {
|
||||||
|
match self {
|
||||||
|
JustfileKind::Path { path } => path.display(),
|
||||||
|
JustfileKind::Stdin { .. } => Path::new("<STDIN>").display(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self) -> Option<&Path> {
|
||||||
|
match self {
|
||||||
|
JustfileKind::Path { path } => path.parent(),
|
||||||
|
JustfileKind::Stdin { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq<PathBuf> for JustfileKind {
|
||||||
|
fn eq(&self, other: &PathBuf) -> bool {
|
||||||
|
match self {
|
||||||
|
JustfileKind::Path { path } => path == other,
|
||||||
|
JustfileKind::Stdin { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Search {
|
impl Search {
|
||||||
fn global_justfile_paths() -> Vec<PathBuf> {
|
fn global_justfile_paths() -> Vec<PathBuf> {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
|
@ -33,6 +77,7 @@ impl Search {
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search for a Justfile
|
||||||
pub(crate) fn find(
|
pub(crate) fn find(
|
||||||
search_config: &SearchConfig,
|
search_config: &SearchConfig,
|
||||||
invocation_directory: &Path,
|
invocation_directory: &Path,
|
||||||
|
@ -41,24 +86,29 @@ impl Search {
|
||||||
SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory),
|
SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory),
|
||||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||||
let search_directory = Self::clean(invocation_directory, search_directory);
|
let search_directory = Self::clean(invocation_directory, search_directory);
|
||||||
let justfile = Self::justfile(&search_directory)?;
|
let path = Self::justfile(&search_directory)?;
|
||||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
let working_directory = Self::working_directory_from_justfile(&path)?;
|
||||||
|
let justfile = JustfileKind::Path { path: path.clone() };
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SearchConfig::GlobalJustfile => Ok(Self {
|
SearchConfig::GlobalJustfile => {
|
||||||
justfile: Self::global_justfile_paths()
|
let path = Self::global_justfile_paths()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|path| path.exists())
|
.find(|path| path.exists())
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or(SearchError::GlobalJustfileNotFound)?,
|
.ok_or(SearchError::GlobalJustfileNotFound)?;
|
||||||
working_directory: Self::project_root(invocation_directory)?,
|
Ok(Self {
|
||||||
}),
|
justfile: JustfileKind::Path { path },
|
||||||
|
working_directory: Self::project_root(invocation_directory)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
SearchConfig::WithJustfile { justfile } => {
|
SearchConfig::WithJustfile { justfile } => {
|
||||||
let justfile = Self::clean(invocation_directory, justfile);
|
let path = Self::clean(invocation_directory, justfile);
|
||||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
let justfile = JustfileKind::Path { path: path.clone() };
|
||||||
|
let working_directory = Self::working_directory_from_justfile(&path)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
@ -67,22 +117,58 @@ impl Search {
|
||||||
SearchConfig::WithJustfileAndWorkingDirectory {
|
SearchConfig::WithJustfileAndWorkingDirectory {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
} => Ok(Self {
|
} => {
|
||||||
justfile: Self::clean(invocation_directory, justfile),
|
let path = Self::clean(invocation_directory, justfile);
|
||||||
working_directory: Self::clean(invocation_directory, working_directory),
|
let justfile = JustfileKind::Path { path };
|
||||||
}),
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory: Self::clean(invocation_directory, working_directory),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SearchConfig::WithStdin => {
|
||||||
|
let working_directory = PathBuf::from(invocation_directory);
|
||||||
|
let mut data = String::new();
|
||||||
|
if let Err(err) = stdin().read_to_string(&mut data) {
|
||||||
|
return Err(SearchError::Io {
|
||||||
|
directory: working_directory,
|
||||||
|
io_error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let justfile = JustfileKind::Stdin { data };
|
||||||
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
SearchConfig::WithStdinAndWorkingDirectory { working_directory } => {
|
||||||
|
let working_directory = working_directory.to_owned();
|
||||||
|
let mut data = String::new();
|
||||||
|
if let Err(err) = stdin().read_to_string(&mut data) {
|
||||||
|
return Err(SearchError::Io {
|
||||||
|
directory: working_directory,
|
||||||
|
io_error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let justfile = JustfileKind::Stdin { data };
|
||||||
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn find_next(starting_dir: &Path) -> SearchResult<Self> {
|
pub(crate) fn find_next(starting_dir: &Path) -> SearchResult<Self> {
|
||||||
let justfile = Self::justfile(starting_dir)?;
|
let path = Self::justfile(starting_dir)?;
|
||||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
let justfile = JustfileKind::Path { path: path.clone() };
|
||||||
|
let working_directory = Self::working_directory_from_justfile(&path)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search for a Justfile when running "init" subcommand
|
||||||
pub(crate) fn init(
|
pub(crate) fn init(
|
||||||
search_config: &SearchConfig,
|
search_config: &SearchConfig,
|
||||||
invocation_directory: &Path,
|
invocation_directory: &Path,
|
||||||
|
@ -90,7 +176,8 @@ impl Search {
|
||||||
match search_config {
|
match search_config {
|
||||||
SearchConfig::FromInvocationDirectory => {
|
SearchConfig::FromInvocationDirectory => {
|
||||||
let working_directory = Self::project_root(invocation_directory)?;
|
let working_directory = Self::project_root(invocation_directory)?;
|
||||||
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
|
let path = working_directory.join(DEFAULT_JUSTFILE_NAME);
|
||||||
|
let justfile = JustfileKind::Path { path };
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
@ -99,7 +186,8 @@ impl Search {
|
||||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||||
let search_directory = Self::clean(invocation_directory, search_directory);
|
let search_directory = Self::clean(invocation_directory, search_directory);
|
||||||
let working_directory = Self::project_root(&search_directory)?;
|
let working_directory = Self::project_root(&search_directory)?;
|
||||||
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
|
let path = working_directory.join(DEFAULT_JUSTFILE_NAME);
|
||||||
|
let justfile = JustfileKind::Path { path };
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
@ -107,8 +195,9 @@ impl Search {
|
||||||
}
|
}
|
||||||
SearchConfig::GlobalJustfile => Err(SearchError::GlobalJustfileInit),
|
SearchConfig::GlobalJustfile => Err(SearchError::GlobalJustfileInit),
|
||||||
SearchConfig::WithJustfile { justfile } => {
|
SearchConfig::WithJustfile { justfile } => {
|
||||||
let justfile = Self::clean(invocation_directory, justfile);
|
let path = Self::clean(invocation_directory, justfile);
|
||||||
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
let working_directory = Self::working_directory_from_justfile(&path)?;
|
||||||
|
let justfile = JustfileKind::Path { path };
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
|
@ -117,14 +206,51 @@ impl Search {
|
||||||
SearchConfig::WithJustfileAndWorkingDirectory {
|
SearchConfig::WithJustfileAndWorkingDirectory {
|
||||||
justfile,
|
justfile,
|
||||||
working_directory,
|
working_directory,
|
||||||
} => Ok(Self {
|
} => {
|
||||||
justfile: Self::clean(invocation_directory, justfile),
|
let path = Self::clean(invocation_directory, justfile);
|
||||||
working_directory: Self::clean(invocation_directory, working_directory),
|
let justfile = JustfileKind::Path { path };
|
||||||
}),
|
let working_directory = Self::clean(invocation_directory, working_directory);
|
||||||
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchConfig::WithStdin => {
|
||||||
|
let working_directory = Self::project_root(invocation_directory)?;
|
||||||
|
let mut data = String::new();
|
||||||
|
if let Err(err) = io::stdin().read_to_string(&mut data) {
|
||||||
|
return Err(SearchError::Io {
|
||||||
|
directory: working_directory,
|
||||||
|
io_error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let justfile = JustfileKind::Stdin { data };
|
||||||
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchConfig::WithStdinAndWorkingDirectory { working_directory } => {
|
||||||
|
let working_directory = working_directory.to_owned();
|
||||||
|
let mut data = String::new();
|
||||||
|
if let Err(err) = io::stdin().read_to_string(&mut data) {
|
||||||
|
return Err(SearchError::Io {
|
||||||
|
directory: working_directory,
|
||||||
|
io_error: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let justfile = JustfileKind::Stdin { data };
|
||||||
|
Ok(Self {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
||||||
for directory in directory.ancestors() {
|
for directory in directory.ancestors() {
|
||||||
let mut candidates = BTreeSet::new();
|
let mut candidates = BTreeSet::new();
|
||||||
|
|
||||||
|
|
|
@ -19,4 +19,10 @@ pub(crate) enum SearchConfig {
|
||||||
justfile: PathBuf,
|
justfile: PathBuf,
|
||||||
working_directory: PathBuf,
|
working_directory: PathBuf,
|
||||||
},
|
},
|
||||||
|
/// Use justfile loaded from stdin by using "-" as justfile name.
|
||||||
|
WithStdin,
|
||||||
|
/// Use justfile loaded from stdin and working directory.
|
||||||
|
WithStdinAndWorkingDirectory {
|
||||||
|
working_directory: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ impl<'src> Keyed<'src> for Set<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Set<'src> {
|
impl<'src> Display for Set<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "set {} := {}", self.name, self.value)
|
write!(f, "set {} := {}", self.name, self.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub(crate) enum Setting<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Setting<'src> {
|
impl<'src> Display for Setting<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::AllowDuplicateRecipes(value)
|
Self::AllowDuplicateRecipes(value)
|
||||||
| Self::AllowDuplicateVariables(value)
|
| Self::AllowDuplicateVariables(value)
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub(crate) struct Shell<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for Shell<'src> {
|
impl<'src> Display for Shell<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "[{}", self.command)?;
|
write!(f, "[{}", self.command)?;
|
||||||
|
|
||||||
for argument in &self.arguments {
|
for argument in &self.arguments {
|
||||||
|
|
|
@ -42,11 +42,7 @@ pub(crate) enum Subcommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subcommand {
|
impl Subcommand {
|
||||||
pub(crate) fn execute<'src>(
|
pub(crate) fn execute<'src>(&self, config: &Config, loader: &'src Loader) -> RunResult<'src> {
|
||||||
&self,
|
|
||||||
config: &Config,
|
|
||||||
loader: &'src Loader,
|
|
||||||
) -> Result<(), Error<'src>> {
|
|
||||||
use Subcommand::*;
|
use Subcommand::*;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
|
@ -97,7 +93,7 @@ impl Subcommand {
|
||||||
|
|
||||||
fn groups(config: &Config, justfile: &Justfile) {
|
fn groups(config: &Config, justfile: &Justfile) {
|
||||||
println!("Recipe groups:");
|
println!("Recipe groups:");
|
||||||
for group in justfile.public_groups() {
|
for group in justfile.public_groups(config) {
|
||||||
println!("{}{group}", config.list_prefix);
|
println!("{}{group}", config.list_prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +103,7 @@ impl Subcommand {
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
) -> Result<(), Error<'src>> {
|
) -> RunResult<'src> {
|
||||||
if matches!(
|
if matches!(
|
||||||
config.search_config,
|
config.search_config,
|
||||||
SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }
|
SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }
|
||||||
|
@ -150,7 +146,7 @@ impl Subcommand {
|
||||||
};
|
};
|
||||||
|
|
||||||
match Self::run_inner(config, loader, arguments, overrides, &search) {
|
match Self::run_inner(config, loader, arguments, overrides, &search) {
|
||||||
Err((err @ Error::UnknownRecipes { .. }, true)) => {
|
Err((err @ Error::UnknownRecipe { .. }, true)) => {
|
||||||
match search.justfile.parent().unwrap().parent() {
|
match search.justfile.parent().unwrap().parent() {
|
||||||
Some(parent) => {
|
Some(parent) => {
|
||||||
unknown_recipes_errors.get_or_insert(err);
|
unknown_recipes_errors.get_or_insert(err);
|
||||||
|
@ -192,7 +188,7 @@ impl Subcommand {
|
||||||
config: &Config,
|
config: &Config,
|
||||||
loader: &'src Loader,
|
loader: &'src Loader,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
) -> Result<Compilation<'src>, Error<'src>> {
|
) -> RunResult<'src, Compilation<'src>> {
|
||||||
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
|
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
|
||||||
|
|
||||||
if config.verbosity.loud() {
|
if config.verbosity.loud() {
|
||||||
|
@ -214,8 +210,8 @@ impl Subcommand {
|
||||||
search: &Search,
|
search: &Search,
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
chooser: Option<&str>,
|
chooser: Option<&str>,
|
||||||
) -> Result<(), Error<'src>> {
|
) -> RunResult<'src> {
|
||||||
let mut recipes = Vec::<&Recipe<Dependency>>::new();
|
let mut recipes = Vec::<&Recipe>::new();
|
||||||
let mut stack = vec![justfile];
|
let mut stack = vec![justfile];
|
||||||
while let Some(module) = stack.pop() {
|
while let Some(module) = stack.pop() {
|
||||||
recipes.extend(
|
recipes.extend(
|
||||||
|
@ -304,7 +300,7 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump(config: &Config, ast: &Ast, justfile: &Justfile) -> Result<(), Error<'static>> {
|
fn dump(config: &Config, ast: &Ast, justfile: &Justfile) -> RunResult<'static> {
|
||||||
match config.dump_format {
|
match config.dump_format {
|
||||||
DumpFormat::Json => {
|
DumpFormat::Json => {
|
||||||
serde_json::to_writer(io::stdout(), justfile)
|
serde_json::to_writer(io::stdout(), justfile)
|
||||||
|
@ -316,7 +312,7 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit(search: &Search) -> Result<(), Error<'static>> {
|
fn edit(search: &Search) -> RunResult<'static> {
|
||||||
let editor = env::var_os("VISUAL")
|
let editor = env::var_os("VISUAL")
|
||||||
.or_else(|| env::var_os("EDITOR"))
|
.or_else(|| env::var_os("EDITOR"))
|
||||||
.unwrap_or_else(|| "vim".into());
|
.unwrap_or_else(|| "vim".into());
|
||||||
|
@ -338,7 +334,7 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> Result<(), Error<'static>> {
|
fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> RunResult<'static> {
|
||||||
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
||||||
|
|
||||||
let formatted = ast.to_string();
|
let formatted = ast.to_string();
|
||||||
|
@ -383,7 +379,7 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(config: &Config) -> Result<(), Error<'static>> {
|
fn init(config: &Config) -> RunResult<'static> {
|
||||||
let search = Search::init(&config.search_config, &config.invocation_directory)?;
|
let search = Search::init(&config.search_config, &config.invocation_directory)?;
|
||||||
|
|
||||||
if search.justfile.is_file() {
|
if search.justfile.is_file() {
|
||||||
|
@ -403,7 +399,7 @@ impl Subcommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn man() -> Result<(), Error<'static>> {
|
fn man() -> RunResult<'static> {
|
||||||
let mut buffer = Vec::<u8>::new();
|
let mut buffer = Vec::<u8>::new();
|
||||||
|
|
||||||
Man::new(Config::app())
|
Man::new(Config::app())
|
||||||
|
@ -423,12 +419,14 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(config: &Config, mut module: &Justfile, path: &ModulePath) -> Result<(), Error<'static>> {
|
fn list(config: &Config, mut module: &Justfile, path: &ModulePath) -> RunResult<'static> {
|
||||||
for name in &path.path {
|
for name in &path.path {
|
||||||
module = module
|
module = module
|
||||||
.modules
|
.modules
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
|
.ok_or_else(|| Error::UnknownSubmodule {
|
||||||
|
path: path.to_string(),
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::list_module(config, module, 0);
|
Self::list_module(config, module, 0);
|
||||||
|
@ -502,7 +500,17 @@ impl Subcommand {
|
||||||
groups
|
groups
|
||||||
};
|
};
|
||||||
|
|
||||||
for (i, (group, recipes)) in groups.iter().enumerate() {
|
let mut ordered = module
|
||||||
|
.public_groups(config)
|
||||||
|
.into_iter()
|
||||||
|
.map(Some)
|
||||||
|
.collect::<Vec<Option<String>>>();
|
||||||
|
|
||||||
|
if groups.contains_key(&None) {
|
||||||
|
ordered.insert(0, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, group) in ordered.into_iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
@ -511,14 +519,14 @@ impl Subcommand {
|
||||||
|
|
||||||
if !no_groups {
|
if !no_groups {
|
||||||
print!("{list_prefix}");
|
print!("{list_prefix}");
|
||||||
if let Some(group_name) = group {
|
if let Some(group) = &group {
|
||||||
println!("[{group_name}]");
|
println!("[{group}]");
|
||||||
} else {
|
} else {
|
||||||
println!("(no group)");
|
println!("(no group)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for recipe in recipes {
|
for recipe in groups.get(&group).unwrap() {
|
||||||
for (i, name) in iter::once(&recipe.name())
|
for (i, name) in iter::once(&recipe.name())
|
||||||
.chain(aliases.get(recipe.name()).unwrap_or(&Vec::new()))
|
.chain(aliases.get(recipe.name()).unwrap_or(&Vec::new()))
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
@ -583,12 +591,14 @@ impl Subcommand {
|
||||||
config: &Config,
|
config: &Config,
|
||||||
mut module: &Justfile<'src>,
|
mut module: &Justfile<'src>,
|
||||||
path: &ModulePath,
|
path: &ModulePath,
|
||||||
) -> Result<(), Error<'src>> {
|
) -> RunResult<'src> {
|
||||||
for name in &path.path[0..path.path.len() - 1] {
|
for name in &path.path[0..path.path.len() - 1] {
|
||||||
module = module
|
module = module
|
||||||
.modules
|
.modules
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| Error::UnknownSubmodule { path: path.clone() })?;
|
.ok_or_else(|| Error::UnknownSubmodule {
|
||||||
|
path: path.to_string(),
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = path.path.last().unwrap();
|
let name = path.path.last().unwrap();
|
||||||
|
@ -602,8 +612,8 @@ impl Subcommand {
|
||||||
println!("{}", recipe.color_display(config.color.stdout()));
|
println!("{}", recipe.color_display(config.color.stdout()));
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(Error::UnknownRecipes {
|
Err(Error::UnknownRecipe {
|
||||||
recipes: vec![name.to_owned()],
|
recipe: name.to_owned(),
|
||||||
suggestion: module.suggest_recipe(name),
|
suggestion: module.suggest_recipe(name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ mod full {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn summary(path: &Path) -> Result<Result<Summary, String>, io::Error> {
|
pub fn summary(path: &Path) -> io::Result<Result<Summary, String>> {
|
||||||
let loader = Loader::new();
|
let loader = Loader::new();
|
||||||
|
|
||||||
match Compiler::compile(false, &loader, path) {
|
match Compiler::compile(false, &loader, path) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use {super::*, pretty_assertions::assert_eq};
|
use {self::search::JustfileKind, super::*, pretty_assertions::assert_eq};
|
||||||
|
|
||||||
pub(crate) fn compile(src: &str) -> Justfile {
|
pub(crate) fn compile(src: &str) -> Justfile {
|
||||||
Compiler::test_compile(src).expect("expected successful compilation")
|
Compiler::test_compile(src).expect("expected successful compilation")
|
||||||
|
@ -17,7 +17,8 @@ pub(crate) fn config(args: &[&str]) -> Config {
|
||||||
|
|
||||||
pub(crate) fn search(config: &Config) -> Search {
|
pub(crate) fn search(config: &Config) -> Search {
|
||||||
let working_directory = config.invocation_directory.clone();
|
let working_directory = config.invocation_directory.clone();
|
||||||
let justfile = working_directory.join("justfile");
|
let path = working_directory.join("justfile");
|
||||||
|
let justfile = JustfileKind::Path { path };
|
||||||
|
|
||||||
Search {
|
Search {
|
||||||
justfile,
|
justfile,
|
||||||
|
@ -131,7 +132,7 @@ macro_rules! run_error {
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! assert_matches {
|
macro_rules! assert_matches {
|
||||||
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )?) => {
|
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
|
||||||
match $expression {
|
match $expression {
|
||||||
$( $pattern )|+ $( if $guard )? => {}
|
$( $pattern )|+ $( if $guard )? => {}
|
||||||
left => panic!(
|
left => panic!(
|
||||||
|
|
14
src/thunk.rs
14
src/thunk.rs
|
@ -6,42 +6,42 @@ pub(crate) enum Thunk<'src> {
|
||||||
Nullary {
|
Nullary {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context) -> Result<String, String>,
|
function: fn(function::Context) -> FunctionResult,
|
||||||
},
|
},
|
||||||
Unary {
|
Unary {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str) -> Result<String, String>,
|
function: fn(function::Context, &str) -> FunctionResult,
|
||||||
arg: Box<Expression<'src>>,
|
arg: Box<Expression<'src>>,
|
||||||
},
|
},
|
||||||
UnaryOpt {
|
UnaryOpt {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str, Option<&str>) -> Result<String, String>,
|
function: fn(function::Context, &str, Option<&str>) -> FunctionResult,
|
||||||
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
|
args: (Box<Expression<'src>>, Box<Option<Expression<'src>>>),
|
||||||
},
|
},
|
||||||
UnaryPlus {
|
UnaryPlus {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str, &[String]) -> Result<String, String>,
|
function: fn(function::Context, &str, &[String]) -> FunctionResult,
|
||||||
args: (Box<Expression<'src>>, Vec<Expression<'src>>),
|
args: (Box<Expression<'src>>, Vec<Expression<'src>>),
|
||||||
},
|
},
|
||||||
Binary {
|
Binary {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str, &str) -> Result<String, String>,
|
function: fn(function::Context, &str, &str) -> FunctionResult,
|
||||||
args: [Box<Expression<'src>>; 2],
|
args: [Box<Expression<'src>>; 2],
|
||||||
},
|
},
|
||||||
BinaryPlus {
|
BinaryPlus {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str, &str, &[String]) -> Result<String, String>,
|
function: fn(function::Context, &str, &str, &[String]) -> FunctionResult,
|
||||||
args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),
|
args: ([Box<Expression<'src>>; 2], Vec<Expression<'src>>),
|
||||||
},
|
},
|
||||||
Ternary {
|
Ternary {
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
function: fn(function::Context, &str, &str, &str) -> Result<String, String>,
|
function: fn(function::Context, &str, &str, &str) -> FunctionResult,
|
||||||
args: [Box<Expression<'src>>; 3],
|
args: [Box<Expression<'src>>; 3],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,7 @@ pub(crate) enum TokenKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for TokenKind {
|
impl Display for TokenKind {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
use TokenKind::*;
|
use TokenKind::*;
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
|
|
@ -7,7 +7,7 @@ pub(crate) struct UnresolvedDependency<'src> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Display for UnresolvedDependency<'src> {
|
impl<'src> Display for UnresolvedDependency<'src> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
if self.arguments.is_empty() {
|
if self.arguments.is_empty() {
|
||||||
write!(f, "{}", self.recipe)
|
write!(f, "{}", self.recipe)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -185,7 +185,13 @@ fn status_error() {
|
||||||
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
||||||
};
|
};
|
||||||
|
|
||||||
("chmod", "+x", tmp.path().join("exit-2")).run();
|
let output = Command::new("chmod")
|
||||||
|
.arg("+x")
|
||||||
|
.arg(tmp.path().join("exit-2"))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.status.success());
|
||||||
|
|
||||||
let path = env::join_paths(
|
let path = env::join_paths(
|
||||||
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||||
|
|
|
@ -33,6 +33,10 @@ fn replacements() {
|
||||||
.args(["--completions", shell])
|
.args(["--completions", shell])
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(output.status.success());
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"shell completion generation for {shell} failed: {}",
|
||||||
|
output.status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
tests/datetime.rs
Normal file
27
tests/datetime.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn datetime() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
x := datetime('%Y-%m-%d %z')
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--eval", "x"])
|
||||||
|
.stdout_regex(r"\d\d\d\d-\d\d-\d\d [+-]\d\d\d\d")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn datetime_utc() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
x := datetime_utc('%Y-%m-%d %Z')
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--eval", "x"])
|
||||||
|
.stdout_regex(r"\d\d\d\d-\d\d-\d\d UTC")
|
||||||
|
.run();
|
||||||
|
}
|
|
@ -360,6 +360,7 @@ fn no_dotenv() {
|
||||||
.stderr("echo DEFAULT\n")
|
.stderr("echo DEFAULT\n")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dotenv_env_var_override() {
|
fn dotenv_env_var_override() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
@ -375,3 +376,21 @@ fn dotenv_env_var_override() {
|
||||||
.stderr("echo $DOTENV_KEY\n")
|
.stderr("echo $DOTENV_KEY\n")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dotenv_path_usable_from_subdir() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
set dotenv-path := '.custom-env'
|
||||||
|
|
||||||
|
@echo:
|
||||||
|
echo $DOTENV_KEY
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.create_dir("sub")
|
||||||
|
.current_dir("sub")
|
||||||
|
.write(".custom-env", "DOTENV_KEY=dotenv-value")
|
||||||
|
.stdout("dotenv-value\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
|
@ -64,7 +64,13 @@ fn status_error() {
|
||||||
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
||||||
};
|
};
|
||||||
|
|
||||||
("chmod", "+x", tmp.path().join("exit-2")).run();
|
let output = Command::new("chmod")
|
||||||
|
.arg("+x")
|
||||||
|
.arg(tmp.path().join("exit-2"))
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.status.success());
|
||||||
|
|
||||||
let path = env::join_paths(
|
let path = env::join_paths(
|
||||||
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||||
|
|
|
@ -126,7 +126,13 @@ fn write_error() {
|
||||||
|
|
||||||
let justfile_path = test.justfile_path();
|
let justfile_path = test.justfile_path();
|
||||||
|
|
||||||
("chmod", "400", &justfile_path).run();
|
let output = Command::new("chmod")
|
||||||
|
.arg("400")
|
||||||
|
.arg(&justfile_path)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(output.status.success());
|
||||||
|
|
||||||
let _tempdir = test.run();
|
let _tempdir = test.run();
|
||||||
|
|
||||||
|
|
124
tests/groups.rs
124
tests/groups.rs
|
@ -96,6 +96,47 @@ fn list_with_groups_unsorted() {
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_with_groups_unsorted_group_order() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
[group('y')]
|
||||||
|
[group('x')]
|
||||||
|
f:
|
||||||
|
|
||||||
|
[group('b')]
|
||||||
|
b:
|
||||||
|
|
||||||
|
[group('a')]
|
||||||
|
e:
|
||||||
|
|
||||||
|
c:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--list", "--unsorted"])
|
||||||
|
.stdout(
|
||||||
|
"
|
||||||
|
Available recipes:
|
||||||
|
(no group)
|
||||||
|
c
|
||||||
|
|
||||||
|
[x]
|
||||||
|
f
|
||||||
|
|
||||||
|
[y]
|
||||||
|
f
|
||||||
|
|
||||||
|
[b]
|
||||||
|
b
|
||||||
|
|
||||||
|
[a]
|
||||||
|
e
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_groups() {
|
fn list_groups() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
@ -157,12 +198,89 @@ fn list_groups_with_shorthand_syntax() {
|
||||||
bar:
|
bar:
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.args(["--groups", "--list-prefix", "..."])
|
.arg("--groups")
|
||||||
.stdout(
|
.stdout(
|
||||||
"
|
"
|
||||||
Recipe groups:
|
Recipe groups:
|
||||||
...A
|
A
|
||||||
...B
|
B
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_groups_unsorted() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
[group: 'Z']
|
||||||
|
baz:
|
||||||
|
|
||||||
|
[group: 'B']
|
||||||
|
foo:
|
||||||
|
|
||||||
|
[group: 'A', group: 'B']
|
||||||
|
bar:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--groups", "--unsorted"])
|
||||||
|
.stdout(
|
||||||
|
"
|
||||||
|
Recipe groups:
|
||||||
|
Z
|
||||||
|
B
|
||||||
|
A
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_groups_private_unsorted() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
[private]
|
||||||
|
[group: 'A']
|
||||||
|
foo:
|
||||||
|
|
||||||
|
[group: 'B']
|
||||||
|
bar:
|
||||||
|
|
||||||
|
[group: 'A']
|
||||||
|
baz:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--groups", "--unsorted"])
|
||||||
|
.stdout(
|
||||||
|
"
|
||||||
|
Recipe groups:
|
||||||
|
B
|
||||||
|
A
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_groups_private() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
[private]
|
||||||
|
[group: 'A']
|
||||||
|
foo:
|
||||||
|
|
||||||
|
[group: 'B']
|
||||||
|
bar:
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--groups", "--unsorted"])
|
||||||
|
.stdout(
|
||||||
|
"
|
||||||
|
Recipe groups:
|
||||||
|
B
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
|
|
|
@ -97,3 +97,41 @@ fn dont_evaluate_comments() {
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dont_analyze_comments() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
set ignore-comments
|
||||||
|
|
||||||
|
some_recipe:
|
||||||
|
# {{ bar }}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comments_still_must_be_parsable_when_ignored() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
set ignore-comments
|
||||||
|
|
||||||
|
some_recipe:
|
||||||
|
# {{ foo bar }}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr(
|
||||||
|
"
|
||||||
|
error: Expected '}}', '(', '+', or '/', but found identifier
|
||||||
|
——▶ justfile:4:12
|
||||||
|
│
|
||||||
|
4 │ # {{ foo bar }}
|
||||||
|
│ ^^^
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ pub(crate) use {
|
||||||
tempdir::tempdir,
|
tempdir::tempdir,
|
||||||
test::{assert_eval_eq, Output, Test},
|
test::{assert_eval_eq, Output, Test},
|
||||||
},
|
},
|
||||||
cradle::input::Input,
|
|
||||||
executable_path::executable_path,
|
executable_path::executable_path,
|
||||||
just::unindent,
|
just::unindent,
|
||||||
libc::{EXIT_FAILURE, EXIT_SUCCESS},
|
libc::{EXIT_FAILURE, EXIT_SUCCESS},
|
||||||
|
@ -47,6 +46,7 @@ mod completions;
|
||||||
mod conditional;
|
mod conditional;
|
||||||
mod confirm;
|
mod confirm;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod datetime;
|
||||||
mod delimiters;
|
mod delimiters;
|
||||||
mod directories;
|
mod directories;
|
||||||
mod dotenv;
|
mod dotenv;
|
||||||
|
@ -106,6 +106,8 @@ mod timestamps;
|
||||||
mod undefined_variables;
|
mod undefined_variables;
|
||||||
mod unexport;
|
mod unexport;
|
||||||
mod unstable;
|
mod unstable;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows;
|
||||||
#[cfg(target_family = "windows")]
|
#[cfg(target_family = "windows")]
|
||||||
mod windows_shell;
|
mod windows_shell;
|
||||||
mod working_directory;
|
mod working_directory;
|
||||||
|
|
|
@ -652,7 +652,7 @@ test! {
|
||||||
justfile: "hello:",
|
justfile: "hello:",
|
||||||
args: ("foo", "bar"),
|
args: ("foo", "bar"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "error: Justfile does not contain recipes `foo` or `bar`.\n",
|
stderr: "error: Justfile does not contain recipe `foo`.\n",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -115,7 +115,7 @@ fn missing_recipe_after_invalid_path() {
|
||||||
.test_round_trip(false)
|
.test_round_trip(false)
|
||||||
.arg(":foo::foo")
|
.arg(":foo::foo")
|
||||||
.arg("bar")
|
.arg("bar")
|
||||||
.stderr("error: Justfile does not contain recipes `:foo::foo` or `bar`.\n")
|
.stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
|
||||||
.status(EXIT_FAILURE)
|
.status(EXIT_FAILURE)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
@ -690,3 +690,94 @@ fn recipes_with_same_name_are_both_run() {
|
||||||
.stdout("MODULE\nROOT\n")
|
.stdout("MODULE\nROOT\n")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.args(["--unstable", "foo::bar"])
|
||||||
|
.stderr("error: Justfile does not contain submodule `foo`\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_spaced_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar:\n @echo MODULE")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo", "baz"])
|
||||||
|
.stderr("error: Justfile does not contain recipe `foo baz`.\nDid you mean `bar`?\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submodule_recipe_not_found_colon_separated_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.write("foo.just", "bar:\n @echo MODULE")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo::baz"])
|
||||||
|
.stderr("error: Justfile does not contain recipe `foo::baz`.\nDid you mean `bar`?\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colon_separated_path_does_not_run_recipes() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo:
|
||||||
|
@echo FOO
|
||||||
|
|
||||||
|
bar:
|
||||||
|
@echo BAR
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["--unstable", "foo::bar"])
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_submodule_but_found_recipe_in_root_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("foo:")
|
||||||
|
.arg("foo::baz")
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expected_submodule_but_found_recipe_in_submodule_error() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("mod foo")
|
||||||
|
.write("foo.just", "bar:")
|
||||||
|
.test_round_trip(false)
|
||||||
|
.args(["--unstable", "foo::bar::baz"])
|
||||||
|
.stderr("error: Expected submodule at `foo::bar` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colon_separated_path_components_are_not_used_as_arguments() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("foo bar:")
|
||||||
|
.args(["foo::bar"])
|
||||||
|
.stderr("error: Expected submodule at `foo` but found recipe.\n")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,31 @@ test! {
|
||||||
"#,
|
"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: linewise_with_attribute,
|
||||||
|
justfile: r#"
|
||||||
|
[positional-arguments]
|
||||||
|
foo bar baz:
|
||||||
|
echo $0
|
||||||
|
echo $1
|
||||||
|
echo $2
|
||||||
|
echo "$@"
|
||||||
|
"#,
|
||||||
|
args: ("foo", "hello", "goodbye"),
|
||||||
|
stdout: "
|
||||||
|
foo
|
||||||
|
hello
|
||||||
|
goodbye
|
||||||
|
hello goodbye
|
||||||
|
",
|
||||||
|
stderr: r#"
|
||||||
|
echo $0
|
||||||
|
echo $1
|
||||||
|
echo $2
|
||||||
|
echo "$@"
|
||||||
|
"#,
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: variadic_linewise,
|
name: variadic_linewise,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
|
@ -51,6 +76,18 @@ test! {
|
||||||
stdout: "hello\n",
|
stdout: "hello\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: shebang_with_attribute,
|
||||||
|
justfile: "
|
||||||
|
[positional-arguments]
|
||||||
|
foo bar:
|
||||||
|
#!/bin/sh
|
||||||
|
echo $1
|
||||||
|
",
|
||||||
|
args: ("foo", "hello"),
|
||||||
|
stdout: "hello\n",
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: variadic_shebang,
|
name: variadic_shebang,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
|
|
|
@ -151,3 +151,39 @@ test! {
|
||||||
stderr: "echo bar\necho foo\n",
|
stderr: "echo bar\necho foo\n",
|
||||||
shell: false,
|
shell: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recipe_shell_not_found_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo:
|
||||||
|
@echo bar
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.shell(false)
|
||||||
|
.args(["--shell", "NOT_A_REAL_SHELL"])
|
||||||
|
.stderr_regex(
|
||||||
|
"error: Recipe `foo` could not be run because just could not find the shell: .*\n",
|
||||||
|
)
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn backtick_recipe_shell_not_found_error_message() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
bar := `echo bar`
|
||||||
|
|
||||||
|
foo:
|
||||||
|
echo {{bar}}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.shell(false)
|
||||||
|
.args(["--shell", "NOT_A_REAL_SHELL"])
|
||||||
|
.stderr_regex("(?s)error: Backtick could not be run because just could not find the shell:.*")
|
||||||
|
.status(1)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
|
@ -94,6 +94,11 @@ impl Test {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_dir(self, path: impl AsRef<Path>) -> Self {
|
||||||
|
fs::create_dir_all(self.tempdir.path().join(path.as_ref())).unwrap();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn current_dir(mut self, path: impl AsRef<Path>) -> Self {
|
pub(crate) fn current_dir(mut self, path: impl AsRef<Path>) -> Self {
|
||||||
path.as_ref().clone_into(&mut self.current_dir);
|
path.as_ref().clone_into(&mut self.current_dir);
|
||||||
self
|
self
|
||||||
|
|
|
@ -99,6 +99,28 @@ fn unexport_doesnt_override_local_recipe_export() {
|
||||||
)
|
)
|
||||||
.args(["recipe", "value"])
|
.args(["recipe", "value"])
|
||||||
.stdout("variable: value\n")
|
.stdout("variable: value\n")
|
||||||
.status(0)
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unexport_does_not_conflict_with_recipe_syntax() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
unexport foo:
|
||||||
|
@echo {{foo}}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.args(["unexport", "bar"])
|
||||||
|
.stdout("bar\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unexport_does_not_conflict_with_assignment_syntax() {
|
||||||
|
Test::new()
|
||||||
|
.justfile("unexport := 'foo'")
|
||||||
|
.args(["--evaluate", "unexport"])
|
||||||
|
.stdout("foo")
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
15
tests/windows.rs
Normal file
15
tests/windows.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bare_bash_in_shebang() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
default:
|
||||||
|
#!bash
|
||||||
|
echo FOO
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user