Compare commits

..

44 Commits

Author SHA1 Message Date
Greg Shuflin
8d80f83795 group attribute on import
https://github.com/casey/just/issues/2087
2024-06-25 23:59:51 -07:00
dependabot[bot]
c900b6f478
Update softprops/action-gh-release (#2183) 2024-06-24 12:42:39 -07:00
Casey Rodarmor
af86a471e2
Don't analyze comments when ignore-comments is set (#2180) 2024-06-21 20:39:34 +00:00
Casey Rodarmor
e4564f45a3
Don't exit process in run() on argument parse error (#2176) 2024-06-20 03:57:46 +00:00
Casey Rodarmor
aa43a664ee
Document remote justfile workaround (#2175) 2024-06-19 17:18:03 -07:00
Casey Rodarmor
553adc1004
Document library interface (#2174) 2024-06-19 23:38:02 +00:00
Casey Rodarmor
e572b93d84
Allow passing command-line arguments into run() (#2173) 2024-06-19 23:25:36 +00:00
Ryan McGuire
fcac7ee768
Ignore env_logger initialization errors (#2170) 2024-06-19 07:50:37 +00:00
Blair Noctis
71b72c4a53
Remove dependency on cradle (#2169) 2024-06-18 02:42:16 +00:00
Casey Rodarmor
0e8f660d6d
Add datetime() and datetime_utc() functions (#2167) 2024-06-14 22:48:34 -07:00
Casey Rodarmor
1c3c1dd3c0
Add note to readme about quoting paths on Windows (#2166) 2024-06-15 05:32:07 +00:00
Casey Rodarmor
197e1002d0
List recipes by group in group justfile order with just --list --unsorted (#2164) 2024-06-15 03:04:47 +00:00
Casey Rodarmor
4a59769faa
Add missing changelog credits (#2163) 2024-06-14 23:58:31 +00:00
Casey Rodarmor
bf6ec6bf16
Credit myself in changelog (#2162) 2024-06-14 23:42:14 +00:00
Casey Rodarmor
1547af08b5
Allow setting more command-line options with environment variables (#2161) 2024-06-14 23:11:22 +00:00
Casey Rodarmor
b05a75d168
List groups in source order with just --groups --unsorted (#2160) 2024-06-14 20:35:03 +00:00
Casey Rodarmor
5f91b37c82
Release 1.29.1 (#2159)
- Bump version: 1.29.0 → 1.29.1
- Update changelog
- Update changelog contributor credits
2024-06-14 12:44:52 -07:00
Casey Rodarmor
dd9792571b
Fix unexport syntax conflicts (#2158) 2024-06-14 19:39:34 +00:00
Casey Rodarmor
e6c37aacd1
Release 1.29.0 (#2155)
- Bump version: 1.28.0 → 1.29.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
- Update version references in readme
- Fix zsh completion script
2024-06-14 02:57:12 +00:00
Casey Rodarmor
18ec9796b9
Improve argument parsing and error handling for submodules (#2154) 2024-06-14 02:41:45 +00:00
Casey Rodarmor
e1b17fe9cf
Document shell expanded string defaults (#2153) 2024-06-13 21:41:19 +00:00
Casey Rodarmor
4b5ba8f6f5
Load environment file from dotenv-path relative to working directory (#2152) 2024-06-13 20:21:00 +00:00
Casey Rodarmor
1ce7a05bef
Add [positional-arguments] attribute (#2151) 2024-06-13 19:35:14 +00:00
Casey Rodarmor
637023e86f
Test bare bash path in shebang on windows (#2144) 2024-06-13 19:19:22 +00:00
Ruben Nicolaides
4f16428bcb
Use --justfile in Fish shell completions (#2148) 2024-06-12 18:38:37 +00:00
Casey Rodarmor
8778972014
Test shell not found error messages (#2145) 2024-06-11 13:10:32 -07:00
Greg Shuflin
5ac98c020d
Add is_dependency() function (#2139) 2024-06-09 01:17:55 +00:00
Casey Rodarmor
3236154d8d
Fix fzf chooser preview with space-separated module paths (#2141) 2024-06-09 01:01:24 +00:00
Casey Rodarmor
0de971942a
Allow printing nu completion script with just --completions nushell (#2140) 2024-06-08 21:56:21 +00:00
Greg Shuflin
1ca53e8b22
Add [ATTRIBUTE: VALUE] shorthand (#2136) 2024-06-08 18:33:45 +00:00
Greg Shuflin
4fbd03735a
Refactor evaluator (#2138) 2024-06-08 14:42:16 +00:00
Greg Shuflin
38873dcb74
Allow unexporting environment variables (#2098) 2024-06-05 20:16:47 +00:00
Casey Rodarmor
3ddd1b1683
Fix man page generation in release workflow (#2132) 2024-06-05 19:35:06 +00:00
Casey Rodarmor
0eb2a0678c
Release 1.28.0 (#2131)
- Bump version: 1.27.0 → 1.28.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
2024-06-05 19:29:55 +00:00
Casey Rodarmor
3c40c0c6eb
Don't check in manpage (#2130) 2024-06-05 19:17:05 +00:00
Casey Rodarmor
70d1e1b3af
Document default shell (#2129) 2024-06-05 19:06:32 +00:00
Casey Rodarmor
af249dbce1
Write shebang recipes to $XDG_RUNTIME_DIR (#2128) 2024-06-05 19:03:14 +00:00
Potter XU
7c30fb4944
Remove duplicate section in Chinese readme (#2127) 2024-06-05 20:53:36 +02:00
Potter XU
6fe068c432
Update Chinese readme (#2124) 2024-06-04 11:28:29 +02:00
Potter XU
b5ad133b93
Fix typo in readme (#2122) 2024-06-02 19:40:29 +00:00
Casey Rodarmor
8d3d88fc13
Don't check in auto-generated completion scripts (#2120) 2024-06-01 23:26:41 +00:00
Casey Rodarmor
f2201d8684
Add set dotenv-required to require an environment file (#2116) 2024-05-30 23:12:07 +00:00
Casey Rodarmor
d38c1add13
Allow listing recipes in submodules with --list-submodules (#2113) 2024-05-30 12:28:54 -05:00
Casey Rodarmor
d2b10e04d3
Use space-separated recipe paths in --choose (#2115) 2024-05-30 17:24:06 +00:00
88 changed files with 3314 additions and 2434 deletions

View File

@ -30,12 +30,6 @@ jobs:
- name: Format - name: Format
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Completion Scripts
run: |
./bin/update-completions
git diff --no-ext-diff --quiet --exit-code
./tests/completions/just.bash
- name: Install Dependencies - name: Install Dependencies
run: | run: |
sudo apt-get update sudo apt-get update

View File

@ -73,6 +73,17 @@ jobs:
id: ref-type id: ref-type
run: cargo run --package ref-type -- --reference ${{ github.ref }} >> $GITHUB_OUTPUT run: cargo run --package ref-type -- --reference ${{ github.ref }} >> $GITHUB_OUTPUT
- name: Generate Completion Scripts and Manpage
run: |
set -euxo pipefail
cargo build
mkdir -p completions
for shell in bash elvish fish nu powershell zsh; do
./target/debug/just --completions $shell > completions/just.$shell
done
mkdir -p man
./target/debug/just --man > man/just.1
- name: Package - name: Package
id: package id: package
env: env:
@ -84,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
@ -94,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/')

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
/fuzz/artifacts /fuzz/artifacts
/fuzz/corpus /fuzz/corpus
/fuzz/target /fuzz/target
/man
/target /target
/test-utilities/Cargo.lock /test-utilities/Cargo.lock
/test-utilities/target /test-utilities/target

File diff suppressed because it is too large Load Diff

113
Cargo.lock generated
View File

@ -67,9 +67,9 @@ dependencies = [
[[package]] [[package]]
name = "anstyle-query" name = "anstyle-query"
version = "1.0.3" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -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,18 +221,19 @@ 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",
] ]
[[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",
@ -243,26 +244,38 @@ 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]]
name = "clap_derive"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.66",
] ]
[[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",
] ]
@ -293,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"
@ -505,12 +509,6 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -593,16 +591,15 @@ dependencies = [
[[package]] [[package]]
name = "just" name = "just"
version = "1.27.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",
@ -678,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"
@ -782,9 +779,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.83" version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -891,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",
] ]
@ -909,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",
@ -920,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"
@ -963,18 +960,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.202" version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1090,11 +1087,11 @@ dependencies = [
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.26.2" version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [ dependencies = [
"heck 0.4.1", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
@ -1125,9 +1122,9 @@ dependencies = [
[[package]] [[package]]
name = "target" name = "target"
version = "2.0.1" version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4df6b0340c7cc29eb3b955cc588d145ed60651bf1ab939083295d19ec8cc282" checksum = "1e8f05f774b2db35bdad5a8237a90be1102669f8ea013fea9777b366d34ab145"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
@ -1224,9 +1221,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.12" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]] [[package]]
name = "update-contributors" name = "update-contributors"
@ -1237,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"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "just" name = "just"
version = "1.27.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"]
@ -22,7 +22,7 @@ ansi_term = "0.12.0"
blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
camino = "1.0.4" camino = "1.0.4"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.0.0", features = ["env", "wrap_help"] } clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.0.0" clap_complete = "4.0.0"
clap_mangen = "0.2.20" clap_mangen = "0.2.20"
ctrlc = { version = "3.1.1", features = ["termination"] } ctrlc = { version = "3.1.1", features = ["termination"] }
@ -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"

128
README.md
View File

@ -812,6 +812,7 @@ foo:
| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. | | `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |
| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. | | `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |
| `dotenv-path` | string | - | Load a `.env` file from a custom path and error if not present. Overrides `dotenv-filename`. | | `dotenv-path` | string | - | Load a `.env` file from a custom path and error if not present. Overrides `dotenv-filename`. |
| `dotenv-required` | boolean | `false` | Error if a `.env` file isn't found. |
| `export` | boolean | `false` | Export all variables as environment variables. | | `export` | boolean | `false` | Export all variables as environment variables. |
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. | | `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. | | `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
@ -877,17 +878,25 @@ bar
#### Dotenv Settings #### Dotenv Settings
If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load If any of `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`
environment variables from a file. are set, `just` will try to load environment variables from a file.
If `dotenv-path` is set, `just` will look for a file at the given path. It is If `dotenv-path` is set, `just` will look for a file at the given path, which
an error if a dotenv file is not found at `dotenv-path`, but not an error if a may be absolute, or relative to the working directory.
dotenv file is not found with `dotenv-filename`.
Otherwise, `just` looks for a file named `.env` by default, unless If `dotenv-filename` is set `just` will look for a file at the given path,
`dotenv-filename` set, in which case the value of `dotenv-filename` is used. relative to the working directory and each of its ancestors.
This file can be located in the same directory as your `justfile` or in a
parent directory. If `dotenv-filename` is not set, but `dotenv-load` or `dotenv-required` are
set, just will look for a file named `.env`, relative to the working directory
and each of its ancestors.
`dotenv-filename` and `dotenv-path` and similar, but `dotenv-path` is only
checked relative to the working directory, whereas `dotenv-filename` is checked
relative to the working directory and each of its ancestors.
It is not an error if an environment file is not found, unless
`dotenv-required` is set.
The loaded variables are environment variables, not `just` variables, and so The loaded variables are environment variables, not `just` variables, and so
must be accessed using `$VARIABLE_NAME` in recipes and backticks. must be accessed using `$VARIABLE_NAME` in recipes and backticks.
@ -987,10 +996,24 @@ $ 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
backticks. Shebang recipes are unaffected. backticks. Shebang recipes are unaffected. The default shell is `sh -cu`.
```just ```just
# use python3 to execute recipe lines and backticks # use python3 to execute recipe lines and backticks
@ -1291,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 |
@ -1417,6 +1441,12 @@ $ just
- `env(key)`<sup>1.15.0</sup> — Alias for `env_var(key)`. - `env(key)`<sup>1.15.0</sup> — Alias for `env_var(key)`.
- `env(key, default)`<sup>1.15.0</sup> — Alias for `env_var_or_default(key, default)`. - `env(key, default)`<sup>1.15.0</sup> — Alias for `env_var_or_default(key, default)`.
#### Invocation Information
- `is_dependency()` - Returns the string `true` if the current recipe is being
run as a dependency of another recipe, rather than being run directly,
otherwise returns the string `false`.
#### Invocation Directory #### Invocation Directory
- `invocation_directory()` - Retrieves the absolute path to the current - `invocation_directory()` - Retrieves the absolute path to the current
@ -1612,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
@ -1671,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. |
@ -1776,7 +1817,7 @@ js-lint:
[group('rust recipes')] [group('rust recipes')]
[group('lint')] [group('lint')]
rust-lint: rust-lint:
echo 'Runninng Rust linter…' echo 'Running Rust linter…'
[group('lint')] [group('lint')]
cpp-lint: cpp-lint:
@ -1830,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:
@ -2044,6 +2087,23 @@ 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>1.29.0</sup>
Environment variables can be unexported with the `unexport keyword`:
```just
unexport FOO
@foo:
echo $FOO
```
```
$ export FOO=bar
$ just foo
sh: FOO: unbound variable
```
#### Getting Environment Variables from the environment #### Getting Environment Variables from the environment
Environment variables from the environment are passed automatically to the Environment variables from the environment are passed automatically to the
@ -3330,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
@ -3463,18 +3523,18 @@ complete -F _just -o bashdefault -o default j
### Shell Completion Scripts ### Shell Completion Scripts
Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are Shell completion scripts for Bash, Elvish, Fish, Nushell, PowerShell, and Zsh
available in the are available [release archives](https://github.com/casey/just/releases).
[completions](https://github.com/casey/just/tree/master/completions) directory.
Please refer to your shell's documentation for how to install them.
The `just` binary can also generate the same completion scripts at runtime, The `just` binary can also generate the same completion scripts at runtime
using the `--completions` command: using `just --completions SHELL`:
```sh ```sh
$ just --completions zsh > just.zsh $ just --completions zsh > just.zsh
``` ```
Please refer to your shell's documentation for how to install them.
*macOS Note:* Recent versions of macOS use zsh as the default shell. If you use *macOS Note:* Recent versions of macOS use zsh as the default shell. If you use
Homebrew to install `just`, it will automatically install the most recent copy Homebrew to install `just`, it will automatically install the most recent copy
of the zsh completion script in the Homebrew zsh directory, which the built-in of the zsh completion script in the Homebrew zsh directory, which the built-in
@ -3597,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
@ -3664,7 +3751,6 @@ Release x.y.z
- Update changelog - Update changelog
- Update changelog contributor credits - Update changelog contributor credits
- Update dependencies - Update dependencies
- Update man page
- Update version references in readme - Update version references in readme
``` ```

View File

@ -50,7 +50,7 @@ Yay, all your tests passed!
- 错误会尽可能被静态地解决。未知的配方和循环依赖关系会在运行之前被报告。 - 错误会尽可能被静态地解决。未知的配方和循环依赖关系会在运行之前被报告。
- `just` 可以 [加载`.env`文件](#env-集成),简化环境变量注入。 - `just` 可以 [加载`.env`文件](#环境变量加载),简化环境变量注入。
- 配方可以在 [命令行中列出](#列出可用的配方)。 - 配方可以在 [命令行中列出](#列出可用的配方)。
@ -642,9 +642,13 @@ foo:
#### 设置一览表 #### 设置一览表
| 名称 | 值 | 默认 | 描述 | | 名称 | 值 | 默认 | 描述 |
| ------------------------- | ------------------ | --------|------------------------------------------------------------------------------- | | --------------------------- | ------------------ | ----- | --------------------------------------------------------------------------------------- |
| `allow-duplicate-recipes` | boolean | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方 | | `allow-duplicate-recipes` | boolean | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方 |
| `allow-duplicate-variables` | boolean | False | 允许在 `justfile` 后面出现的变量覆盖之前的同名变量 |
| `dotenv-filename` | string | - | 如果有自定义名称的 `.env` 环境变量文件的话,则将其加载 |
| `dotenv-load` | boolean | False | 如果有`.env` 环境变量文件的话,则将其加载 | | `dotenv-load` | boolean | False | 如果有`.env` 环境变量文件的话,则将其加载 |
| `dotenv-path` | string | - | 从自定义路径中加载 `.env` 环境变量文件, 文件不存在将会报错。可以覆盖 `dotenv-filename` |
| `dotenv-required` | boolean | False | 如果 `.env` 环境变量文件不存在的话,需要报错 |
| `export` | boolean | False | 将所有变量导出为环境变量 | | `export` | boolean | False | 将所有变量导出为环境变量 |
| `fallback` | boolean | False | 如果命令行中的第一个配方没有找到,则在父目录中搜索 `justfile` | | `fallback` | boolean | False | 如果命令行中的第一个配方没有找到,则在父目录中搜索 `justfile` |
| `ignore-comments` | boolean | False | 忽略以`#`开头的配方行 | | `ignore-comments` | boolean | False | 忽略以`#`开头的配方行 |
@ -685,9 +689,69 @@ $ just foo
bar bar
``` ```
#### 允许重复的变量
如果 `allow-duplicate-variables` 被设置为 `true`,那么定义多个同名的变量将不会报错。默认为 `false`
```just
set allow-duplicate-variables
a := "foo"
a := "bar"
@foo:
echo $a
```
```sh
$ just foo
bar
```
#### 环境变量加载 #### 环境变量加载
如果将 `dotenv-load` 设置为 `true`,并且存在 `.env` 文件,则该环境配置文件将被加载。默认为 `false` 如果 `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`
中任意一项被设置, `just` 会尝试从文件中加载环境变量
如果设置了 `dotenv-path`, `just` 会在指定的路径下搜索文件,该路径可以是绝对路径,
也可以是基于当前工作路径的相对路径
如果设置了 `dotenv-filename``just` 会在指定的相对路径,以及其所有的上层目录中,搜索指定文件
如果没有设置 `dotenv-filename`,但是设置了 `dotenv-load``dotenv-required`
`just` 会在当前工作路径,以及其所有的上层目录中,寻找名为 `.env` 的文件。
`dotenv-filename``dotenv-path` 很相似,但是 `dotenv-path` 只会检查指定的目录
`dotenv-filename` 会检查指定目录以及其所有的上层目录。
如果没有找到环境变量文件也不会报错,除非设置了 `dotenv-required`
从文件中加载的变量是环境变量,而非 `just` 变量,所以在配方和反引号中需要必须通过 `$VARIABLE_NAME` 来调用。
比如,如果你的 `.env` 文件包含以下内容:
```sh
# a comment, will be ignored
DATABASE_ADDRESS=localhost:6379
SERVER_PORT=1337
```
并且你的 `justfile` 包含:
```just
set dotenv-load
serve:
@echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```
`just serve` 将会输出:
```sh
$ just serve
Starting server with database localhost:6379 on port 1337…
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```
#### 导出 #### 导出
@ -853,36 +917,6 @@ Available recipes:
test # test stuff test # test stuff
``` ```
### `.env` 集成
如果 [`dotenv-load`](#环境变量加载) 被设置,`just` 将从一个名为 `.env` 的文件中加载环境变量。这个文件可以和你的 `justfile` 位于同一目录下,或者位于其父目录下。这些变量是环境变量,而不是 `just` 的变量,因此必须使用 `$VARIABLE_NAME` 在配方和反引号中访问。
例如,假如你的 `.env` 文件包含:
```sh
# 注释,将被忽略
DATABASE_ADDRESS=localhost:6379
SERVER_PORT=1337
```
而你的 `justfile` 包含:
```just
set dotenv-load
serve:
@echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```
`just serve` 将会输出:
```sh
$ just serve
Starting server with database localhost:6379 on port 1337…
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```
### 变量和替换 ### 变量和替换
支持在变量、字符串、拼接、路径连接和替换中使用 `{{…}}` 支持在变量、字符串、拼接、路径连接和替换中使用 `{{…}}`
@ -1463,7 +1497,7 @@ HOME is '/home/myuser'
#### 从 `.env` 文件加载环境变量 #### 从 `.env` 文件加载环境变量
如果 [dotenv-load](#环境变量加载) 被设置,`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#env-集成) 以获得更多信息。 如果 [dotenv-load](#环境变量加载) 被设置,`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#环境变量加载) 以获得更多信息。
#### 从环境变量中设置 `just` 变量 #### 从环境变量中设置 `just` 变量

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
cargo build
for script in completions/*; do
shell=${script##*.}
if [ $shell == nu ]; then
continue
fi
./target/debug/just --completions $shell > $script
done

View File

@ -1,165 +0,0 @@
_just() {
local i cur prev words cword opts cmd
COMPREPLY=()
# Modules use "::" as the separator, which is considered a wordbreak character in bash.
# The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without
# modifying the global COMP_WORDBREAKS environment variable.
if type _get_comp_words_by_ref &>/dev/null; then
_get_comp_words_by_ref -n : cur prev words cword
else
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
words=$COMP_WORDS
cword=$COMP_CWORD
fi
cmd=""
opts=""
for i in ${words[@]}
do
case "${cmd},${i}" in
",$1")
cmd="just"
;;
*)
;;
esac
done
case "${cmd}" in
just)
opts="-n -f -q -u -v -d -c -e -l -s -E -g -h -V --check --chooser --color --command-color --yes --dry-run --dump-format --highlight --list-heading --list-prefix --no-aliases --no-deps --no-dotenv --no-highlight --justfile --quiet --set --shell --shell-arg --shell-command --clear-shell-args --unsorted --unstable --verbose --working-directory --changelog --choose --command --completions --dump --edit --evaluate --fmt --init --list --groups --man --show --summary --variables --dotenv-filename --dotenv-path --global-justfile --timestamp --timestamp-format --help --version [ARGUMENTS]..."
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
elif [[ ${cword} -eq 1 ]]; then
local recipes=$(just --summary 2> /dev/null)
if echo "${cur}" | \grep -qF '/'; then
local path_prefix=$(echo "${cur}" | sed 's/[/][^/]*$/\//')
local recipes=$(just --summary 2> /dev/null -- "${path_prefix}")
local recipes=$(printf "${path_prefix}%s\t" $recipes)
fi
if [[ $? -eq 0 ]]; then
COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
if type __ltrim_colon_completions &>/dev/null; then
__ltrim_colon_completions "$cur"
fi
return 0
fi
fi
case "${prev}" in
--chooser)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
return 0
;;
--command-color)
COMPREPLY=($(compgen -W "black blue cyan green purple red yellow" -- "${cur}"))
return 0
;;
--dump-format)
COMPREPLY=($(compgen -W "just json" -- "${cur}"))
return 0
;;
--list-heading)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-prefix)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--justfile)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--set)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell-arg)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--working-directory)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--command)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--completions)
COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}"))
return 0
;;
--list)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-l)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--show)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dotenv-filename)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dotenv-path)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-E)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--timestamp-format)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _just -o nosort -o bashdefault -o default just
else
complete -F _just -o bashdefault -o default just
fi

View File

@ -1,84 +0,0 @@
use builtin;
use str;
set edit:completion:arg-completer[just] = {|@words|
fn spaces {|n|
builtin:repeat $n ' ' | str:join ''
}
fn cand {|text desc|
edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
}
var command = 'just'
for word $words[1..-1] {
if (str:has-prefix $word '-') {
break
}
set command = $command';'$word
}
var completions = [
&'just'= {
cand --chooser 'Override binary invoked by `--choose`'
cand --color 'Print colorful output'
cand --command-color 'Echo recipe lines in <COMMAND-COLOR>'
cand --dump-format 'Dump justfile as <FORMAT>'
cand --list-heading 'Print <TEXT> before list'
cand --list-prefix 'Print <TEXT> before each list item'
cand -f 'Use <JUSTFILE> as justfile'
cand --justfile 'Use <JUSTFILE> as justfile'
cand --set 'Override <VARIABLE> with <VALUE>'
cand --shell 'Invoke <SHELL> to run recipes'
cand --shell-arg 'Invoke shell with <SHELL-ARG> as an argument'
cand -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand --working-directory 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --completions 'Print shell completion script for <SHELL>'
cand -l 'List available recipes'
cand --list 'List available recipes'
cand -s 'Show recipe at <PATH>'
cand --show 'Show recipe at <PATH>'
cand --dotenv-filename 'Search for environment file named <DOTENV-FILENAME> instead of `.env`'
cand -E 'Load <DOTENV-PATH> as environment file instead of searching for one'
cand --dotenv-path 'Load <DOTENV-PATH> as environment file instead of searching for one'
cand --timestamp-format 'Timestamp format string'
cand --check 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
cand --yes 'Automatically confirm all recipes.'
cand -n 'Print what just would do without doing it'
cand --dry-run 'Print what just would do without doing it'
cand --highlight 'Highlight echoed recipe lines in bold'
cand --no-aliases 'Don''t show aliases in list'
cand --no-deps 'Don''t run recipe dependencies'
cand --no-dotenv 'Don''t load `.env` file'
cand --no-highlight 'Don''t highlight echoed recipe lines in bold'
cand -q 'Suppress all output'
cand --quiet 'Suppress all output'
cand --shell-command 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
cand --clear-shell-args 'Clear shell arguments'
cand -u 'Return list and summary entries in source order'
cand --unsorted 'Return list and summary entries in source order'
cand --unstable 'Enable unstable features'
cand -v 'Use verbose output'
cand --verbose 'Use verbose output'
cand --changelog 'Print changelog'
cand --choose '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`'
cand --dump 'Print justfile'
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.'
cand --fmt 'Format and overwrite justfile'
cand --init 'Initialize new justfile in project root'
cand --groups 'List recipe groups'
cand --man 'Print man page'
cand --summary 'List names of available recipes'
cand --variables 'List names of variables'
cand -g 'Use global justfile'
cand --global-justfile 'Use global justfile'
cand --timestamp 'Print recipe command timestamps'
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]
}

View File

@ -1,84 +0,0 @@
function __fish_just_complete_recipes
just --list 2> /dev/null | tail -n +2 | awk '{
command = $1;
args = $0;
desc = "";
delim = "";
sub(/^[[:space:]]*[^[:space:]]*/, "", args);
gsub(/^[[:space:]]+|[[:space:]]+$/, "", args);
if (match(args, /#.*/)) {
desc = substr(args, RSTART+2, RLENGTH);
args = substr(args, 0, RSTART-1);
gsub(/^[[:space:]]+|[[:space:]]+$/, "", args);
}
gsub(/\+|=[`\'"][^`\'"]*[`\'"]/, "", args);
gsub(/ /, ",", args);
if (args != ""){
args = "Args: " args;
}
if (args != "" && desc != "") {
delim = "; ";
}
print command "\t" args delim desc
}'
end
# don't suggest files right off
complete -c just -n "__fish_is_first_arg" --no-files
# complete recipes
complete -c just -a '(__fish_just_complete_recipes)'
# autogenerated completions
complete -c just -l chooser -d 'Override binary invoked by `--choose`' -r
complete -c just -l color -d 'Print colorful output' -r -f -a "{auto '',always '',never ''}"
complete -c just -l command-color -d 'Echo recipe lines in <COMMAND-COLOR>' -r -f -a "{black '',blue '',cyan '',green '',purple '',red '',yellow ''}"
complete -c just -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a "{just '',json ''}"
complete -c just -l list-heading -d 'Print <TEXT> before list' -r
complete -c just -l list-prefix -d 'Print <TEXT> before each list item' -r
complete -c just -s f -l justfile -d 'Use <JUSTFILE> as justfile' -r -F
complete -c just -l set -d 'Override <VARIABLE> with <VALUE>' -r
complete -c just -l shell -d 'Invoke <SHELL> to run recipes' -r
complete -c just -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument' -r
complete -c just -s d -l working-directory -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set' -r -F
complete -c just -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -r
complete -c just -l completions -d 'Print shell completion script for <SHELL>' -r -f -a "{bash '',elvish '',fish '',powershell '',zsh ''}"
complete -c just -s l -l list -d 'List available recipes' -r
complete -c just -s s -l show -d 'Show recipe at <PATH>' -r
complete -c just -l dotenv-filename -d 'Search for environment file named <DOTENV-FILENAME> instead of `.env`' -r
complete -c just -s E -l dotenv-path -d 'Load <DOTENV-PATH> as environment file instead of searching for one' -r -F
complete -c just -l timestamp-format -d 'Timestamp format string' -r
complete -c just -l check -d 'Run `--fmt` in \'check\' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
complete -c just -l yes -d 'Automatically confirm all recipes.'
complete -c just -s n -l dry-run -d 'Print what just would do without doing it'
complete -c just -l highlight -d 'Highlight echoed recipe lines in bold'
complete -c just -l no-aliases -d 'Don\'t show aliases in list'
complete -c just -l no-deps -d 'Don\'t run recipe dependencies'
complete -c just -l no-dotenv -d 'Don\'t load `.env` file'
complete -c just -l no-highlight -d 'Don\'t highlight echoed recipe lines in bold'
complete -c just -s q -l quiet -d 'Suppress all output'
complete -c just -l shell-command -d 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
complete -c just -l clear-shell-args -d 'Clear shell arguments'
complete -c just -s u -l unsorted -d 'Return list and summary entries in source order'
complete -c just -l unstable -d 'Enable unstable features'
complete -c just -s v -l verbose -d 'Use verbose output'
complete -c just -l changelog -d 'Print changelog'
complete -c just -l choose -d '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`'
complete -c just -l dump -d 'Print justfile'
complete -c just -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
complete -c just -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.'
complete -c just -l fmt -d 'Format and overwrite justfile'
complete -c just -l init -d 'Initialize new justfile in project root'
complete -c just -l groups -d 'List recipe groups'
complete -c just -l man -d 'Print man page'
complete -c just -l summary -d 'List names of available recipes'
complete -c just -l variables -d 'List names of variables'
complete -c just -s g -l global-justfile -d 'Use global justfile'
complete -c just -l timestamp -d 'Print recipe command timestamps'
complete -c just -s h -l help -d 'Print help'
complete -c just -s V -l version -d 'Print version'

View File

@ -1,8 +0,0 @@
def "nu-complete just" [] {
(^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description
}
# Just: A Command Runner
export extern "just" [
...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s)
]

View File

@ -1,110 +0,0 @@
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'just'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-') -or
$element.Value -eq $wordToComplete) {
break
}
$element.Value
}) -join ';'
$completions = @(switch ($command) {
'just' {
[CompletionResult]::new('--chooser', 'chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')
[CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
[CompletionResult]::new('--command-color', 'command-color', [CompletionResultType]::ParameterName, 'Echo recipe lines in <COMMAND-COLOR>')
[CompletionResult]::new('--dump-format', 'dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as <FORMAT>')
[CompletionResult]::new('--list-heading', 'list-heading', [CompletionResultType]::ParameterName, 'Print <TEXT> before list')
[CompletionResult]::new('--list-prefix', 'list-prefix', [CompletionResultType]::ParameterName, 'Print <TEXT> before each list item')
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')
[CompletionResult]::new('--justfile', 'justfile', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')
[CompletionResult]::new('--set', 'set', [CompletionResultType]::ParameterName, 'Override <VARIABLE> with <VALUE>')
[CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Invoke <SHELL> to run recipes')
[CompletionResult]::new('--shell-arg', 'shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with <SHELL-ARG> as an argument')
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('--working-directory', 'working-directory', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--command', 'command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes')
[CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')
[CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')
[CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named <DOTENV-FILENAME> instead of `.env`')
[CompletionResult]::new('-E', 'E ', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')
[CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')
[CompletionResult]::new('--timestamp-format', 'timestamp-format', [CompletionResultType]::ParameterName, 'Timestamp format string')
[CompletionResult]::new('--check', 'check', [CompletionResultType]::ParameterName, 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.')
[CompletionResult]::new('--yes', 'yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.')
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--dry-run', 'dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')
[CompletionResult]::new('--no-aliases', 'no-aliases', [CompletionResultType]::ParameterName, 'Don''t show aliases in list')
[CompletionResult]::new('--no-deps', 'no-deps', [CompletionResultType]::ParameterName, 'Don''t run recipe dependencies')
[CompletionResult]::new('--no-dotenv', 'no-dotenv', [CompletionResultType]::ParameterName, 'Don''t load `.env` file')
[CompletionResult]::new('--no-highlight', 'no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold')
[CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--shell-command', 'shell-command', [CompletionResultType]::ParameterName, 'Invoke <COMMAND> with the shell used to run recipe lines and backticks')
[CompletionResult]::new('--clear-shell-args', 'clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments')
[CompletionResult]::new('-u', 'u', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
[CompletionResult]::new('--unsorted', 'unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
[CompletionResult]::new('--unstable', 'unstable', [CompletionResultType]::ParameterName, 'Enable unstable features')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--changelog', 'changelog', [CompletionResultType]::ParameterName, 'Print changelog')
[CompletionResult]::new('--choose', 'choose', [CompletionResultType]::ParameterName, '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`')
[CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print justfile')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--evaluate', 'evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.')
[CompletionResult]::new('--fmt', 'fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile')
[CompletionResult]::new('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')
[CompletionResult]::new('--groups', 'groups', [CompletionResultType]::ParameterName, 'List recipe groups')
[CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page')
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
[CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables')
[CompletionResult]::new('-g', 'g', [CompletionResultType]::ParameterName, 'Use global justfile')
[CompletionResult]::new('--global-justfile', 'global-justfile', [CompletionResultType]::ParameterName, 'Use global justfile')
[CompletionResult]::new('--timestamp', 'timestamp', [CompletionResultType]::ParameterName, 'Print recipe command timestamps')
[CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})
function Get-JustFileRecipes([string[]]$CommandElements) {
$justFileIndex = $commandElements.IndexOf("--justfile");
if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) {
$justFileLocation = $commandElements[$justFileIndex + 1]
}
$justArgs = @("--summary")
if (Test-Path $justFileLocation) {
$justArgs += @("--justfile", $justFileLocation)
}
$recipes = $(just @justArgs) -split ' '
return $recipes | ForEach-Object { [CompletionResult]::new($_) }
}
$elementValues = $commandElements | Select-Object -ExpandProperty Value
$recipes = Get-JustFileRecipes -CommandElements $elementValues
$completions += $recipes
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}

View File

@ -1,171 +0,0 @@
#compdef just
autoload -U is-at-least
_just() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
local common=(
'--chooser=[Override binary invoked by \`--choose\`]: : ' \
'--color=[Print colorful output]: :(auto always never)' \
'--command-color=[Echo recipe lines in <COMMAND-COLOR>]: :(black blue cyan green purple red yellow)' \
'--dump-format=[Dump justfile as <FORMAT>]:FORMAT:(just json)' \
'--list-heading=[Print <TEXT> before list]:TEXT: ' \
'--list-prefix=[Print <TEXT> before each list item]:TEXT: ' \
'-f+[Use <JUSTFILE> as justfile]: :_files' \
'--justfile=[Use <JUSTFILE> as justfile]: :_files' \
'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \
'--shell=[Invoke <SHELL> to run recipes]: : ' \
'*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]: : ' \
'-d+[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \
'--working-directory=[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \
'*-c+[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \
'*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \
'*--completions=[Print shell completion script for <SHELL>]:SHELL:(bash elvish fish powershell zsh)' \
'()-l+[List available recipes]' \
'()--list=[List available recipes]' \
'-s+[Show recipe at <PATH>]: :(_just_commands)' \
'--show=[Show recipe at <PATH>]: :(_just_commands)' \
'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of \`.env\`]: : ' \
'-E+[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \
'--dotenv-path=[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \
'--timestamp-format=[Timestamp format string]: : ' \
'--check[Run \`--fmt\` in '\''check'\'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.]' \
'--yes[Automatically confirm all recipes.]' \
'(-q --quiet)-n[Print what just would do without doing it]' \
'(-q --quiet)--dry-run[Print what just would do without doing it]' \
'--highlight[Highlight echoed recipe lines in bold]' \
'--no-aliases[Don'\''t show aliases in list]' \
'--no-deps[Don'\''t run recipe dependencies]' \
'--no-dotenv[Don'\''t load \`.env\` file]' \
'--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \
'(-n --dry-run)-q[Suppress all output]' \
'(-n --dry-run)--quiet[Suppress all output]' \
'--shell-command[Invoke <COMMAND> with the shell used to run recipe lines and backticks]' \
'--clear-shell-args[Clear shell arguments]' \
'-u[Return list and summary entries in source order]' \
'--unsorted[Return list and summary entries in source order]' \
'--unstable[Enable unstable features]' \
'*-v[Use verbose output]' \
'*--verbose[Use verbose output]' \
'--changelog[Print changelog]' \
'--choose[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\`]' \
'--dump[Print justfile]' \
'-e[Edit justfile with editor given by \$VISUAL or \$EDITOR, falling back to \`vim\`]' \
'--edit[Edit justfile with editor given by \$VISUAL or \$EDITOR, falling back to \`vim\`]' \
'--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \
'--fmt[Format and overwrite justfile]' \
'--init[Initialize new justfile in project root]' \
'--groups[List recipe groups]' \
'--man[Print man page]' \
'--summary[List names of available recipes]' \
'--variables[List names of variables]' \
'(-f --justfile -d --working-directory)-g[Use global justfile]' \
'(-f --justfile -d --working-directory)--global-justfile[Use global justfile]' \
'--timestamp[Print recipe command timestamps]' \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
)
_arguments "${_arguments_options[@]}" $common \
'1: :_just_commands' \
'*: :->args' \
&& ret=0
case $state in
args)
curcontext="${curcontext%:*}-${words[2]}:"
local lastarg=${words[${#words}]}
local recipe
local cmds; cmds=(
${(s: :)$(_call_program commands just --summary)}
)
# Find first recipe name
for ((i = 2; i < $#words; i++ )) do
if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then
recipe=${words[i]}
break
fi
done
if [[ $lastarg = */* ]]; then
# Arguments contain slash would be recognised as a file
_arguments -s -S $common '*:: :_files'
elif [[ $lastarg = *=* ]]; then
# Arguments contain equal would be recognised as a variable
_message "value"
elif [[ $recipe ]]; then
# Show usage message
_message "`just --show $recipe`"
# Or complete with other commands
#_arguments -s -S $common '*:: :_just_commands'
else
_arguments -s -S $common '*:: :_just_commands'
fi
;;
esac
return ret
}
(( $+functions[_just_commands] )) ||
_just_commands() {
[[ $PREFIX = -* ]] && return 1
integer ret=1
local variables; variables=(
${(s: :)$(_call_program commands just --variables)}
)
local commands; commands=(
${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
)
if compset -P '*='; then
case "${${words[-1]%=*}#*=}" in
*) _message 'value' && ret=0 ;;
esac
else
_describe -t variables 'variables' variables -qS "=" && ret=0
_describe -t commands 'just commands' commands "$@"
fi
}
if [ "$funcstack[1]" = "_just" ]; then
(( $+functions[_just_variables] )) ||
_just_variables() {
[[ $PREFIX = -* ]] && return 1
integer ret=1
local variables; variables=(
${(s: :)$(_call_program commands just --variables)}
)
if compset -P '*='; then
case "${${words[-1]%=*}#*=}" in
*) _message 'value' && ret=0 ;;
esac
else
_describe -t variables 'variables' variables && ret=0
fi
return ret
}
_just "$@"
else
compdef _just just
fi

View File

@ -31,12 +31,8 @@ fn main() {
&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}))"),
contributor => {
format!("([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))") format!("([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))")
}
}
}, },
), ),
) )

View File

@ -43,6 +43,7 @@ shellcheck:
shellcheck www/install.sh shellcheck www/install.sh
man: man:
mkdir -p man
cargo run -- --man > man/just.1 cargo run -- --man > man/just.1
view-man: man view-man: man
@ -164,9 +165,6 @@ watch-readme:
just render-readme just render-readme
fswatch -ro README.adoc | xargs -n1 -I{} just render-readme fswatch -ro README.adoc | xargs -n1 -I{} just render-readme
update-completions:
./bin/update-completions
test-completions: test-completions:
./tests/completions/just.bash ./tests/completions/just.bash

View File

@ -1,195 +0,0 @@
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH just 1 "just 1.27.0"
.SH NAME
just \- 🤖 Just a command runner \- https://github.com/casey/just
.SH SYNOPSIS
\fBjust\fR [\fB\-\-check\fR] [\fB\-\-chooser\fR] [\fB\-\-color\fR] [\fB\-\-command\-color\fR] [\fB\-\-yes\fR] [\fB\-n\fR|\fB\-\-dry\-run\fR] [\fB\-\-dump\-format\fR] [\fB\-\-highlight\fR] [\fB\-\-list\-heading\fR] [\fB\-\-list\-prefix\fR] [\fB\-\-no\-aliases\fR] [\fB\-\-no\-deps\fR] [\fB\-\-no\-dotenv\fR] [\fB\-\-no\-highlight\fR] [\fB\-f\fR|\fB\-\-justfile\fR] [\fB\-q\fR|\fB\-\-quiet\fR] [\fB\-\-set\fR] [\fB\-\-shell\fR] [\fB\-\-shell\-arg\fR] [\fB\-\-shell\-command\fR] [\fB\-\-clear\-shell\-args\fR] [\fB\-u\fR|\fB\-\-unsorted\fR] [\fB\-\-unstable\fR] [\fB\-v\fR|\fB\-\-verbose\fR]... [\fB\-d\fR|\fB\-\-working\-directory\fR] [\fB\-\-changelog\fR] [\fB\-\-choose\fR] [\fB\-c\fR|\fB\-\-command\fR] [\fB\-\-completions\fR] [\fB\-\-dump\fR] [\fB\-e\fR|\fB\-\-edit\fR] [\fB\-\-evaluate\fR] [\fB\-\-fmt\fR] [\fB\-\-init\fR] [\fB\-l\fR|\fB\-\-list\fR] [\fB\-\-groups\fR] [\fB\-\-man\fR] [\fB\-s\fR|\fB\-\-show\fR] [\fB\-\-summary\fR] [\fB\-\-variables\fR] [\fB\-\-dotenv\-filename\fR] [\fB\-E\fR|\fB\-\-dotenv\-path\fR] [\fB\-g\fR|\fB\-\-global\-justfile\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIARGUMENTS\fR]
.SH DESCRIPTION
🤖 Just a command runner \- https://github.com/casey/just
.SH OPTIONS
.TP
\fB\-\-check\fR
Run `\-\-fmt` in \*(Aqcheck\*(Aq mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.
.TP
\fB\-\-chooser\fR
Override binary invoked by `\-\-choose`
.RS
May also be specified with the \fBJUST_CHOOSER\fR environment variable.
.RE
.TP
\fB\-\-color\fR [default: auto]
Print colorful output
.br
.br
[\fIpossible values: \fRauto, always, never]
.RS
May also be specified with the \fBJUST_COLOR\fR environment variable.
.RE
.TP
\fB\-\-command\-color\fR
Echo recipe lines in <COMMAND\-COLOR>
.br
.br
[\fIpossible values: \fRblack, blue, cyan, green, purple, red, yellow]
.RS
May also be specified with the \fBJUST_COMMAND_COLOR\fR environment variable.
.RE
.TP
\fB\-\-yes\fR
Automatically confirm all recipes.
.TP
\fB\-n\fR, \fB\-\-dry\-run\fR
Print what just would do without doing it
.RS
May also be specified with the \fBJUST_DRY_RUN\fR environment variable.
.RE
.TP
\fB\-\-dump\-format\fR=\fIFORMAT\fR [default: just]
Dump justfile as <FORMAT>
.br
.br
[\fIpossible values: \fRjust, json]
.TP
\fB\-\-highlight\fR
Highlight echoed recipe lines in bold
.TP
\fB\-\-list\-heading\fR=\fITEXT\fR
Print <TEXT> before list
.TP
\fB\-\-list\-prefix\fR=\fITEXT\fR
Print <TEXT> before each list item
.TP
\fB\-\-no\-aliases\fR
Don\*(Aqt show aliases in list
.TP
\fB\-\-no\-deps\fR
Don\*(Aqt run recipe dependencies
.TP
\fB\-\-no\-dotenv\fR
Don\*(Aqt load `.env` file
.TP
\fB\-\-no\-highlight\fR
Don\*(Aqt highlight echoed recipe lines in bold
.TP
\fB\-f\fR, \fB\-\-justfile\fR
Use <JUSTFILE> as justfile
.RS
May also be specified with the \fBJUST_JUSTFILE\fR environment variable.
.RE
.TP
\fB\-q\fR, \fB\-\-quiet\fR
Suppress all output
.RS
May also be specified with the \fBJUST_QUIET\fR environment variable.
.RE
.TP
\fB\-\-set\fR=\fIVARIABLE VALUE\fR
Override <VARIABLE> with <VALUE>
.TP
\fB\-\-shell\fR
Invoke <SHELL> to run recipes
.TP
\fB\-\-shell\-arg\fR
Invoke shell with <SHELL\-ARG> as an argument
.TP
\fB\-\-shell\-command\fR
Invoke <COMMAND> with the shell used to run recipe lines and backticks
.TP
\fB\-\-clear\-shell\-args\fR
Clear shell arguments
.TP
\fB\-u\fR, \fB\-\-unsorted\fR
Return list and summary entries in source order
.TP
\fB\-\-unstable\fR
Enable unstable features
.RS
May also be specified with the \fBJUST_UNSTABLE\fR environment variable.
.RE
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Use verbose output
.RS
May also be specified with the \fBJUST_VERBOSE\fR environment variable.
.RE
.TP
\fB\-d\fR, \fB\-\-working\-directory\fR
Use <WORKING\-DIRECTORY> as working directory. \-\-justfile must also be set
.RS
May also be specified with the \fBJUST_WORKING_DIRECTORY\fR environment variable.
.RE
.TP
\fB\-\-changelog\fR
Print changelog
.TP
\fB\-\-choose\fR
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`
.TP
\fB\-c\fR, \fB\-\-command\fR
Run an arbitrary command with the working directory, `.env`, overrides, and exports set
.TP
\fB\-\-completions\fR=\fISHELL\fR
Print shell completion script for <SHELL>
.br
.br
[\fIpossible values: \fRbash, elvish, fish, powershell, zsh]
.TP
\fB\-\-dump\fR
Print justfile
.TP
\fB\-e\fR, \fB\-\-edit\fR
Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`
.TP
\fB\-\-evaluate\fR
Evaluate and print all variables. If a variable name is given as an argument, only print that variable\*(Aqs value.
.TP
\fB\-\-fmt\fR
Format and overwrite justfile
.TP
\fB\-\-init\fR
Initialize new justfile in project root
.TP
\fB\-l\fR, \fB\-\-list\fR
List available recipes and their arguments
.TP
\fB\-\-groups\fR
List recipe groups
.TP
\fB\-\-man\fR
Print man page
.TP
\fB\-s\fR, \fB\-\-show\fR=\fIRECIPE\fR
Show information about <RECIPE>
.TP
\fB\-\-summary\fR
List names of available recipes
.TP
\fB\-\-variables\fR
List names of variables
.TP
\fB\-\-dotenv\-filename\fR
Search for environment file named <DOTENV\-FILENAME> instead of `.env`
.TP
\fB\-E\fR, \fB\-\-dotenv\-path\fR
Load <DOTENV\-PATH> as environment file instead of searching for one
.TP
\fB\-g\fR, \fB\-\-global\-justfile\fR
Use global justfile
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help
.TP
\fB\-V\fR, \fB\-\-version\fR
Print version
.TP
[\fIARGUMENTS\fR]
Overrides and recipe(s) to run, defaulting to the first recipe in the justfile
.SH VERSION
v1.27.0
.SH AUTHORS
Casey Rodarmor <casey@rodarmor.com>

View File

@ -3,7 +3,7 @@ use super::*;
/// An alias, e.g. `name := target` /// An alias, e.g. `name := target`
#[derive(Debug, PartialEq, Clone, Serialize)] #[derive(Debug, PartialEq, Clone, Serialize)]
pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> { pub(crate) struct Alias<'src, T = Rc<Recipe<'src>>> {
pub(crate) attributes: BTreeSet<Attribute<'src>>, pub(crate) attributes: AttributeSet<'src>,
pub(crate) name: Name<'src>, pub(crate) name: Name<'src>,
#[serde( #[serde(
bound(serialize = "T: Keyed<'src>"), bound(serialize = "T: Keyed<'src>"),

View File

@ -37,6 +37,8 @@ impl<'src> Analyzer<'src> {
let mut modules: Table<Justfile> = Table::new(); let mut modules: Table<Justfile> = Table::new();
let mut unexports: HashSet<String> = HashSet::new();
let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new(); let mut definitions: HashMap<&str, (&'static str, Name)> = HashMap::new();
let mut define = |name: Name<'src>, let mut define = |name: Name<'src>,
@ -77,7 +79,15 @@ impl<'src> Analyzer<'src> {
assignments.push(assignment); assignments.push(assignment);
} }
Item::Comment(_) => (), Item::Comment(_) => (),
Item::Import { absolute, .. } => { Item::Import {
absolute,
attributes,
..
} => {
//TODO check attributes for validity
let _groups = attributes.groups();
if let Some(absolute) = absolute { if let Some(absolute) = absolute {
stack.push(asts.get(absolute).unwrap()); stack.push(asts.get(absolute).unwrap());
} }
@ -98,6 +108,13 @@ impl<'src> Analyzer<'src> {
self.analyze_set(set)?; self.analyze_set(set)?;
self.sets.insert(set.clone()); self.sets.insert(set.clone());
} }
Item::Unexport { name } => {
if !unexports.insert(name.lexeme().to_string()) {
return Err(name.token.error(DuplicateUnexport {
variable: name.lexeme(),
}));
}
}
} }
} }
@ -109,21 +126,23 @@ impl<'src> Analyzer<'src> {
let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default(); let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default();
for assignment in assignments { for assignment in assignments {
if !settings.allow_duplicate_variables let variable = assignment.name.lexeme();
&& self.assignments.contains_key(assignment.name.lexeme())
{ if !settings.allow_duplicate_variables && self.assignments.contains_key(variable) {
return Err(assignment.name.token.error(DuplicateVariable { return Err(assignment.name.token.error(DuplicateVariable { variable }));
variable: assignment.name.lexeme(),
}));
} }
if self if self
.assignments .assignments
.get(assignment.name.lexeme()) .get(variable)
.map_or(true, |original| assignment.depth <= original.depth) .map_or(true, |original| assignment.depth <= original.depth)
{ {
self.assignments.insert(assignment.clone()); self.assignments.insert(assignment.clone());
} }
if unexports.contains(variable) {
return Err(assignment.name.token.error(ExportUnexported { variable }));
}
} }
AssignmentResolver::resolve_assignments(&self.assignments)?; AssignmentResolver::resolve_assignments(&self.assignments)?;
@ -138,7 +157,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() {
@ -167,6 +186,7 @@ impl<'src> Analyzer<'src> {
recipes, recipes,
settings, settings,
source: root.into(), source: root.into(),
unexports,
warnings, warnings,
}) })
} }
@ -219,7 +239,7 @@ impl<'src> Analyzer<'src> {
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> { fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
let name = alias.name.lexeme(); let name = alias.name.lexeme();
for attribute in &alias.attributes { for attribute in &alias.attributes.inner {
if *attribute != Attribute::Private { if *attribute != Attribute::Private {
return Err(alias.name.token.error(AliasInvalidAttribute { return Err(alias.name.token.error(AliasInvalidAttribute {
alias: name, alias: name,

403
src/argument_parser.rs Normal file
View 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) = &current.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"],
},
],
);
}
}

View File

@ -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 ")?;
} }

View File

@ -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() {

View File

@ -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,
@ -57,9 +59,7 @@ impl<'src> Attribute<'src> {
})?; })?;
let found = argument.as_ref().iter().count(); let found = argument.as_ref().iter().count();
let range = discriminant.argument_range(); let range = discriminant.argument_range();
if !range.contains(&found) { if !range.contains(&found) {
return Err( return Err(
name.error(CompileErrorKind::AttributeArgumentCountMismatch { name.error(CompileErrorKind::AttributeArgumentCountMismatch {
@ -80,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,
@ -100,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,
@ -108,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})")?;
@ -118,6 +120,49 @@ impl<'src> Display for Attribute<'src> {
} }
} }
#[derive(Debug, Clone, PartialEq, Serialize)]
pub(crate) struct AttributeSet<'src> {
#[serde(flatten)]
pub(crate) inner: BTreeSet<Attribute<'src>>,
}
impl<'src> AttributeSet<'src> {
pub(crate) fn empty() -> Self {
Self {
inner: BTreeSet::new(),
}
}
pub(crate) fn from_map<T>(input: BTreeMap<Attribute<'src>, T>) -> Self {
Self {
inner: input.into_keys().collect(),
}
}
pub(crate) fn to_btree_set(self) -> BTreeSet<Attribute<'src>> {
self.inner
}
pub(crate) fn contains(&self, attribute: &Attribute) -> bool {
self.inner.contains(attribute)
}
/// Get the names of all Group attributes defined in this attribute set
pub(crate) fn groups(&self) -> Vec<&StringLiteral<'src>> {
self
.inner
.iter()
.filter_map(|attr| {
if let Attribute::Group(name) = attr {
Some(name)
} else {
None
}
})
.collect()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -1,25 +1,41 @@
use super::*; use super::*;
pub(crate) trait CommandExt { pub(crate) trait CommandExt {
fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope); fn export(
&mut self,
settings: &Settings,
dotenv: &BTreeMap<String, String>,
scope: &Scope,
unexports: &HashSet<String>,
);
fn export_scope(&mut self, settings: &Settings, scope: &Scope); fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>);
} }
impl CommandExt for Command { impl CommandExt for Command {
fn export(&mut self, settings: &Settings, dotenv: &BTreeMap<String, String>, scope: &Scope) { fn export(
&mut self,
settings: &Settings,
dotenv: &BTreeMap<String, String>,
scope: &Scope,
unexports: &HashSet<String>,
) {
for (name, value) in dotenv { for (name, value) in dotenv {
self.env(name, value); self.env(name, value);
} }
if let Some(parent) = scope.parent() { if let Some(parent) = scope.parent() {
self.export_scope(settings, parent); self.export_scope(settings, parent, unexports);
} }
} }
fn export_scope(&mut self, settings: &Settings, scope: &Scope) { fn export_scope(&mut self, settings: &Settings, scope: &Scope, unexports: &HashSet<String>) {
if let Some(parent) = scope.parent() { if let Some(parent) = scope.parent() {
self.export_scope(settings, parent); self.export_scope(settings, parent, unexports);
}
for unexport in unexports {
self.env_remove(unexport);
} }
for binding in scope.bindings() { for binding in scope.bindings() {

View File

@ -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 {
@ -131,6 +131,9 @@ impl Display for CompileError<'_> {
DuplicateVariable { variable } => { DuplicateVariable { variable } => {
write!(f, "Variable `{variable}` has multiple definitions") write!(f, "Variable `{variable}` has multiple definitions")
} }
DuplicateUnexport { variable } => {
write!(f, "Variable `{variable}` is unexported multiple times")
}
ExpectedKeyword { expected, found } => { ExpectedKeyword { expected, found } => {
let expected = List::or_ticked(expected); let expected = List::or_ticked(expected);
if found.kind == TokenKind::Identifier { if found.kind == TokenKind::Identifier {
@ -143,6 +146,9 @@ impl Display for CompileError<'_> {
write!(f, "Expected keyword {expected} but found `{}`", found.kind) write!(f, "Expected keyword {expected} but found `{}`", found.kind)
} }
} }
ExportUnexported { variable } => {
write!(f, "Variable {variable} is both exported and unexported")
}
ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"), ExtraLeadingWhitespace => write!(f, "Recipe line has extra leading whitespace"),
FunctionArgumentCountMismatch { FunctionArgumentCountMismatch {
function, function,

View File

@ -52,10 +52,16 @@ pub(crate) enum CompileErrorKind<'src> {
DuplicateVariable { DuplicateVariable {
variable: &'src str, variable: &'src str,
}, },
DuplicateUnexport {
variable: &'src str,
},
ExpectedKeyword { ExpectedKeyword {
expected: Vec<Keyword>, expected: Vec<Keyword>,
found: Token<'src>, found: Token<'src>,
}, },
ExportUnexported {
variable: &'src str,
},
ExtraLeadingWhitespace, ExtraLeadingWhitespace,
FunctionArgumentCountMismatch { FunctionArgumentCountMismatch {
function: &'src str, function: &'src str,

View File

@ -79,6 +79,7 @@ impl Compiler {
absolute, absolute,
optional, optional,
path, path,
attributes: _,
} => { } => {
let import = current let import = current
.path .path

View File

@ -1,4 +1,102 @@
pub(crate) const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes use {super::*, clap::ValueEnum};
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq)]
pub(crate) enum Shell {
Bash,
Elvish,
Fish,
#[value(alias = "nu")]
Nushell,
Powershell,
Zsh,
}
impl Shell {
pub(crate) fn script(self) -> RunResult<'static, String> {
match self {
Self::Bash => completions::clap(clap_complete::Shell::Bash),
Self::Elvish => completions::clap(clap_complete::Shell::Elvish),
Self::Fish => completions::clap(clap_complete::Shell::Fish),
Self::Nushell => Ok(completions::NUSHELL_COMPLETION_SCRIPT.into()),
Self::Powershell => completions::clap(clap_complete::Shell::PowerShell),
Self::Zsh => completions::clap(clap_complete::Shell::Zsh),
}
}
}
fn clap(shell: clap_complete::Shell) -> RunResult<'static, String> {
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
if let Some(index) = haystack.find(needle) {
haystack.replace_range(index..index + needle.len(), replacement);
Ok(())
} else {
Err(Error::internal(format!(
"Failed to find text:\n{needle}\n…in completion script:\n{haystack}"
)))
}
}
let mut script = {
let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?;
clap_complete::generate(
shell,
&mut crate::config::Config::app(),
env!("CARGO_PKG_NAME"),
&mut tempfile,
);
tempfile
.rewind()
.map_err(|io_error| Error::TempfileIo { io_error })?;
let mut buffer = String::new();
tempfile
.read_to_string(&mut buffer)
.map_err(|io_error| Error::TempfileIo { io_error })?;
buffer
};
match shell {
clap_complete::Shell::Bash => {
for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
clap_complete::Shell::Fish => {
script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS);
}
clap_complete::Shell::PowerShell => {
for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
clap_complete::Shell::Zsh => {
for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
_ => {}
}
Ok(script.trim().into())
}
const NUSHELL_COMPLETION_SCRIPT: &str = r#"def "nu-complete just" [] {
(^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description
}
# Just: A Command Runner
export extern "just" [
...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s)
]"#;
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;
@ -37,9 +135,9 @@ complete -c just -a '(__fish_just_complete_recipes)'
# autogenerated completions # autogenerated completions
"#; "#;
pub(crate) 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=(",
), ),
( (
@ -151,7 +249,7 @@ _just "$@""#,
), ),
]; ];
pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } | r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText"#, Sort-Object -Property ListItemText"#,
r#"function Get-JustFileRecipes([string[]]$CommandElements) { r#"function Get-JustFileRecipes([string[]]$CommandElements) {
@ -178,7 +276,7 @@ pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
Sort-Object -Property ListItemText"#, Sort-Object -Property ListItemText"#,
)]; )];
pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
( (
r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )

View File

@ -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)
} }
} }

View File

@ -7,18 +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`";
pub(crate) fn chooser_default(justfile: &Path) -> OsString {
let mut chooser = OsString::new();
chooser.push("fzf --multi --preview 'just --unstable --color always --justfile \"");
chooser.push(justfile);
chooser.push("\" --show {}'");
chooser
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub(crate) struct Config { pub(crate) struct Config {
pub(crate) check: bool, pub(crate) check: bool,
@ -32,6 +20,7 @@ pub(crate) struct Config {
pub(crate) invocation_directory: PathBuf, pub(crate) invocation_directory: PathBuf,
pub(crate) list_heading: String, pub(crate) list_heading: String,
pub(crate) list_prefix: String, pub(crate) list_prefix: String,
pub(crate) list_submodules: bool,
pub(crate) load_dotenv: bool, pub(crate) load_dotenv: bool,
pub(crate) no_aliases: bool, pub(crate) no_aliases: bool,
pub(crate) no_dependencies: bool, pub(crate) no_dependencies: bool,
@ -97,11 +86,12 @@ mod arg {
pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH"; pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH";
pub(crate) const DRY_RUN: &str = "DRY-RUN"; pub(crate) const DRY_RUN: &str = "DRY-RUN";
pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT"; pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT";
pub(crate) const GLOBAL_JUSTFILE: &str = "GLOBAL_JUSTFILE"; pub(crate) const GLOBAL_JUSTFILE: &str = "GLOBAL-JUSTFILE";
pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT"; pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT";
pub(crate) const JUSTFILE: &str = "JUSTFILE"; pub(crate) const JUSTFILE: &str = "JUSTFILE";
pub(crate) const LIST_HEADING: &str = "LIST-HEADING"; pub(crate) const LIST_HEADING: &str = "LIST-HEADING";
pub(crate) const LIST_PREFIX: &str = "LIST-PREFIX"; pub(crate) const LIST_PREFIX: &str = "LIST-PREFIX";
pub(crate) const LIST_SUBMODULES: &str = "LIST-SUBMODULES";
pub(crate) const NO_ALIASES: &str = "NO-ALIASES"; pub(crate) const NO_ALIASES: &str = "NO-ALIASES";
pub(crate) const NO_DEPS: &str = "NO-DEPS"; pub(crate) const NO_DEPS: &str = "NO-DEPS";
pub(crate) const NO_DOTENV: &str = "NO-DOTENV"; pub(crate) const NO_DOTENV: &str = "NO-DOTENV";
@ -112,7 +102,7 @@ mod arg {
pub(crate) const SHELL_ARG: &str = "SHELL-ARG"; pub(crate) const SHELL_ARG: &str = "SHELL-ARG";
pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND"; pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND";
pub(crate) const TIMESTAMP: &str = "TIMESTAMP"; pub(crate) const TIMESTAMP: &str = "TIMESTAMP";
pub(crate) const TIMESTAMP_FORMAT: &str = "TIMESTAMP_FORMAT"; pub(crate) const TIMESTAMP_FORMAT: &str = "TIMESTAMP-FORMAT";
pub(crate) const UNSORTED: &str = "UNSORTED"; pub(crate) const UNSORTED: &str = "UNSORTED";
pub(crate) const UNSTABLE: &str = "UNSTABLE"; pub(crate) const UNSTABLE: &str = "UNSTABLE";
pub(crate) const VERBOSE: &str = "VERBOSE"; pub(crate) const VERBOSE: &str = "VERBOSE";
@ -161,16 +151,19 @@ impl Config {
.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)
@ -179,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")
@ -196,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')
@ -209,59 +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::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')
@ -271,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')
@ -310,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"),
@ -349,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")
@ -372,10 +435,9 @@ impl Config {
.arg( .arg(
Arg::new(cmd::COMPLETIONS) Arg::new(cmd::COMPLETIONS)
.long("completions") .long("completions")
.action(ArgAction::Append) .action(ArgAction::Set)
.num_args(1..)
.value_name("SHELL") .value_name("SHELL")
.value_parser(value_parser!(clap_complete::Shell)) .value_parser(value_parser!(completions::Shell))
.ignore_case(true) .ignore_case(true)
.help("Print shell completion script for <SHELL>"), .help("Print shell completion script for <SHELL>"),
) )
@ -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> {
@ -560,15 +584,20 @@ impl Config {
} }
} }
fn parse_module_path(path: ValuesRef<String>) -> ConfigResult<ModulePath> { fn parse_module_path(values: ValuesRef<String>) -> ConfigResult<ModulePath> {
let path = values.clone().map(|s| (*s).as_str()).collect::<Vec<&str>>();
let path = if path.len() == 1 && path[0].contains(' ') {
path[0].split_whitespace().collect::<Vec<&str>>()
} else {
path
};
path path
.clone()
.map(|s| (*s).as_str())
.collect::<Vec<&str>>()
.as_slice() .as_slice()
.try_into() .try_into()
.map_err(|()| ConfigError::ModulePath { .map_err(|()| ConfigError::ModulePath {
path: path.cloned().collect(), path: values.cloned().collect(),
}) })
} }
@ -606,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()) {
@ -677,30 +695,12 @@ impl Config {
arguments, arguments,
overrides, overrides,
} }
} else if let Some(&shell) = matches.get_one::<clap_complete::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 {
@ -717,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 {
@ -726,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),
@ -747,19 +755,26 @@ 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),
list_prefix: matches list_prefix: matches
.get_one::<String>(arg::LIST_PREFIX) .get_one::<String>(arg::LIST_PREFIX)
.map_or_else(|| " ".into(), Into::into), .map_or_else(|| " ".into(), Into::into),
list_submodules: matches.get_flag(arg::LIST_SUBMODULES),
load_dotenv: !matches.get_flag(arg::NO_DOTENV), load_dotenv: !matches.get_flag(arg::NO_DOTENV),
no_aliases: matches.get_flag(arg::NO_ALIASES), no_aliases: matches.get_flag(arg::NO_ALIASES),
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),
@ -768,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 {
@ -784,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}");
} }
@ -1245,13 +1264,13 @@ mod tests {
test! { test! {
name: subcommand_completions, name: subcommand_completions,
args: ["--completions", "bash"], args: ["--completions", "bash"],
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
} }
test! { test! {
name: subcommand_completions_uppercase, name: subcommand_completions_uppercase,
args: ["--completions", "BASH"], args: ["--completions", "BASH"],
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
} }
error! { error! {
@ -1534,15 +1553,30 @@ mod tests {
} }
error_matches! { error_matches! {
name: completions_arguments, name: completions_argument,
args: ["--completions", "zsh", "foo"], args: ["--completions", "foo"],
error: error, error: error,
check: { check: {
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
assert_eq!(error.context().collect::<Vec<_>>(), vec![ assert_eq!(error.context().collect::<Vec<_>>(), vec![
(ContextKind::InvalidArg, &ContextValue::String("--completions <SHELL>...".into())), (
(ContextKind::InvalidValue, &ContextValue::String("foo".into())), ContextKind::InvalidArg,
(ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())), &ContextValue::String("--completions <SHELL>".into())),
(
ContextKind::InvalidValue,
&ContextValue::String("foo".into()),
),
(
ContextKind::ValidValue,
&ContextValue::Strings([
"bash".into(),
"elvish".into(),
"fish".into(),
"nushell".into(),
"powershell".into(),
"zsh".into()].into()
),
),
]); ]);
}, },
} }

View File

@ -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 {

View File

@ -20,7 +20,7 @@ pub(crate) enum Error<'src> {
token: Token<'src>, token: Token<'src>,
output_error: OutputError, output_error: OutputError,
}, },
CacheDirIo { RuntimeDirIo {
io_error: io::Error, io_error: io::Error,
path: PathBuf, path: PathBuf,
}, },
@ -79,6 +79,7 @@ pub(crate) enum Error<'src> {
Dotenv { Dotenv {
dotenv_error: dotenvy::Error, dotenv_error: dotenvy::Error,
}, },
DotenvRequired,
DumpJson { DumpJson {
serde_json_error: serde_json::Error, serde_json_error: serde_json::Error,
}, },
@ -94,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>,
@ -161,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 {
@ -286,9 +290,6 @@ impl<'src> ColorDisplay for Error<'src> {
}?, }?,
OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?, OutputError::Utf8(utf8_error) => write!(f, "Backtick succeeded but stdout was not utf8: {utf8_error}")?,
} }
CacheDirIo { io_error, path } => {
write!(f, "I/O error in cache dir `{}`: {io_error}", path.display())?;
}
ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => { ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => {
let chooser = chooser.to_string_lossy(); let chooser = chooser.to_string_lossy();
write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?; write!(f, "Chooser `{shell_binary} {shell_arguments} {chooser}` invocation failed: {io_error}")?;
@ -347,6 +348,9 @@ impl<'src> ColorDisplay for Error<'src> {
Dotenv { dotenv_error } => { Dotenv { dotenv_error } => {
write!(f, "Failed to load environment file: {dotenv_error}")?; write!(f, "Failed to load environment file: {dotenv_error}")?;
} }
DotenvRequired => {
write!(f, "Dotenv file not found")?;
}
DumpJson { serde_json_error } => { DumpJson { serde_json_error } => {
write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?; write!(f, "Failed to dump JSON to stdout: {serde_json_error}")?;
} }
@ -364,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.")?;
} }
@ -403,6 +410,9 @@ impl<'src> ColorDisplay for Error<'src> {
write!(f, "Recipe `{recipe}` was not confirmed")?; write!(f, "Recipe `{recipe}` was not confirmed")?;
} }
RegexCompile { source } => write!(f, "{source}")?, RegexCompile { source } => write!(f, "{source}")?,
RuntimeDirIo { io_error, path } => {
write!(f, "I/O error in runtime dir `{}`: {io_error}", path.display())?;
}
Search { search_error } => Display::fmt(search_error, f)?, Search { search_error } => Display::fmt(search_error, f)?,
Shebang { recipe, command, argument, io_error} => { Shebang { recipe, command, argument, io_error} => {
if let Some(argument) = argument { if let Some(argument) = argument {
@ -443,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}")?;
} }

View File

@ -2,35 +2,58 @@ use super::*;
pub(crate) struct Evaluator<'src: 'run, 'run> { pub(crate) struct Evaluator<'src: 'run, 'run> {
pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>, pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>,
pub(crate) config: &'run Config, pub(crate) context: ExecutionContext<'src, 'run>,
pub(crate) dotenv: &'run BTreeMap<String, String>, pub(crate) is_dependency: bool,
pub(crate) module_source: &'run Path,
pub(crate) scope: Scope<'src, 'run>, pub(crate) scope: Scope<'src, 'run>,
pub(crate) search: &'run Search,
pub(crate) settings: &'run Settings<'run>,
} }
impl<'src, 'run> Evaluator<'src, 'run> { impl<'src, 'run> Evaluator<'src, 'run> {
pub(crate) fn evaluate_assignments( pub(crate) fn evaluate_assignments(
assignments: &'run Table<'src, Assignment<'src>>,
config: &'run Config, config: &'run Config,
dotenv: &'run BTreeMap<String, String>, dotenv: &'run BTreeMap<String, String>,
module_source: &'run Path, module: &'run Justfile<'src>,
scope: Scope<'src, 'run>, overrides: &BTreeMap<String, String>,
parent: &'run Scope<'src, 'run>,
search: &'run Search, search: &'run Search,
settings: &'run Settings<'run>, ) -> RunResult<'src, Scope<'src, 'run>>
) -> RunResult<'src, Scope<'src, 'run>> { where
let mut evaluator = Self { 'src: 'run,
assignments: Some(assignments), {
let context = ExecutionContext {
config, config,
dotenv, dotenv,
module_source, module_source: &module.source,
scope, scope: parent,
search, search,
settings, settings: &module.settings,
unexports: &module.unexports,
}; };
for assignment in assignments.values() { let mut scope = context.scope.child();
let mut unknown_overrides = Vec::new();
for (name, value) in overrides {
if let Some(assignment) = module.assignments.get(name) {
scope.bind(assignment.export, assignment.name, value.clone());
} else {
unknown_overrides.push(name.clone());
}
}
if !unknown_overrides.is_empty() {
return Err(Error::UnknownOverrides {
overrides: unknown_overrides,
});
}
let mut evaluator = Self {
context,
assignments: Some(&module.assignments),
scope,
is_dependency: false,
};
for assignment in module.assignments.values() {
evaluator.evaluate_assignment(assignment)?; evaluator.evaluate_assignment(assignment)?;
} }
@ -152,7 +175,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
} }
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()), Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
Expression::Backtick { contents, token } => { Expression::Backtick { contents, token } => {
if self.config.dry_run { if self.context.config.dry_run {
Ok(format!("`{contents}`")) Ok(format!("`{contents}`"))
} else { } else {
Ok(self.run_backtick(contents, token)?) Ok(self.run_backtick(contents, token)?)
@ -213,13 +236,18 @@ impl<'src, 'run> Evaluator<'src, 'run> {
} }
pub(crate) fn run_command(&self, command: &str, args: &[&str]) -> Result<String, OutputError> { pub(crate) fn run_command(&self, command: &str, args: &[&str]) -> Result<String, OutputError> {
let mut cmd = self.settings.shell_command(self.config); let mut cmd = self.context.settings.shell_command(self.context.config);
cmd.arg(command); cmd.arg(command);
cmd.args(args); cmd.args(args);
cmd.current_dir(&self.search.working_directory); cmd.current_dir(&self.context.search.working_directory);
cmd.export(self.settings, self.dotenv, &self.scope); cmd.export(
self.context.settings,
self.context.dotenv,
&self.scope,
self.context.unexports,
);
cmd.stdin(Stdio::inherit()); cmd.stdin(Stdio::inherit());
cmd.stderr(if self.config.verbosity.quiet() { cmd.stderr(if self.context.config.verbosity.quiet() {
Stdio::null() Stdio::null()
} else { } else {
Stdio::inherit() Stdio::inherit()
@ -253,26 +281,12 @@ impl<'src, 'run> Evaluator<'src, 'run> {
} }
pub(crate) fn evaluate_parameters( pub(crate) fn evaluate_parameters(
context: &ExecutionContext<'src, 'run>,
is_dependency: bool,
arguments: &[String], arguments: &[String],
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
module_source: &'run Path,
parameters: &[Parameter<'src>], parameters: &[Parameter<'src>],
scope: &'run Scope<'src, 'run>,
search: &'run Search,
settings: &'run Settings,
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> { ) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
let mut evaluator = Self { let mut evaluator = Self::new(context, is_dependency, context.scope);
assignments: None,
config,
dotenv,
module_source,
scope: scope.child(),
search,
settings,
};
let mut scope = scope.child();
let mut positional = Vec::new(); let mut positional = Vec::new();
@ -303,28 +317,24 @@ impl<'src, 'run> Evaluator<'src, 'run> {
rest = &rest[1..]; rest = &rest[1..];
value value
}; };
scope.bind(parameter.export, parameter.name, value); evaluator
.scope
.bind(parameter.export, parameter.name, value);
} }
Ok((scope, positional)) Ok((evaluator.scope, positional))
} }
pub(crate) fn recipe_evaluator( pub(crate) fn new(
config: &'run Config, context: &ExecutionContext<'src, 'run>,
dotenv: &'run BTreeMap<String, String>, is_dependency: bool,
module_source: &'run Path,
scope: &'run Scope<'src, 'run>, scope: &'run Scope<'src, 'run>,
search: &'run Search,
settings: &'run Settings,
) -> Self { ) -> Self {
Self { Self {
assignments: None, assignments: None,
config, context: *context,
dotenv, is_dependency,
module_source, scope: scope.child(),
scope: Scope::child(scope),
search,
settings,
} }
} }
} }

View File

@ -1,10 +1,12 @@
use super::*; use super::*;
pub(crate) struct RecipeContext<'src: 'run, 'run> { #[derive(Copy, Clone)]
pub(crate) struct ExecutionContext<'src: 'run, 'run> {
pub(crate) config: &'run Config, pub(crate) config: &'run Config,
pub(crate) dotenv: &'run BTreeMap<String, String>, pub(crate) dotenv: &'run BTreeMap<String, String>,
pub(crate) module_source: &'run Path, pub(crate) module_source: &'run Path,
pub(crate) scope: &'run Scope<'src, 'run>, pub(crate) scope: &'run Scope<'src, 'run>,
pub(crate) search: &'run Search, pub(crate) search: &'run Search,
pub(crate) settings: &'run Settings<'src>, pub(crate) settings: &'run Settings<'src>,
pub(crate) unexports: &'run HashSet<String>,
} }

View File

@ -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()),

View File

@ -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),
@ -59,6 +61,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
"home_directory" => Nullary(|_| dir("home", dirs::home_dir)), "home_directory" => Nullary(|_| dir("home", dirs::home_dir)),
"invocation_directory" => Nullary(invocation_directory), "invocation_directory" => Nullary(invocation_directory),
"invocation_directory_native" => Nullary(invocation_directory_native), "invocation_directory_native" => Nullary(invocation_directory_native),
"is_dependency" => Nullary(is_dependency),
"join" => BinaryPlus(join), "join" => BinaryPlus(join),
"just_executable" => Nullary(just_executable), "just_executable" => Nullary(just_executable),
"just_pid" => Nullary(just_pid), "just_pid" => Nullary(just_pid),
@ -118,9 +121,10 @@ 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
.search .search
.working_directory .working_directory
.join(path) .join(path)
@ -129,12 +133,12 @@ fn absolute_path(context: Context, path: &str) -> Result<String, String> {
Some(absolute_path) => Ok(absolute_path.to_owned()), Some(absolute_path) => Ok(absolute_path.to_owned()),
None => Err(format!( None => Err(format!(
"Working directory is not valid unicode: {}", "Working directory is not valid unicode: {}",
context.evaluator.search.working_directory.display() context.evaluator.context.search.working_directory.display()
)), )),
} }
} }
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}"))
@ -143,16 +147,21 @@ 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.evaluator.search.working_directory.join(path); let path = context
.evaluator
.context
.search
.working_directory
.join(path);
let mut hasher = blake3::Hasher::new(); let mut hasher = blake3::Hasher::new();
hasher hasher
.update_mmap_rayon(&path) .update_mmap_rayon(&path)
@ -160,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}"))?;
@ -172,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 {
@ -184,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());
} }
@ -208,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()
@ -228,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'_')
@ -242,10 +259,10 @@ 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.dotenv.get(key) { if let Some(value) = context.evaluator.context.dotenv.get(key) {
return Ok(value.clone()); return Ok(value.clone());
} }
@ -258,10 +275,10 @@ 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.dotenv.get(key) { if let Some(value) = context.evaluator.context.dotenv.get(key) {
return Ok(value.clone()); return Ok(value.clone());
} }
@ -274,49 +291,50 @@ 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.search.working_directory, &context.evaluator.context.search.working_directory,
&context.evaluator.config.invocation_directory, &context.evaluator.context.config.invocation_directory,
) )
.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
.config .config
.invocation_directory .invocation_directory
.to_str() .to_str()
@ -324,12 +342,21 @@ fn invocation_directory_native(context: Context) -> Result<String, String> {
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"Invocation directory is not valid unicode: {}", "Invocation directory is not valid unicode: {}",
context.evaluator.config.invocation_directory.display() context
.evaluator
.context
.config
.invocation_directory
.display()
) )
}) })
} }
fn prepend(_context: Context, prefix: &str, s: &str) -> Result<String, String> { fn is_dependency(context: Context) -> FunctionResult {
Ok(context.evaluator.is_dependency.to_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}"))
@ -338,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);
@ -346,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}"))?;
@ -358,13 +385,14 @@ 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
.search .search
.justfile .justfile
.to_str() .to_str()
@ -372,16 +400,22 @@ fn justfile(context: Context) -> Result<String, String> {
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"Justfile path is not valid unicode: {}", "Justfile path is not valid unicode: {}",
context.evaluator.search.justfile.display() context.evaluator.context.search.justfile.display()
) )
}) })
} }
fn justfile_directory(context: Context) -> Result<String, String> { fn justfile_directory(context: Context) -> FunctionResult {
let justfile_directory = context.evaluator.search.justfile.parent().ok_or_else(|| { let justfile_directory = context
.evaluator
.context
.search
.justfile
.parent()
.ok_or_else(|| {
format!( format!(
"Could not resolve justfile directory. Justfile `{}` had no parent.", "Could not resolve justfile directory. Justfile `{}` had no parent.",
context.evaluator.search.justfile.display() context.evaluator.context.search.justfile.display()
) )
})?; })?;
@ -396,26 +430,27 @@ 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
.search .search
.justfile .justfile
.parent() .parent()
.unwrap() .unwrap()
.join(context.evaluator.module_source) .join(context.evaluator.context.module_source)
.parent() .parent()
.unwrap() .unwrap()
.to_str() .to_str()
@ -423,53 +458,61 @@ fn module_directory(context: Context) -> Result<String, String> {
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"Module directory is not valid unicode: {}", "Module directory is not valid unicode: {}",
context.evaluator.module_source.parent().unwrap().display(), context
.evaluator
.context
.module_source
.parent()
.unwrap()
.display(),
) )
}) })
} }
fn module_file(context: Context) -> Result<String, String> { fn module_file(context: Context) -> FunctionResult {
context context
.evaluator .evaluator
.context
.search .search
.justfile .justfile
.parent() .parent()
.unwrap() .unwrap()
.join(context.evaluator.module_source) .join(context.evaluator.context.module_source)
.to_str() .to_str()
.map(str::to_owned) .map(str::to_owned)
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"Module file path is not valid unicode: {}", "Module file path is not valid unicode: {}",
context.evaluator.module_source.display(), context.evaluator.context.module_source.display(),
) )
}) })
} }
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
.context
.search .search
.working_directory .working_directory
.join(path) .join(path)
@ -478,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())?
@ -500,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);
@ -508,9 +546,14 @@ 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.evaluator.search.working_directory.join(path); let path = context
.evaluator
.context
.search
.working_directory
.join(path);
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
let mut file = let mut file =
fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?; fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;
@ -520,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>>();
@ -531,21 +574,22 @@ 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
.search .search
.justfile .justfile
.parent() .parent()
@ -563,9 +607,10 @@ 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
.search .search
.justfile .justfile
.parent() .parent()
@ -581,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}`"))?;
@ -639,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>()

View File

@ -7,6 +7,7 @@ pub(crate) enum Item<'src> {
Assignment(Assignment<'src>), Assignment(Assignment<'src>),
Comment(&'src str), Comment(&'src str),
Import { Import {
attributes: AttributeSet<'src>,
absolute: Option<PathBuf>, absolute: Option<PathBuf>,
optional: bool, optional: bool,
path: Token<'src>, path: Token<'src>,
@ -20,6 +21,9 @@ pub(crate) enum Item<'src> {
}, },
Recipe(UnresolvedRecipe<'src>), Recipe(UnresolvedRecipe<'src>),
Set(Set<'src>), Set(Set<'src>),
Unexport {
name: Name<'src>,
},
} }
impl<'src> Display for Item<'src> { impl<'src> Display for Item<'src> {
@ -61,6 +65,7 @@ impl<'src> Display for Item<'src> {
} }
Self::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())), Self::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
Self::Set(set) => write!(f, "{set}"), Self::Set(set) => write!(f, "{set}"),
Self::Unexport { name } => write!(f, "unexport {name}"),
} }
} }
} }

View File

@ -24,6 +24,7 @@ pub(crate) struct Justfile<'src> {
pub(crate) settings: Settings<'src>, pub(crate) settings: Settings<'src>,
#[serde(skip)] #[serde(skip)]
pub(crate) source: PathBuf, pub(crate) source: PathBuf,
pub(crate) unexports: HashSet<String>,
pub(crate) warnings: Vec<Warning>, pub(crate) warnings: Vec<Warning>,
} }
@ -77,45 +78,6 @@ impl<'src> Justfile<'src> {
.next() .next()
} }
fn scope<'run>(
&'run self,
config: &'run Config,
dotenv: &'run BTreeMap<String, String>,
search: &'run Search,
overrides: &BTreeMap<String, String>,
parent: &'run Scope<'src, 'run>,
) -> RunResult<'src, Scope<'src, 'run>>
where
'src: 'run,
{
let mut scope = parent.child();
let mut unknown_overrides = Vec::new();
for (name, value) in overrides {
if let Some(assignment) = self.assignments.get(name) {
scope.bind(assignment.export, assignment.name, value.clone());
} else {
unknown_overrides.push(name.clone());
}
}
if !unknown_overrides.is_empty() {
return Err(Error::UnknownOverrides {
overrides: unknown_overrides,
});
}
Evaluator::evaluate_assignments(
&self.assignments,
config,
dotenv,
&self.source,
scope,
search,
&self.settings,
)
}
pub(crate) fn run( pub(crate) fn run(
&self, &self,
config: &Config, config: &Config,
@ -143,7 +105,7 @@ impl<'src> Justfile<'src> {
let root = Scope::root(); let root = Scope::root();
let scope = self.scope(config, &dotenv, search, overrides, &root)?; let scope = Evaluator::evaluate_assignments(config, &dotenv, self, overrides, &root, search)?;
match &config.subcommand { match &config.subcommand {
Subcommand::Command { Subcommand::Command {
@ -163,7 +125,7 @@ impl<'src> Justfile<'src> {
let scope = scope.child(); let scope = scope.child();
command.export(&self.settings, &dotenv, &scope); command.export(&self.settings, &dotenv, &scope, &self.unexports);
let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| { let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
Error::CommandInvoke { Error::CommandInvoke {
@ -194,11 +156,7 @@ impl<'src> Justfile<'src> {
}); });
} }
} else { } else {
let mut width = 0; let width = scope.names().fold(0, |max, name| name.len().max(max));
for name in scope.names() {
width = cmp::max(name.len(), width);
}
for binding in scope.bindings() { for binding in scope.bindings() {
println!( println!(
@ -215,77 +173,38 @@ 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();
for invocation in invocations { for invocation in invocations {
let context = RecipeContext { let context = ExecutionContext {
config, config,
dotenv: &dotenv, dotenv: &dotenv,
module_source: invocation.module_source, module_source: invocation.module_source,
scope: invocation.scope, scope: invocation.scope,
search, search,
settings: invocation.settings, settings: invocation.settings,
unexports: &self.unexports,
}; };
Self::run_recipe( Self::run_recipe(
@ -298,7 +217,7 @@ impl<'src> Justfile<'src> {
&context, &context,
&mut ran, &mut ran,
invocation.recipe, invocation.recipe,
search, false,
)?; )?;
} }
@ -319,96 +238,56 @@ 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 = module.scope(config, dotenv, search, &BTreeMap::new(), parent)?; let scope = Evaluator::evaluate_assignments(
config,
dotenv,
module,
&BTreeMap::new(),
parent,
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() {
if let Some(recipe) = &module.default {
recipe.check_can_be_default_recipe()?;
return Ok(Some((
Invocation {
settings: &module.settings,
recipe,
arguments: Vec::new(),
scope,
module_source: &self.source,
},
depth,
)));
}
Err(Error::NoDefaultRecipe)
} else {
module.invocation( module.invocation(
depth + 1,
path,
arena, arena,
scopes, arguments,
config, config,
dotenv, dotenv,
search,
scope, scope,
rest[0], path,
&rest[1..], position + 1,
scopes,
search,
) )
} }
} 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)
}
} }
pub(crate) fn name(&self) -> &'src str { pub(crate) fn name(&self) -> &'src str {
@ -417,10 +296,10 @@ impl<'src> Justfile<'src> {
fn run_recipe( fn run_recipe(
arguments: &[String], arguments: &[String],
context: &RecipeContext<'src, '_>, context: &ExecutionContext<'src, '_>,
ran: &mut Ran<'src>, ran: &mut Ran<'src>,
recipe: &Recipe<'src>, recipe: &Recipe<'src>,
search: &Search, is_dependency: bool,
) -> RunResult<'src> { ) -> RunResult<'src> {
if ran.has_run(&recipe.namepath, arguments) { if ran.has_run(&recipe.namepath, arguments) {
return Ok(()); return Ok(());
@ -432,27 +311,12 @@ impl<'src> Justfile<'src> {
}); });
} }
let (outer, positional) = Evaluator::evaluate_parameters( let (outer, positional) =
arguments, Evaluator::evaluate_parameters(context, is_dependency, arguments, &recipe.parameters)?;
context.config,
context.dotenv,
context.module_source,
&recipe.parameters,
context.scope,
search,
context.settings,
)?;
let scope = outer.child(); let scope = outer.child();
let mut evaluator = Evaluator::recipe_evaluator( let mut evaluator = Evaluator::new(context, true, &scope);
context.config,
context.dotenv,
context.module_source,
&scope,
search,
context.settings,
);
if !context.config.no_dependencies { if !context.config.no_dependencies {
for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) { for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
@ -461,11 +325,11 @@ impl<'src> Justfile<'src> {
.map(|argument| evaluator.evaluate_expression(argument)) .map(|argument| evaluator.evaluate_expression(argument))
.collect::<RunResult<Vec<String>>>()?; .collect::<RunResult<Vec<String>>>()?;
Self::run_recipe(&arguments, context, ran, recipe, search)?; Self::run_recipe(&arguments, context, ran, recipe, true)?;
} }
} }
recipe.run(context, &scope, &positional)?; recipe.run(context, &scope, &positional, is_dependency)?;
if !context.config.no_dependencies { if !context.config.no_dependencies {
let mut ran = Ran::default(); let mut ran = Ran::default();
@ -477,7 +341,7 @@ impl<'src> Justfile<'src> {
evaluated.push(evaluator.evaluate_expression(argument)?); evaluated.push(evaluator.evaluate_expression(argument)?);
} }
Self::run_recipe(&evaluated, context, &mut ran, recipe, search)?; Self::run_recipe(&evaluated, context, &mut ran, recipe, true)?;
} }
} }
@ -501,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));
@ -516,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 {
@ -572,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
@ -594,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"),

View File

@ -10,6 +10,7 @@ pub(crate) enum Keyword {
DotenvFilename, DotenvFilename,
DotenvLoad, DotenvLoad,
DotenvPath, DotenvPath,
DotenvRequired,
Else, Else,
Export, Export,
Fallback, Fallback,
@ -24,6 +25,7 @@ pub(crate) enum Keyword {
Shell, Shell,
Tempdir, Tempdir,
True, True,
Unexport,
WindowsPowershell, WindowsPowershell,
WindowsShell, WindowsShell,
X, X,

View File

@ -13,53 +13,100 @@
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,
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding, analyzer::Analyzer,
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation, argument_parser::ArgumentParser,
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler, assignment::Assignment,
condition::Condition, conditional_operator::ConditionalOperator, config::Config, assignment_resolver::AssignmentResolver,
config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter, ast::Ast,
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error, attribute::{Attribute, AttributeSet},
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function, binding::Binding,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item, color::Color,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List, color_display::ColorDisplay,
load_dotenv::load_dotenv, loader::Loader, module_path::ModulePath, name::Name, command_ext::CommandExt,
namepath::Namepath, ordinal::Ordinal, output::output, output_error::OutputError, compilation::Compilation,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform, compile_error::CompileError,
platform_interface::PlatformInterface, position::Position, positional::Positional, ran::Ran, compile_error_kind::CompileErrorKind,
range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext, compiler::Compiler,
recipe_resolver::RecipeResolver, recipe_signature::RecipeSignature, scope::Scope, condition::Condition,
search::Search, search_config::SearchConfig, search_error::SearchError, set::Set, conditional_operator::ConditionalOperator,
setting::Setting, settings::Settings, shebang::Shebang, shell::Shell, config::Config,
show_whitespace::ShowWhitespace, source::Source, string_kind::StringKind, config_error::ConfigError,
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table, constants::constants,
thunk::Thunk, token::Token, token_kind::TokenKind, unresolved_dependency::UnresolvedDependency, count::Count,
unresolved_recipe::UnresolvedRecipe, use_color::UseColor, variables::Variables, delimiter::Delimiter,
verbosity::Verbosity, warning::Warning, dependency::Dependency,
dump_format::DumpFormat,
enclosure::Enclosure,
error::Error,
evaluator::Evaluator,
execution_context::ExecutionContext,
expression::Expression,
fragment::Fragment,
function::Function,
interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler,
item::Item,
justfile::Justfile,
keyed::Keyed,
keyword::Keyword,
lexer::Lexer,
line::Line,
list::List,
load_dotenv::load_dotenv,
loader::Loader,
module_path::ModulePath,
name::Name,
namepath::Namepath,
ordinal::Ordinal,
output::output,
output_error::OutputError,
parameter::Parameter,
parameter_kind::ParameterKind,
parser::Parser,
platform::Platform,
platform_interface::PlatformInterface,
position::Position,
positional::Positional,
ran::Ran,
range_ext::RangeExt,
recipe::Recipe,
recipe_resolver::RecipeResolver,
recipe_signature::RecipeSignature,
scope::Scope,
search::Search,
search_config::SearchConfig,
search_error::SearchError,
set::Set,
setting::Setting,
settings::Settings,
shebang::Shebang,
shell::Shell,
show_whitespace::ShowWhitespace,
source::Source,
string_kind::StringKind,
string_literal::StringLiteral,
subcommand::Subcommand,
suggestion::Suggestion,
table::Table,
thunk::Thunk,
token::Token,
token_kind::TokenKind,
unresolved_dependency::UnresolvedDependency,
unresolved_recipe::UnresolvedRecipe,
use_color::UseColor,
variables::Variables,
verbosity::Verbosity,
warning::Warning,
}, },
std::{
borrow::Cow,
cmp,
collections::{BTreeMap, BTreeSet, HashMap},
env,
ffi::OsString,
fmt::{self, Debug, Display, Formatter},
fs,
io::{self, Write},
iter::{self, FromIterator},
mem,
ops::Deref,
ops::{Index, Range, RangeInclusive},
path::{self, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio},
rc::Rc,
str::{self, Chars},
sync::{Mutex, MutexGuard, OnceLock},
vec,
},
{
camino::Utf8Path, camino::Utf8Path,
derivative::Derivative, derivative::Derivative,
edit_distance::edit_distance, edit_distance::edit_distance,
@ -72,10 +119,30 @@ pub(crate) use {
Serialize, Serializer, Serialize, Serializer,
}, },
snafu::{ResultExt, Snafu}, snafu::{ResultExt, Snafu},
std::{
borrow::Cow,
cmp,
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
env,
ffi::OsString,
fmt::{self, Debug, Display, Formatter},
fs,
io::{self, Read, Seek, Write},
iter::{self, FromIterator},
mem,
ops::Deref,
ops::{Index, Range, RangeInclusive},
path::{self, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio},
rc::Rc,
str::{self, Chars},
sync::{Mutex, MutexGuard, OnceLock},
vec,
},
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr}, strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
tempfile::tempfile,
typed_arena::Arena, typed_arena::Arena,
unicode_width::{UnicodeWidthChar, UnicodeWidthStr}, unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
},
}; };
#[cfg(test)] #[cfg(test)]
@ -87,10 +154,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]
@ -114,6 +182,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;
@ -139,6 +208,7 @@ mod dump_format;
mod enclosure; mod enclosure;
mod error; mod error;
mod evaluator; mod evaluator;
mod execution_context;
mod expression; mod expression;
mod fragment; mod fragment;
mod function; mod function;
@ -169,7 +239,6 @@ mod positional;
mod ran; mod ran;
mod range_ext; mod range_ext;
mod recipe; mod recipe;
mod recipe_context;
mod recipe_resolver; mod recipe_resolver;
mod recipe_signature; mod recipe_signature;
mod run; mod run;

View File

@ -1,7 +1,5 @@
use super::*; use super::*;
const DEFAULT_DOTENV_FILENAME: &str = ".env";
pub(crate) fn load_dotenv( pub(crate) fn load_dotenv(
config: &Config, config: &Config,
settings: &Settings, settings: &Settings,
@ -17,16 +15,22 @@ pub(crate) fn load_dotenv(
.as_ref() .as_ref()
.or(settings.dotenv_path.as_ref()); .or(settings.dotenv_path.as_ref());
if !settings.dotenv_load.unwrap_or_default() && dotenv_filename.is_none() && dotenv_path.is_none() if !settings.dotenv_load
&& dotenv_filename.is_none()
&& dotenv_path.is_none()
&& !settings.dotenv_required
{ {
return Ok(BTreeMap::new()); return Ok(BTreeMap::new());
} }
if let Some(path) = dotenv_path { if let Some(path) = dotenv_path {
return load_from_file(&working_directory.join(path)); let path = working_directory.join(path);
if path.is_file() {
return load_from_file(&path);
}
} }
let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str()); let filename = dotenv_filename.map_or(".env", |s| s.as_str());
for directory in working_directory.ancestors() { for directory in working_directory.ancestors() {
let path = directory.join(filename); let path = directory.join(filename);
@ -35,7 +39,11 @@ pub(crate) fn load_dotenv(
} }
} }
if settings.dotenv_required {
Err(Error::DotenvRequired)
} else {
Ok(BTreeMap::new()) Ok(BTreeMap::new())
}
} }
fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> { fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> {

View File

@ -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);
} }
} }

View File

@ -7,6 +7,13 @@ impl<'src> Namepath<'src> {
pub(crate) fn join(&self, name: Name<'src>) -> Self { pub(crate) fn join(&self, name: Name<'src>) -> Self {
Self(self.0.iter().copied().chain(iter::once(name)).collect()) Self(self.0.iter().copied().chain(iter::once(name)).collect())
} }
pub(crate) fn spaced(&self) -> ModulePath {
ModulePath {
path: self.0.iter().map(|name| name.lexeme().into()).collect(),
spaced: true,
}
}
} }
impl<'src> Display for Namepath<'src> { impl<'src> Display for Namepath<'src> {

View File

@ -54,6 +54,11 @@ impl<'src> Node<'src> for Item<'src> {
} }
Self::Recipe(recipe) => recipe.tree(), Self::Recipe(recipe) => recipe.tree(),
Self::Set(set) => set.tree(), Self::Set(set) => set.tree(),
Self::Unexport { name } => {
let mut unexport = Tree::atom(Keyword::Unexport.lexeme());
unexport.push_mut(name.lexeme().replace('-', "_"));
unexport
}
} }
} }
} }
@ -284,6 +289,7 @@ impl<'src> Node<'src> for Set<'src> {
Setting::AllowDuplicateRecipes(value) Setting::AllowDuplicateRecipes(value)
| Setting::AllowDuplicateVariables(value) | Setting::AllowDuplicateVariables(value)
| Setting::DotenvLoad(value) | Setting::DotenvLoad(value)
| Setting::DotenvRequired(value)
| Setting::Export(value) | Setting::Export(value)
| Setting::Fallback(value) | Setting::Fallback(value)
| Setting::PositionalArguments(value) | Setting::PositionalArguments(value)

View File

@ -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}"),

View File

@ -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))?;
} }

View File

@ -334,26 +334,27 @@ impl<'run, 'src> Parser<'run, 'src> {
} else if self.next_is(Identifier) { } else if self.next_is(Identifier) {
match Keyword::from_lexeme(next.lexeme()) { match Keyword::from_lexeme(next.lexeme()) {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(BTreeSet::new())?)); items.push(Item::Alias(self.parse_alias(AttributeSet::empty())?));
} }
Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
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)
if self.next_are(&[Identifier, Identifier, Eof])
|| self.next_are(&[Identifier, Identifier, Eol]) =>
{
self.presume_keyword(Keyword::Unexport)?;
let name = self.parse_name()?;
self.expect_eol()?;
items.push(Item::Unexport { name });
}
Some(Keyword::Import) Some(Keyword::Import)
if self.next_are(&[Identifier, StringToken]) if self.next_are(&[Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, StringToken]) || self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) => || self.next_are(&[Identifier, QuestionMark]) =>
{ {
self.presume_keyword(Keyword::Import)?; items.push(self.parse_import(AttributeSet::empty())?);
let optional = self.accepted(QuestionMark)?;
let (path, relative) = self.parse_string_literal_token()?;
items.push(Item::Import {
absolute: None,
optional,
path,
relative,
});
} }
Some(Keyword::Mod) Some(Keyword::Mod)
if self.next_are(&[Identifier, Identifier, StringToken]) if self.next_are(&[Identifier, Identifier, StringToken])
@ -399,7 +400,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe( items.push(Item::Recipe(self.parse_recipe(
doc, doc,
false, false,
BTreeSet::new(), AttributeSet::empty(),
)?)); )?));
} }
} }
@ -409,7 +410,7 @@ impl<'run, 'src> Parser<'run, 'src> {
items.push(Item::Recipe(self.parse_recipe( items.push(Item::Recipe(self.parse_recipe(
doc, doc,
true, true,
BTreeSet::new(), AttributeSet::empty(),
)?)); )?));
} else if let Some(attributes) = self.parse_attributes()? { } else if let Some(attributes) = self.parse_attributes()? {
let next_keyword = Keyword::from_lexeme(self.next()?.lexeme()); let next_keyword = Keyword::from_lexeme(self.next()?.lexeme());
@ -417,6 +418,13 @@ impl<'run, 'src> Parser<'run, 'src> {
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => { Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
items.push(Item::Alias(self.parse_alias(attributes)?)); items.push(Item::Alias(self.parse_alias(attributes)?));
} }
Some(Keyword::Import)
if self.next_are(&[Identifier, StringToken])
|| self.next_are(&[Identifier, Identifier, StringToken])
|| self.next_are(&[Identifier, QuestionMark]) =>
{
items.push(self.parse_import(attributes)?);
}
_ => { _ => {
let quiet = self.accepted(At)?; let quiet = self.accepted(At)?;
let doc = pop_doc_comment(&mut items, eol_since_last_comment); let doc = pop_doc_comment(&mut items, eol_since_last_comment);
@ -441,10 +449,22 @@ impl<'run, 'src> Parser<'run, 'src> {
} }
} }
fn parse_import(&mut self, attributes: AttributeSet<'src>) -> CompileResult<'src, Item<'src>> {
self.presume_keyword(Keyword::Import)?;
let optional = self.accepted(QuestionMark)?;
let (path, relative) = self.parse_string_literal_token()?;
Ok(Item::Import {
absolute: None,
attributes,
optional,
path,
relative,
})
}
/// Parse an alias, e.g `alias name := target` /// Parse an alias, e.g `alias name := target`
fn parse_alias( fn parse_alias(
&mut self, &mut self,
attributes: BTreeSet<Attribute<'src>>, attributes: AttributeSet<'src>,
) -> CompileResult<'src, Alias<'src, Name<'src>>> { ) -> CompileResult<'src, Alias<'src, Name<'src>>> {
self.presume_keyword(Keyword::Alias)?; self.presume_keyword(Keyword::Alias)?;
let name = self.parse_name()?; let name = self.parse_name()?;
@ -736,7 +756,7 @@ impl<'run, 'src> Parser<'run, 'src> {
&mut self, &mut self,
doc: Option<&'src str>, doc: Option<&'src str>,
quiet: bool, quiet: bool,
attributes: BTreeSet<Attribute<'src>>, attributes: AttributeSet<'src>,
) -> CompileResult<'src, UnresolvedRecipe<'src>> { ) -> CompileResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?; let name = self.parse_name()?;
@ -798,7 +818,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Ok(Recipe { Ok(Recipe {
shebang: body.first().map_or(false, Line::is_shebang), shebang: body.first().map_or(false, Line::is_shebang),
attributes, attributes: attributes.to_btree_set(),
body, body,
dependencies, dependencies,
doc, doc,
@ -917,6 +937,7 @@ impl<'run, 'src> Parser<'run, 'src> {
Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?)) Some(Setting::AllowDuplicateVariables(self.parse_set_bool()?))
} }
Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)), Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
Keyword::DotenvRequired => Some(Setting::DotenvRequired(self.parse_set_bool()?)),
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)), Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)), Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
@ -974,22 +995,25 @@ impl<'run, 'src> Parser<'run, 'src> {
} }
/// Parse recipe attributes /// Parse recipe attributes
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute<'src>>>> { fn parse_attributes(&mut self) -> CompileResult<'src, Option<AttributeSet<'src>>> {
let mut attributes = BTreeMap::new(); let mut attributes = BTreeMap::new();
while self.accepted(BracketL)? { while self.accepted(BracketL)? {
loop { loop {
let name = self.parse_name()?; let name = self.parse_name()?;
let argument = if self.accepted(ParenL)? { let maybe_argument = if self.accepted(Colon)? {
let argument = self.parse_string_literal()?; let arg = self.parse_string_literal()?;
Some(arg)
} else if self.accepted(ParenL)? {
let arg = self.parse_string_literal()?;
self.expect(ParenR)?; self.expect(ParenR)?;
Some(argument) Some(arg)
} else { } else {
None None
}; };
let attribute = Attribute::new(name, argument)?; let attribute = Attribute::new(name, maybe_argument)?;
if let Some(line) = attributes.get(&attribute) { if let Some(line) = attributes.get(&attribute) {
return Err(name.error(CompileErrorKind::DuplicateAttribute { return Err(name.error(CompileErrorKind::DuplicateAttribute {
@ -1011,7 +1035,7 @@ impl<'run, 'src> Parser<'run, 'src> {
if attributes.is_empty() { if attributes.is_empty() {
Ok(None) Ok(None)
} else { } else {
Ok(Some(attributes.into_keys().collect())) Ok(Some(AttributeSet::from_map(attributes)))
} }
} }
} }
@ -1152,6 +1176,18 @@ mod tests {
tree: (justfile (alias t test)), tree: (justfile (alias t test)),
} }
test! {
name: single_argument_attribute_shorthand,
text: "[group: 'some-group']\nalias t := test",
tree: (justfile (alias t test)),
}
test! {
name: single_argument_attribute_shorthand_multiple_same_line,
text: "[group: 'some-group', group: 'some-other-group']\nalias t := test",
tree: (justfile (alias t test)),
}
test! { test! {
name: aliases_multiple, name: aliases_multiple,
text: "alias t := test\nalias b := build", text: "alias t := test\nalias b := build",

View File

@ -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);

View File

@ -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;
} }

View File

@ -19,6 +19,7 @@ fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: Exit
/// A recipe, e.g. `foo: bar baz` /// A recipe, e.g. `foo: bar baz`
#[derive(PartialEq, Debug, Clone, Serialize)] #[derive(PartialEq, Debug, Clone, Serialize)]
pub(crate) struct Recipe<'src, D = Dependency<'src>> { pub(crate) struct Recipe<'src, D = Dependency<'src>> {
//TODO make this be attributeset too
pub(crate) attributes: BTreeSet<Attribute<'src>>, pub(crate) attributes: BTreeSet<Attribute<'src>>,
pub(crate) body: Vec<Line<'src>>, pub(crate) body: Vec<Line<'src>>,
pub(crate) dependencies: Vec<D>, pub(crate) dependencies: Vec<D>,
@ -106,6 +107,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)
} }
@ -146,9 +151,10 @@ impl<'src, D> Recipe<'src, D> {
pub(crate) fn run<'run>( pub(crate) fn run<'run>(
&self, &self,
context: &RecipeContext<'src, 'run>, context: &ExecutionContext<'src, 'run>,
scope: &Scope<'src, 'run>, scope: &Scope<'src, 'run>,
positional: &[String], positional: &[String],
is_dependency: bool,
) -> RunResult<'src, ()> { ) -> RunResult<'src, ()> {
let config = &context.config; let config = &context.config;
@ -162,14 +168,7 @@ impl<'src, D> Recipe<'src, D> {
); );
} }
let evaluator = Evaluator::recipe_evaluator( let evaluator = Evaluator::new(context, is_dependency, scope);
context.config,
context.dotenv,
context.module_source,
scope,
context.search,
context.settings,
);
if self.shebang { if self.shebang {
self.run_shebang(context, scope, positional, config, evaluator) self.run_shebang(context, scope, positional, config, evaluator)
@ -180,7 +179,7 @@ impl<'src, D> Recipe<'src, D> {
fn run_linewise<'run>( fn run_linewise<'run>(
&self, &self,
context: &RecipeContext<'src, 'run>, context: &ExecutionContext<'src, 'run>,
scope: &Scope<'src, 'run>, scope: &Scope<'src, 'run>,
positional: &[String], positional: &[String],
config: &Config, config: &Config,
@ -269,7 +268,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);
} }
@ -279,7 +278,7 @@ impl<'src, D> Recipe<'src, D> {
cmd.stdout(Stdio::null()); cmd.stdout(Stdio::null());
} }
cmd.export(context.settings, context.dotenv, scope); cmd.export(context.settings, context.dotenv, scope, context.unexports);
match InterruptHandler::guard(|| cmd.status()) { match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) => { Ok(exit_status) => {
@ -312,7 +311,7 @@ impl<'src, D> Recipe<'src, D> {
pub(crate) fn run_shebang<'run>( pub(crate) fn run_shebang<'run>(
&self, &self,
context: &RecipeContext<'src, 'run>, context: &ExecutionContext<'src, 'run>,
scope: &Scope<'src, 'run>, scope: &Scope<'src, 'run>,
positional: &[String], positional: &[String],
config: &Config, config: &Config,
@ -353,9 +352,9 @@ impl<'src, D> Recipe<'src, D> {
let tempdir = match &context.settings.tempdir { let tempdir = match &context.settings.tempdir {
Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)), Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)),
None => { None => {
if let Some(cache_dir) = dirs::cache_dir() { if let Some(runtime_dir) = dirs::runtime_dir() {
let path = cache_dir.join("just"); let path = runtime_dir.join("just");
fs::create_dir_all(&path).map_err(|io_error| Error::CacheDirIo { fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {
io_error, io_error,
path: path.clone(), path: path.clone(),
})?; })?;
@ -421,11 +420,11 @@ 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);
} }
command.export(context.settings, context.dotenv, scope); command.export(context.settings, context.dotenv, scope, context.unexports);
// run it! // run it!
match InterruptHandler::guard(|| command.status()) { match InterruptHandler::guard(|| command.status()) {
@ -478,7 +477,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}")?;
} }

View File

@ -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() {

View File

@ -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);

View File

@ -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)
} }
} }

View File

@ -7,6 +7,7 @@ pub(crate) enum Setting<'src> {
DotenvFilename(String), DotenvFilename(String),
DotenvLoad(bool), DotenvLoad(bool),
DotenvPath(String), DotenvPath(String),
DotenvRequired(bool),
Export(bool), Export(bool),
Fallback(bool), Fallback(bool),
IgnoreComments(bool), IgnoreComments(bool),
@ -19,11 +20,12 @@ 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)
| Self::DotenvLoad(value) | Self::DotenvLoad(value)
| Self::DotenvRequired(value)
| Self::Export(value) | Self::Export(value)
| Self::Fallback(value) | Self::Fallback(value)
| Self::IgnoreComments(value) | Self::IgnoreComments(value)

View File

@ -10,8 +10,9 @@ pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool, pub(crate) allow_duplicate_recipes: bool,
pub(crate) allow_duplicate_variables: bool, pub(crate) allow_duplicate_variables: bool,
pub(crate) dotenv_filename: Option<String>, pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: Option<bool>, pub(crate) dotenv_load: bool,
pub(crate) dotenv_path: Option<PathBuf>, pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dotenv_required: bool,
pub(crate) export: bool, pub(crate) export: bool,
pub(crate) fallback: bool, pub(crate) fallback: bool,
pub(crate) ignore_comments: bool, pub(crate) ignore_comments: bool,
@ -39,11 +40,14 @@ impl<'src> Settings<'src> {
settings.dotenv_filename = Some(filename); settings.dotenv_filename = Some(filename);
} }
Setting::DotenvLoad(dotenv_load) => { Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load); settings.dotenv_load = dotenv_load;
} }
Setting::DotenvPath(path) => { Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path)); settings.dotenv_path = Some(PathBuf::from(path));
} }
Setting::DotenvRequired(dotenv_required) => {
settings.dotenv_required = dotenv_required;
}
Setting::Export(export) => { Setting::Export(export) => {
settings.export = export; settings.export = export;
} }

View File

@ -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 {

View File

@ -1,9 +1,4 @@
use { use {super::*, clap_mangen::Man};
super::*,
clap_mangen::Man,
std::io::{Read, Seek},
tempfile::tempfile,
};
const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n";
@ -20,7 +15,7 @@ pub(crate) enum Subcommand {
overrides: BTreeMap<String, String>, overrides: BTreeMap<String, String>,
}, },
Completions { Completions {
shell: clap_complete::Shell, shell: completions::Shell,
}, },
Dump, Dump,
Edit, Edit,
@ -47,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 {
@ -102,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);
} }
} }
@ -112,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 { .. }
@ -155,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);
@ -197,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() {
@ -219,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(
@ -236,7 +227,15 @@ impl Subcommand {
return Err(Error::NoChoosableRecipes); return Err(Error::NoChoosableRecipes);
} }
let chooser = chooser.map_or_else(|| config::chooser_default(&search.justfile), From::from); let chooser = if let Some(chooser) = chooser {
OsString::from(chooser)
} else {
let mut chooser = OsString::new();
chooser.push("fzf --multi --preview 'just --unstable --color always --justfile \"");
chooser.push(&search.justfile);
chooser.push("\" --show {}'");
chooser
};
let result = justfile let result = justfile
.settings .settings
@ -261,14 +260,15 @@ impl Subcommand {
}; };
for recipe in recipes { for recipe in recipes {
if let Err(io_error) = child writeln!(
.stdin child.stdin.as_mut().unwrap(),
.as_mut() "{}",
.expect("Child was created with piped stdio") recipe.namepath.spaced()
.write_all(format!("{}\n", recipe.namepath).as_bytes()) )
{ .map_err(|io_error| Error::ChooserWrite {
return Err(Error::ChooserWrite { io_error, chooser }); io_error,
} chooser: chooser.clone(),
})?;
} }
let output = match child.wait_with_output() { let output = match child.wait_with_output() {
@ -295,72 +295,12 @@ impl Subcommand {
justfile.run(config, search, overrides, &recipes) justfile.run(config, search, overrides, &recipes)
} }
fn completions(shell: clap_complete::Shell) -> RunResult<'static, ()> { fn completions(shell: completions::Shell) -> RunResult<'static, ()> {
use clap_complete::Shell; println!("{}", shell.script()?);
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
if let Some(index) = haystack.find(needle) {
haystack.replace_range(index..index + needle.len(), replacement);
Ok(())
} else {
Err(Error::internal(format!(
"Failed to find text:\n{needle}\n…in completion script:\n{haystack}"
)))
}
}
let mut script = {
let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?;
clap_complete::generate(
shell,
&mut crate::config::Config::app(),
env!("CARGO_PKG_NAME"),
&mut tempfile,
);
tempfile
.rewind()
.map_err(|io_error| Error::TempfileIo { io_error })?;
let mut buffer = String::new();
tempfile
.read_to_string(&mut buffer)
.map_err(|io_error| Error::TempfileIo { io_error })?;
buffer
};
match shell {
Shell::Bash => {
for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
Shell::Fish => {
script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS);
}
Shell::PowerShell => {
for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
Shell::Zsh => {
for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
_ => {}
}
println!("{}", script.trim());
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)
@ -372,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());
@ -394,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();
@ -439,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() {
@ -459,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())
@ -479,14 +419,22 @@ 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);
Ok(())
}
fn list_module(config: &Config, module: &Justfile, depth: usize) {
let aliases = if config.no_aliases { let aliases = if config.no_aliases {
BTreeMap::new() BTreeMap::new()
} else { } else {
@ -531,7 +479,11 @@ impl Subcommand {
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let list_prefix = config.list_prefix.repeat(depth + 1);
if depth == 0 {
print!("{}", config.list_heading); print!("{}", config.list_heading);
}
let groups = { let groups = {
let mut groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new(); let mut groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new();
@ -548,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!();
} }
@ -556,15 +518,15 @@ impl Subcommand {
let no_groups = groups.contains_key(&None) && groups.len() == 1; let no_groups = groups.contains_key(&None) && groups.len() == 1;
if !no_groups { if !no_groups {
print!("{}", config.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()
@ -579,8 +541,7 @@ impl Subcommand {
if doc.lines().count() > 1 { if doc.lines().count() > 1 {
for line in doc.lines() { for line in doc.lines() {
println!( println!(
"{}{} {}", "{list_prefix}{} {}",
config.list_prefix,
config.color.stdout().doc().paint("#"), config.color.stdout().doc().paint("#"),
config.color.stdout().doc().paint(line), config.color.stdout().doc().paint(line),
); );
@ -589,8 +550,7 @@ impl Subcommand {
} }
print!( print!(
"{}{}", "{list_prefix}{}",
config.list_prefix,
RecipeSignature { name, recipe }.color_display(config.color.stdout()) RecipeSignature { name, recipe }.color_display(config.color.stdout())
); );
@ -610,23 +570,35 @@ impl Subcommand {
} }
} }
for submodule in module.modules(config) { if config.list_submodules {
println!("{}{} ...", config.list_prefix, submodule.name(),); for (i, submodule) in module.modules(config).into_iter().enumerate() {
if i + groups.len() > 0 {
println!();
} }
Ok(()) println!("{list_prefix}{}:", submodule.name());
Self::list_module(config, submodule, depth + 1);
}
} else {
for submodule in module.modules(config) {
println!("{list_prefix}{} ...", submodule.name(),);
}
}
} }
fn show<'src>( fn show<'src>(
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();
@ -640,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),
}) })
} }

View File

@ -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) {

View File

@ -131,7 +131,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!(

View File

@ -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],
}, },
} }

View File

@ -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,

View File

@ -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 {

View File

@ -72,7 +72,7 @@ fn multiple_attributes_one_line_error_message() {
) )
.stderr( .stderr(
" "
error: Expected ']', ',', or '(', but found identifier error: Expected ']', ':', ',', or '(', but found identifier
justfile:1:17 justfile:1:17
1 [macos, windows linux] 1 [macos, windows linux]

View File

@ -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())),

View File

@ -47,16 +47,21 @@ test! {
status: 2, status: 2,
} }
test! { #[test]
name: env_is_loaded, fn env_is_loaded() {
justfile: " Test::new()
.justfile(
"
set dotenv-load set dotenv-load
x: x:
echo XYZ echo XYZ
", ",
args: ("--command", "sh", "-c", "printf $DOTENV_KEY"), )
stdout: "dotenv-value", .args(["--command", "sh", "-c", "printf $DOTENV_KEY"])
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value")
.run();
} }
test! { test! {

View File

@ -1,19 +1,42 @@
use super::*; use super::*;
#[test] #[test]
fn output() { #[cfg(target_os = "linux")]
let tempdir = tempdir(); fn bash() {
let output = Command::new(executable_path("just")) let output = Command::new(executable_path("just"))
.arg("--completions") .args(["--completions", "bash"])
.arg("bash")
.current_dir(tempdir.path())
.output() .output()
.unwrap(); .unwrap();
assert!(output.status.success()); assert!(output.status.success());
let text = String::from_utf8_lossy(&output.stdout); let script = str::from_utf8(&output.stdout).unwrap();
assert!(text.starts_with("_just() {")); let tempdir = tempdir();
let path = tempdir.path().join("just.bash");
fs::write(&path, script).unwrap();
let status = Command::new("./tests/completions/just.bash")
.arg(path)
.status()
.unwrap();
assert!(status.success());
}
#[test]
fn replacements() {
for shell in ["bash", "elvish", "fish", "nushell", "powershell", "zsh"] {
let output = Command::new(executable_path("just"))
.args(["--completions", shell])
.output()
.unwrap();
assert!(
output.status.success(),
"shell completion generation for {shell} failed: {}",
output.status
);
}
} }

View File

@ -18,7 +18,7 @@ reply_equals() {
} }
# --- Initial Setup --- # --- Initial Setup ---
source ./completions/just.bash source "$1"
cd tests/completions cd tests/completions
cargo build cargo build
PATH="$(git rev-parse --show-toplevel)/target/debug:$PATH" PATH="$(git rev-parse --show-toplevel)/target/debug:$PATH"

27
tests/datetime.rs Normal file
View 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();
}

View File

@ -12,40 +12,54 @@ fn dotenv() {
.run(); .run();
} }
test! { #[test]
name: set_false, fn set_false() {
justfile: r#" Test::new()
.justfile(
r#"
set dotenv-load := false set dotenv-load := false
foo: @foo:
if [ -n "${DOTENV_KEY+1}" ]; then echo defined; else echo undefined; fi if [ -n "${DOTENV_KEY+1}" ]; then echo defined; else echo undefined; fi
"#, "#,
stdout: "undefined\n", )
stderr: "if [ -n \"${DOTENV_KEY+1}\" ]; then echo defined; else echo undefined; fi\n", .write(".env", "DOTENV_KEY=dotenv-value")
.stdout("undefined\n")
.run();
} }
test! { #[test]
name: set_implicit, fn set_implicit() {
justfile: r#" Test::new()
.justfile(
"
set dotenv-load set dotenv-load
foo: foo:
echo $DOTENV_KEY echo $DOTENV_KEY
"#, ",
stdout: "dotenv-value\n", )
stderr: "echo $DOTENV_KEY\n", .write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
} }
test! { #[test]
name: set_true, fn set_true() {
justfile: r#" Test::new()
.justfile(
"
set dotenv-load := true set dotenv-load := true
foo: foo:
echo $DOTENV_KEY echo $DOTENV_KEY
"#, ",
stdout: "dotenv-value\n", )
stderr: "echo $DOTENV_KEY\n", .write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
} }
#[test] #[test]
@ -57,28 +71,24 @@ fn no_warning() {
echo ${DOTENV_KEY:-unset} echo ${DOTENV_KEY:-unset}
", ",
) )
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("unset\n") .stdout("unset\n")
.stderr("echo ${DOTENV_KEY:-unset}\n") .stderr("echo ${DOTENV_KEY:-unset}\n")
.run(); .run();
} }
#[test] #[test]
fn path_not_found() { fn dotenv_required() {
Test::new() Test::new()
.justfile( .justfile(
" "
set dotenv-required
foo: foo:
echo $JUST_TEST_VARIABLE
", ",
) )
.args(["--dotenv-path", ".env.prod"]) .stderr("error: Dotenv file not found\n")
.stderr(if cfg!(windows) { .status(1)
"error: Failed to load environment file: The system cannot find the file specified. (os \
error 2)\n"
} else {
"error: Failed to load environment file: No such file or directory (os error 2)\n"
})
.status(EXIT_FAILURE)
.run(); .run();
} }
@ -227,12 +237,12 @@ fn program_argument_has_priority_for_dotenv_filename() {
fn program_argument_has_priority_for_dotenv_path() { fn program_argument_has_priority_for_dotenv_path() {
Test::new() Test::new()
.justfile( .justfile(
r#" "
set dotenv-path := "subdir/.env" set dotenv-path := 'subdir/.env'
foo: foo:
@echo $JUST_TEST_VARIABLE @echo $JUST_TEST_VARIABLE
"#, ",
) )
.tree(tree! { .tree(tree! {
subdir: { subdir: {
@ -257,8 +267,130 @@ fn dotenv_path_is_relative_to_working_directory() {
@echo $DOTENV_KEY @echo $DOTENV_KEY
", ",
) )
.write(".env", "DOTENV_KEY=dotenv-value")
.tree(tree! { subdir: { } }) .tree(tree! { subdir: { } })
.current_dir("subdir") .current_dir("subdir")
.stdout("dotenv-value\n") .stdout("dotenv-value\n")
.run(); .run();
} }
#[test]
fn dotenv_variable_in_recipe() {
Test::new()
.justfile(
"
set dotenv-load
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.run();
}
#[test]
fn dotenv_variable_in_backtick() {
Test::new()
.justfile(
"
set dotenv-load
X:=`echo $DOTENV_KEY`
echo:
echo {{X}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\n")
.stderr("echo dotenv-value\n")
.run();
}
#[test]
fn dotenv_variable_in_function_in_recipe() {
Test::new()
.justfile(
"
set dotenv-load
echo:
echo {{env_var_or_default('DOTENV_KEY', 'foo')}}
echo {{env_var('DOTENV_KEY')}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\ndotenv-value\n")
.stderr("echo dotenv-value\necho dotenv-value\n")
.run();
}
#[test]
fn dotenv_variable_in_function_in_backtick() {
Test::new()
.justfile(
"
set dotenv-load
X:=env_var_or_default('DOTENV_KEY', 'foo')
Y:=env_var('DOTENV_KEY')
echo:
echo {{X}}
echo {{Y}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.stdout("dotenv-value\ndotenv-value\n")
.stderr("echo dotenv-value\necho dotenv-value\n")
.run();
}
#[test]
fn no_dotenv() {
Test::new()
.justfile(
"
X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')
echo:
echo {{X}}
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.arg("--no-dotenv")
.stdout("DEFAULT\n")
.stderr("echo DEFAULT\n")
.run();
}
#[test]
fn dotenv_env_var_override() {
Test::new()
.justfile(
"
echo:
echo $DOTENV_KEY
",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.env("DOTENV_KEY", "not-the-dotenv-value")
.stdout("not-the-dotenv-value\n")
.stderr("echo $DOTENV_KEY\n")
.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();
}

View File

@ -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())),

View File

@ -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();

View File

@ -1027,3 +1027,26 @@ import
) )
.run(); .run();
} }
#[test]
fn is_dependency() {
let justfile = "
alpha: beta
@echo 'alpha {{is_dependency()}}'
beta: && gamma
@echo 'beta {{is_dependency()}}'
gamma:
@echo 'gamma {{is_dependency()}}'
";
Test::new()
.args(["alpha"])
.justfile(justfile)
.stdout("beta true\ngamma true\nalpha false\n")
.run();
Test::new()
.args(["beta"])
.justfile(justfile)
.stdout("beta false\ngamma true\n")
.run();
}

View File

@ -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()
@ -144,3 +185,103 @@ fn list_groups_with_custom_prefix() {
) )
.run(); .run();
} }
#[test]
fn list_groups_with_shorthand_syntax() {
Test::new()
.justfile(
"
[group: 'B']
foo:
[group: 'A', group: 'B']
bar:
",
)
.arg("--groups")
.stdout(
"
Recipe groups:
A
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();
}

View File

@ -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();
}

View File

@ -46,8 +46,9 @@ fn alias() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,
@ -58,6 +59,7 @@ fn alias() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -84,8 +86,9 @@ fn assignment() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -96,6 +99,7 @@ fn assignment() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -136,8 +140,9 @@ fn body() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -148,6 +153,7 @@ fn body() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -200,8 +206,9 @@ fn dependencies() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -212,6 +219,7 @@ fn dependencies() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -302,8 +310,9 @@ fn dependency_argument() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -314,6 +323,7 @@ fn dependency_argument() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -366,8 +376,9 @@ fn duplicate_recipes() {
"allow_duplicate_recipes": true, "allow_duplicate_recipes": true,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -378,6 +389,7 @@ fn duplicate_recipes() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -408,8 +420,9 @@ fn duplicate_variables() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": true, "allow_duplicate_variables": true,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -420,6 +433,7 @@ fn duplicate_variables() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -453,8 +467,9 @@ fn doc_comment() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -465,6 +480,7 @@ fn doc_comment() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -484,8 +500,9 @@ fn empty_justfile() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -496,6 +513,7 @@ fn empty_justfile() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -636,8 +654,9 @@ fn parameters() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -648,6 +667,7 @@ fn parameters() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -721,8 +741,9 @@ fn priors() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -733,6 +754,7 @@ fn priors() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -766,8 +788,9 @@ fn private() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -778,6 +801,7 @@ fn private() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -811,8 +835,9 @@ fn quiet() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -823,6 +848,7 @@ fn quiet() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -870,6 +896,7 @@ fn settings() {
"dotenv_filename": "filename", "dotenv_filename": "filename",
"dotenv_load": true, "dotenv_load": true,
"dotenv_path": "path", "dotenv_path": "path",
"dotenv_required": false,
"export": true, "export": true,
"fallback": true, "fallback": true,
"ignore_comments": true, "ignore_comments": true,
@ -883,6 +910,7 @@ fn settings() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -919,8 +947,9 @@ fn shebang() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -931,6 +960,7 @@ fn shebang() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -964,8 +994,9 @@ fn simple() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"ignore_comments": false, "ignore_comments": false,
@ -976,6 +1007,7 @@ fn simple() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -1012,8 +1044,9 @@ fn attribute() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,
@ -1024,6 +1057,7 @@ fn attribute() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}), }),
); );
@ -1073,8 +1107,9 @@ fn module() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,
@ -1085,6 +1120,7 @@ fn module() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
}, },
}, },
@ -1093,8 +1129,9 @@ fn module() {
"allow_duplicate_recipes": false, "allow_duplicate_recipes": false,
"allow_duplicate_variables": false, "allow_duplicate_variables": false,
"dotenv_filename": null, "dotenv_filename": null,
"dotenv_load": null, "dotenv_load": false,
"dotenv_path": null, "dotenv_path": null,
"dotenv_required": false,
"export": false, "export": false,
"fallback": false, "fallback": false,
"positional_arguments": false, "positional_arguments": false,
@ -1105,6 +1142,7 @@ fn module() {
"windows_powershell": false, "windows_powershell": false,
"windows_shell": null, "windows_shell": null,
}, },
"unexports": [],
"warnings": [], "warnings": [],
})) }))
.unwrap() .unwrap()

View File

@ -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;
@ -104,7 +104,10 @@ mod summary;
mod tempdir; mod tempdir;
mod timestamps; mod timestamps;
mod undefined_variables; mod undefined_variables;
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;

View File

@ -223,3 +223,133 @@ fn list_unknown_submodule() {
.status(1) .status(1)
.run(); .run();
} }
#[test]
fn list_with_groups_in_modules() {
Test::new()
.justfile(
"
[group('FOO')]
foo:
mod bar
",
)
.write("bar.just", "[group('BAZ')]\nbaz:")
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.stdout(
"
Available recipes:
[FOO]
foo
bar:
[BAZ]
baz
",
)
.run();
}
#[test]
fn list_displays_recipes_in_submodules() {
Test::new()
.write("foo.just", "bar:\n @echo FOO")
.justfile(
"
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.stdout(
"
Available recipes:
foo:
bar
",
)
.run();
}
#[test]
fn modules_are_space_separated_in_output() {
Test::new()
.write("foo.just", "foo:")
.write("bar.just", "bar:")
.justfile(
"
mod foo
mod bar
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.stdout(
"
Available recipes:
bar:
bar
foo:
foo
",
)
.run();
}
#[test]
fn module_recipe_list_alignment_ignores_private_recipes() {
Test::new()
.write(
"foo.just",
"
# foos
foo:
@echo FOO
[private]
barbarbar:
@echo BAR
@_bazbazbaz:
@echo BAZ
",
)
.justfile("mod foo")
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.stdout(
"
Available recipes:
foo:
foo # foos
",
)
.run();
}
#[test]
fn nested_modules_are_properly_indented() {
Test::new()
.write("foo.just", "mod bar")
.write("bar.just", "baz:\n @echo FOO")
.justfile(
"
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--list", "--list-submodules"])
.stdout(
"
Available recipes:
foo:
bar:
baz
",
)
.run();
}

View File

@ -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,
} }
@ -1589,84 +1589,6 @@ echo:
stderr: "echo 1\n", stderr: "echo 1\n",
} }
test! {
name: dotenv_variable_in_recipe,
justfile: "
#
set dotenv-load
echo:
echo $DOTENV_KEY
",
stdout: "dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}
test! {
name: dotenv_variable_in_backtick,
justfile: "
#
set dotenv-load
X:=`echo $DOTENV_KEY`
echo:
echo {{X}}
",
stdout: "dotenv-value\n",
stderr: "echo dotenv-value\n",
}
test! {
name: dotenv_variable_in_function_in_recipe,
justfile: "
#
set dotenv-load
echo:
echo {{env_var_or_default('DOTENV_KEY', 'foo')}}
echo {{env_var('DOTENV_KEY')}}
",
stdout: "dotenv-value\ndotenv-value\n",
stderr: "echo dotenv-value\necho dotenv-value\n",
}
test! {
name: dotenv_variable_in_function_in_backtick,
justfile: "
#
set dotenv-load
X:=env_var_or_default('DOTENV_KEY', 'foo')
Y:=env_var('DOTENV_KEY')
echo:
echo {{X}}
echo {{Y}}
",
stdout: "dotenv-value\ndotenv-value\n",
stderr: "echo dotenv-value\necho dotenv-value\n",
}
test! {
name: no_dotenv,
justfile: "
#
X:=env_var_or_default('DOTENV_KEY', 'DEFAULT')
echo:
echo {{X}}
",
args: ("--no-dotenv"),
stdout: "DEFAULT\n",
stderr: "echo DEFAULT\n",
}
test! {
name: dotenv_env_var_override,
justfile: "
#
echo:
echo $DOTENV_KEY
",
env: {"DOTENV_KEY": "not-the-dotenv-value",},
stdout: "not-the-dotenv-value\n",
stderr: "echo $DOTENV_KEY\n",
}
test! { test! {
name: invalid_escape_sequence_message, name: invalid_escape_sequence_message,
justfile: r#" justfile: r#"

View File

@ -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();
} }
@ -515,7 +515,6 @@ fn missing_optional_modules_do_not_conflict() {
#[test] #[test]
fn root_dotenv_is_available_to_submodules() { fn root_dotenv_is_available_to_submodules() {
Test::new() Test::new()
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
.justfile( .justfile(
" "
set dotenv-load set dotenv-load
@ -523,10 +522,10 @@ fn root_dotenv_is_available_to_submodules() {
mod foo mod foo
", ",
) )
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false) .test_round_trip(false)
.arg("--unstable") .args(["--unstable", "foo", "foo"])
.arg("foo")
.arg("foo")
.stdout("dotenv-value\n") .stdout("dotenv-value\n")
.run(); .run();
} }
@ -534,10 +533,6 @@ fn root_dotenv_is_available_to_submodules() {
#[test] #[test]
fn dotenv_settings_in_submodule_are_ignored() { fn dotenv_settings_in_submodule_are_ignored() {
Test::new() Test::new()
.write(
"foo.just",
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
)
.justfile( .justfile(
" "
set dotenv-load set dotenv-load
@ -545,10 +540,13 @@ fn dotenv_settings_in_submodule_are_ignored() {
mod foo mod foo
", ",
) )
.write(
"foo.just",
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
)
.write(".env", "DOTENV_KEY=dotenv-value")
.test_round_trip(false) .test_round_trip(false)
.arg("--unstable") .args(["--unstable", "foo", "foo"])
.arg("foo")
.arg("foo")
.stdout("dotenv-value\n") .stdout("dotenv-value\n")
.run(); .run();
} }
@ -692,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();
}

View File

@ -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#"

View File

@ -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();
}

View File

@ -82,6 +82,7 @@ fn shell_expanded_strings_can_be_used_in_settings() {
echo $DOTENV_KEY echo $DOTENV_KEY
", ",
) )
.write(".env", "DOTENV_KEY=dotenv-value")
.env("JUST_TEST_VARIABLE", ".env") .env("JUST_TEST_VARIABLE", ".env")
.stdout("dotenv-value\n") .stdout("dotenv-value\n")
.run(); .run();

View File

@ -124,3 +124,18 @@ fn show_invalid_path() {
.status(1) .status(1)
.run(); .run();
} }
#[test]
fn show_space_separated_path() {
Test::new()
.write("foo.just", "bar:\n @echo MODULE")
.justfile(
"
mod foo
",
)
.test_round_trip(false)
.args(["--unstable", "--show", "foo bar"])
.stdout("bar:\n @echo MODULE\n")
.run();
}

View File

@ -5,8 +5,8 @@ pub(crate) fn tempdir() -> TempDir {
builder.prefix("just-test-tempdir"); builder.prefix("just-test-tempdir");
if let Some(cache_dir) = dirs::cache_dir() { if let Some(runtime_dir) = dirs::runtime_dir() {
let path = cache_dir.join("just"); let path = runtime_dir.join("just");
fs::create_dir_all(&path).unwrap(); fs::create_dir_all(&path).unwrap();
builder.tempdir_in(path) builder.tempdir_in(path)
} else { } else {

View File

@ -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
@ -201,9 +206,8 @@ impl Test {
} else { } else {
self.stdout.clone() self.stdout.clone()
}; };
let stderr = unindent(&self.stderr);
fs::write(self.tempdir.path().join(".env"), "DOTENV_KEY=dotenv-value").unwrap(); let stderr = unindent(&self.stderr);
let mut command = Command::new(executable_path("just")); let mut command = Command::new(executable_path("just"));
@ -258,7 +262,7 @@ impl Test {
} }
} }
if !compare("status", output.status.code().unwrap(), self.status) if !compare("status", output.status.code(), Some(self.status))
| (self.stdout_regex.is_none() && !compare("stdout", output_stdout, &stdout)) | (self.stdout_regex.is_none() && !compare("stdout", output_stdout, &stdout))
| (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr)) | (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr))
{ {

126
tests/unexport.rs Normal file
View File

@ -0,0 +1,126 @@
use super::*;
#[test]
fn unexport_environment_variable_linewise() {
Test::new()
.justfile(
"
unexport JUST_TEST_VARIABLE
@recipe:
echo ${JUST_TEST_VARIABLE:-unset}
",
)
.env("JUST_TEST_VARIABLE", "foo")
.stdout("unset\n")
.run();
}
#[test]
fn unexport_environment_variable_shebang() {
Test::new()
.justfile(
"
unexport JUST_TEST_VARIABLE
recipe:
#!/usr/bin/env bash
echo ${JUST_TEST_VARIABLE:-unset}
",
)
.env("JUST_TEST_VARIABLE", "foo")
.stdout("unset\n")
.run();
}
#[test]
fn duplicate_unexport_fails() {
Test::new()
.justfile(
"
unexport JUST_TEST_VARIABLE
recipe:
echo \"variable: $JUST_TEST_VARIABLE\"
unexport JUST_TEST_VARIABLE
",
)
.env("JUST_TEST_VARIABLE", "foo")
.stderr(
"
error: Variable `JUST_TEST_VARIABLE` is unexported multiple times
justfile:6:10
6 unexport JUST_TEST_VARIABLE
^^^^^^^^^^^^^^^^^^
",
)
.status(1)
.run();
}
#[test]
fn export_unexport_conflict() {
Test::new()
.justfile(
"
unexport JUST_TEST_VARIABLE
recipe:
echo variable: $JUST_TEST_VARIABLE
export JUST_TEST_VARIABLE := 'foo'
",
)
.stderr(
"
error: Variable JUST_TEST_VARIABLE is both exported and unexported
justfile:6:8
6 export JUST_TEST_VARIABLE := 'foo'
^^^^^^^^^^^^^^^^^^
",
)
.status(1)
.run();
}
#[test]
fn unexport_doesnt_override_local_recipe_export() {
Test::new()
.justfile(
"
unexport JUST_TEST_VARIABLE
recipe $JUST_TEST_VARIABLE:
@echo \"variable: $JUST_TEST_VARIABLE\"
",
)
.args(["recipe", "value"])
.stdout("variable: value\n")
.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();
}

15
tests/windows.rs Normal file
View File

@ -0,0 +1,15 @@
use super::*;
#[test]
fn bare_bash_in_shebang() {
Test::new()
.justfile(
"
default:
#!bash
echo FOO
",
)
.stdout("FOO\n")
.run();
}