Compare commits
74 Commits
master
...
groups-in-
Author | SHA1 | Date | |
---|---|---|---|
|
ba89f94e7e | ||
|
a3693f3e8f | ||
|
ea26e451fa | ||
|
d5ebc9515e | ||
|
023b126eb2 | ||
|
687007a723 | ||
|
6747c79082 | ||
|
458805e283 | ||
|
d6669e0b97 | ||
|
564814208f | ||
|
f1020b4e6a | ||
|
5e9f46e855 | ||
|
241e7b46a5 | ||
|
0c9b159aa4 | ||
|
d2f66815da | ||
|
50e8874e0e | ||
|
8186992340 | ||
|
1242bd64aa | ||
|
42691e1043 | ||
|
3e0909701c | ||
|
5695384271 | ||
|
39b2783c4b | ||
|
208187fbb6 | ||
|
7683c81c08 | ||
|
e0c031272d | ||
|
ef6a813dd1 | ||
|
e07da79d40 | ||
|
97c32e60ae | ||
|
929fd695d5 | ||
|
23f1c1ca9f | ||
|
570d3058cf | ||
|
c900b6f478 | ||
|
af86a471e2 | ||
|
e4564f45a3 | ||
|
aa43a664ee | ||
|
553adc1004 | ||
|
e572b93d84 | ||
|
fcac7ee768 | ||
|
71b72c4a53 | ||
|
0e8f660d6d | ||
|
1c3c1dd3c0 | ||
|
197e1002d0 | ||
|
4a59769faa | ||
|
bf6ec6bf16 | ||
|
1547af08b5 | ||
|
b05a75d168 | ||
|
5f91b37c82 | ||
|
dd9792571b | ||
|
e6c37aacd1 | ||
|
18ec9796b9 | ||
|
e1b17fe9cf | ||
|
4b5ba8f6f5 | ||
|
1ce7a05bef | ||
|
637023e86f | ||
|
4f16428bcb | ||
|
8778972014 | ||
|
5ac98c020d | ||
|
3236154d8d | ||
|
0de971942a | ||
|
1ca53e8b22 | ||
|
4fbd03735a | ||
|
38873dcb74 | ||
|
3ddd1b1683 | ||
|
0eb2a0678c | ||
|
3c40c0c6eb | ||
|
70d1e1b3af | ||
|
af249dbce1 | ||
|
7c30fb4944 | ||
|
6fe068c432 | ||
|
b5ad133b93 | ||
|
8d3d88fc13 | ||
|
f2201d8684 | ||
|
d38c1add13 | ||
|
d2b10e04d3 |
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -30,12 +30,6 @@ jobs:
|
||||
- name: Format
|
||||
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
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
15
.github/workflows/release.yaml
vendored
15
.github/workflows/release.yaml
vendored
@ -73,6 +73,17 @@ jobs:
|
||||
id: ref-type
|
||||
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
|
||||
id: package
|
||||
env:
|
||||
@ -84,7 +95,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- 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/') }}
|
||||
with:
|
||||
draft: false
|
||||
@ -94,7 +105,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish Changelog
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
uses: softprops/action-gh-release@v2.0.6
|
||||
if: >-
|
||||
${{
|
||||
startsWith(github.ref, 'refs/tags/')
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,6 +10,7 @@
|
||||
/fuzz/artifacts
|
||||
/fuzz/corpus
|
||||
/fuzz/target
|
||||
/man
|
||||
/target
|
||||
/test-utilities/Cargo.lock
|
||||
/test-utilities/target
|
||||
|
1022
CHANGELOG.md
1022
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
257
Cargo.lock
generated
257
Cargo.lock
generated
@ -67,9 +67,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@ -121,15 +121,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52"
|
||||
checksum = "3d08263faac5cde2a4d52b513dadb80846023aade56fcd8fc99ba73ba8050e92"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
@ -137,7 +137,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"memmap2",
|
||||
"rayon",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -174,9 +174,9 @@ checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.98"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
||||
checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -201,7 +201,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -221,18 +221,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.4"
|
||||
version = "4.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.2"
|
||||
version = "4.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
|
||||
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -243,26 +244,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.2"
|
||||
version = "4.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
|
||||
checksum = "5b4be9c4c4b1f30b78d8a750e0822b6a6102d97e62061c583a6c1dea2dfb33ae"
|
||||
dependencies = [
|
||||
"clap 4.5.4",
|
||||
"clap 4.5.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "clap_mangen"
|
||||
version = "0.2.20"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e"
|
||||
checksum = "f50dde5bc0c853d6248de457e5eb6e5a674a54b93810a34ded88d882ca1fe2de"
|
||||
dependencies = [
|
||||
"clap 4.5.4",
|
||||
"clap 4.5.9",
|
||||
"roff",
|
||||
]
|
||||
|
||||
@ -293,15 +306,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cradle"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7096122c1023d53de7298f322590170540ad3eba46bbc2750b495f098c27c09a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
@ -403,15 +407,15 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||
|
||||
[[package]]
|
||||
name = "edit-distance"
|
||||
version = "2.1.0"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
|
||||
checksum = "853fc7035888bd1c9320f3a05bfe7f344f49b8766a4bb4209b1ac5f0503d9577"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
|
||||
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
@ -505,12 +509,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@ -593,16 +591,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "just"
|
||||
version = "1.27.0"
|
||||
version = "1.31.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"blake3",
|
||||
"camino",
|
||||
"chrono",
|
||||
"clap 4.5.4",
|
||||
"clap 4.5.9",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
"cradle",
|
||||
"ctrlc",
|
||||
"derivative",
|
||||
"dirs",
|
||||
@ -638,9 +635,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lexiclean"
|
||||
@ -660,7 +657,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -672,15 +669,15 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
@ -697,7 +694,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@ -782,9 +779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.83"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -795,7 +792,7 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.6.0",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
@ -849,16 +846,6 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
@ -891,13 +878,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-automata 0.4.7",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
@ -909,9 +896,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.6"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -920,9 +907,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "roff"
|
||||
@ -936,7 +923,7 @@ version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@ -963,29 +950,29 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.202"
|
||||
version = "1.0.204"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.202"
|
||||
version = "1.0.204"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.117"
|
||||
version = "1.0.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@ -1024,23 +1011,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "snafu"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418b8136fec49956eba89be7da2847ec1909df92a9ae4178b5ff0ff092c8d95e"
|
||||
checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d"
|
||||
dependencies = [
|
||||
"snafu-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snafu-derive"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a4812a669da00d17d8266a0439eddcacbc88b17f732f927e52eeb9d196f7fb5"
|
||||
checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1081,24 +1068,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.2"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.2"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1114,9 +1101,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
version = "2.0.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1125,9 +1112,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target"
|
||||
version = "2.0.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4df6b0340c7cc29eb3b955cc588d145ed60651bf1ab939083295d19ec8cc282"
|
||||
checksum = "1e8f05f774b2db35bdad5a8237a90be1102669f8ea013fea9777b366d34ab145"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
@ -1171,22 +1158,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
version = "1.0.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1224,9 +1211,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
|
||||
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
|
||||
|
||||
[[package]]
|
||||
name = "update-contributors"
|
||||
@ -1237,15 +1224,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
@ -1289,7 +1276,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@ -1311,7 +1298,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.71",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@ -1362,7 +1349,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1380,7 +1367,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1400,18 +1387,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.5",
|
||||
"windows_aarch64_msvc 0.52.5",
|
||||
"windows_i686_gnu 0.52.5",
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.5",
|
||||
"windows_x86_64_gnu 0.52.5",
|
||||
"windows_x86_64_gnullvm 0.52.5",
|
||||
"windows_x86_64_msvc 0.52.5",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1422,9 +1409,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
@ -1434,9 +1421,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
@ -1446,15 +1433,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
@ -1464,9 +1451,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
@ -1476,9 +1463,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
@ -1488,9 +1475,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
@ -1500,9 +1487,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "just"
|
||||
version = "1.27.0"
|
||||
version = "1.31.0"
|
||||
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
||||
autotests = false
|
||||
categories = ["command-line-utilities", "development-tools"]
|
||||
@ -22,7 +22,7 @@ ansi_term = "0.12.0"
|
||||
blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
|
||||
camino = "1.0.4"
|
||||
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_mangen = "0.2.20"
|
||||
ctrlc = { version = "3.1.1", features = ["termination"] }
|
||||
@ -54,7 +54,6 @@ unicode-width = "0.1.0"
|
||||
uuid = { version = "1.0.0", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
cradle = "0.2.0"
|
||||
executable-path = "1.0.0"
|
||||
pretty_assertions = "1.0.0"
|
||||
temptree = "0.2.0"
|
||||
|
@ -98,10 +98,10 @@ value : NAME '(' sequence? ')'
|
||||
| string
|
||||
| '(' expression ')'
|
||||
|
||||
string : STRING
|
||||
| INDENTED_STRING
|
||||
| RAW_STRING
|
||||
| INDENTED_RAW_STRING
|
||||
string : 'x'? STRING
|
||||
| 'x'? INDENTED_STRING
|
||||
| 'x'? RAW_STRING
|
||||
| 'x'? INDENTED_RAW_STRING
|
||||
|
||||
sequence : expression ',' sequence
|
||||
| expression ','?
|
||||
|
291
README.md
291
README.md
@ -379,11 +379,11 @@ There will never be a `just` 2.0. Any desirable backwards-incompatible changes
|
||||
will be opt-in on a per-`justfile` basis, so users may migrate at their
|
||||
leisure.
|
||||
|
||||
Features that aren't yet ready for stabilization are gated behind the
|
||||
`--unstable` flag. Features enabled by `--unstable` may change in backwards
|
||||
incompatible ways at any time. Unstable features can also be enabled by setting
|
||||
the environment variable `JUST_UNSTABLE` to any value other than `false`, `0`,
|
||||
or the empty string.
|
||||
Features that aren't yet ready for stabilization are marked as unstable and may
|
||||
be changed or removed at any time. Using unstable features produces an error by
|
||||
default, which can be suppressed with by passing the `--unstable` flag,
|
||||
`set unstable`, or setting the environment variable `JUST_UNSTABLE`, to any
|
||||
value other than `false`, `0`, or the empty string.
|
||||
|
||||
Editor Support
|
||||
--------------
|
||||
@ -603,8 +603,9 @@ testing… all tests passed!
|
||||
Examples
|
||||
--------
|
||||
|
||||
A variety of example `justfile`s can be found in the
|
||||
[examples directory](https://github.com/casey/just/tree/master/examples).
|
||||
A variety of `justfile`s can be found in the
|
||||
[examples directory](https://github.com/casey/just/tree/master/examples) and on
|
||||
[GitHub](https://github.com/search?q=path%3A**%2Fjustfile&type=code).
|
||||
|
||||
Features
|
||||
--------
|
||||
@ -656,8 +657,8 @@ Available recipes:
|
||||
lint
|
||||
```
|
||||
|
||||
Recipes in submodules can be listed with `just --list PATH`, where `PATH` is a
|
||||
space- or `::`-separated module path:
|
||||
Recipes in [submodules](#modules1190) can be listed with `just --list PATH`,
|
||||
where `PATH` is a space- or `::`-separated module path:
|
||||
|
||||
```
|
||||
$ cat justfile
|
||||
@ -666,10 +667,10 @@ $ cat foo.just
|
||||
mod bar
|
||||
$ cat bar.just
|
||||
baz:
|
||||
$ just --unstable foo bar
|
||||
$ just foo bar
|
||||
Available recipes:
|
||||
baz
|
||||
$ just --unstable foo::bar
|
||||
$ just foo::bar
|
||||
Available recipes:
|
||||
baz
|
||||
```
|
||||
@ -812,12 +813,14 @@ foo:
|
||||
| `dotenv-filename` | string | - | Load a `.env` file with a custom name, 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-required` | boolean | `false` | Error if a `.env` file isn't found. |
|
||||
| `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. |
|
||||
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
|
||||
| `positional-arguments` | boolean | `false` | Pass positional arguments. |
|
||||
| `shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
|
||||
| `tempdir` | string | - | Create temporary directories in `tempdir` instead of the system default temporary directory. |
|
||||
| `unstable`<sup>1.31.0</sup> | boolean | `false` | Enable unstable features. |
|
||||
| `windows-powershell` | boolean | `false` | Use PowerShell on Windows as default shell. (Deprecated. Use `windows-shell` instead. |
|
||||
| `windows-shell` | `[COMMAND, ARGS…]` | - | Set the command used to invoke recipes and evaluate backticks. |
|
||||
|
||||
@ -877,17 +880,25 @@ bar
|
||||
|
||||
#### Dotenv Settings
|
||||
|
||||
If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load
|
||||
environment variables from a file.
|
||||
If any of `dotenv-load`, `dotenv-filename`, `dotenv-path`, or `dotenv-required`
|
||||
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
|
||||
an error if a dotenv file is not found at `dotenv-path`, but not an error if a
|
||||
dotenv file is not found with `dotenv-filename`.
|
||||
If `dotenv-path` is set, `just` will look for a file at the given path, which
|
||||
may be absolute, or relative to the working directory.
|
||||
|
||||
Otherwise, `just` looks for a file named `.env` by default, unless
|
||||
`dotenv-filename` set, in which case the value of `dotenv-filename` is used.
|
||||
This file can be located in the same directory as your `justfile` or in a
|
||||
parent directory.
|
||||
If `dotenv-filename` is set `just` will look for a file at the given path,
|
||||
relative to the working directory and each of its ancestors.
|
||||
|
||||
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
|
||||
must be accessed using `$VARIABLE_NAME` in recipes and backticks.
|
||||
@ -987,10 +998,24 @@ $ just test foo "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
|
||||
|
||||
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
|
||||
# use python3 to execute recipe lines and backticks
|
||||
@ -1291,6 +1316,7 @@ foobar := x'~/$FOO/${BAR}'
|
||||
|------|-------------|
|
||||
| `$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 `~USER` | path to `USER`'s home directory |
|
||||
|
||||
@ -1324,6 +1350,11 @@ Done!
|
||||
`just` provides a few built-in functions that might be useful when writing
|
||||
recipes.
|
||||
|
||||
All functions ending in `_directory` can be abbreviated to `_dir`. So
|
||||
`home_directory()` can also be written as `home_dir()`. In addition,
|
||||
`invocation_directory_native()` can be abbreviated to
|
||||
`invocation_dir_native()`.
|
||||
|
||||
#### System Information
|
||||
|
||||
- `arch()` — Instruction set architecture. Possible values are: `"aarch64"`,
|
||||
@ -1417,6 +1448,12 @@ $ just
|
||||
- `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)`.
|
||||
|
||||
#### 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()` - Retrieves the absolute path to the current
|
||||
@ -1458,7 +1495,7 @@ For example, to run a command relative to the location of the current
|
||||
|
||||
```just
|
||||
script:
|
||||
./{{justfile_directory()}}/scripts/some_script
|
||||
{{justfile_directory()}}/scripts/some_script
|
||||
```
|
||||
|
||||
#### Source and Source Directory
|
||||
@ -1612,6 +1649,16 @@ which will halt execution.
|
||||
characters. For example, `choose('64', HEX)` will generate a random
|
||||
64-character lowercase hex string.
|
||||
|
||||
#### Datetime
|
||||
|
||||
- `datetime(format)`<sup>1.30.0</sup> - Return local time with `format`.
|
||||
- `datetime_utc(format)`<sup>1.30.0</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
|
||||
|
||||
- `semver_matches(version, requirement)`<sup>1.16.0</sup> - Check whether a
|
||||
@ -1665,12 +1712,14 @@ Recipes may be annotated with attributes that change their behavior.
|
||||
| `[confirm]`<sup>1.17.0</sup> | Require confirmation prior to executing recipe. |
|
||||
| `[confirm('PROMPT')]`<sup>1.23.0</sup> | Require confirmation prior to executing recipe with a custom prompt. |
|
||||
| `[doc('DOC')]`<sup>1.27.0</sup> | Set recipe's [documentation comment](#documentation-comments) to `DOC`. |
|
||||
| `[extension('EXT')]`<sup>master</sup> | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
|
||||
| `[group('NAME')]`<sup>1.27.0</sup> | Put recipe in [recipe group](#recipe-groups) `NAME`. |
|
||||
| `[linux]`<sup>1.8.0</sup> | Enable recipe on Linux. |
|
||||
| `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
|
||||
| `[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-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). |
|
||||
| `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
|
||||
| `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |
|
||||
@ -1776,7 +1825,7 @@ js-lint:
|
||||
[group('rust recipes')]
|
||||
[group('lint')]
|
||||
rust-lint:
|
||||
echo 'Runninng Rust linter…'
|
||||
echo 'Running Rust linter…'
|
||||
|
||||
[group('lint')]
|
||||
cpp-lint:
|
||||
@ -1830,6 +1879,8 @@ Recipe groups:
|
||||
rust recipes
|
||||
```
|
||||
|
||||
Use `just --groups --unsorted` to print groups in their justfile order.
|
||||
|
||||
### Command Evaluation Using Backticks
|
||||
|
||||
Backticks can be used to store the result of commands:
|
||||
@ -2044,6 +2095,23 @@ a $A $B=`echo $A`:
|
||||
When [export](#export) is set, all `just` variables are exported as environment
|
||||
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
|
||||
|
||||
Environment variables from the environment are passed automatically to the
|
||||
@ -3085,10 +3153,12 @@ import? 'foo/bar.just'
|
||||
|
||||
Missing source files for optional imports do not produce an error.
|
||||
|
||||
### Modules <sup>1.19.0</sup>
|
||||
### Modules<sup>1.19.0</sup>
|
||||
|
||||
A `justfile` can declare modules using `mod` statements. `mod` statements are
|
||||
currently unstable, so you'll need to use the `--unstable` flag, or set the
|
||||
A `justfile` can declare modules using `mod` statements.
|
||||
|
||||
`mod` statements were stabilized in `just`<sup>1.31.0</sup>. In earlier
|
||||
versions, you'll need to use the `--unstable` flag, `set unstable`, or set the
|
||||
`JUST_UNSTABLE` environment variable to use them.
|
||||
|
||||
If you have the following `justfile`:
|
||||
@ -3114,14 +3184,14 @@ uses its own settings.
|
||||
Recipes in submodules can be invoked as subcommands:
|
||||
|
||||
```sh
|
||||
$ just --unstable bar b
|
||||
$ just bar b
|
||||
B
|
||||
```
|
||||
|
||||
Or with path syntax:
|
||||
|
||||
```sh
|
||||
$ just --unstable bar::b
|
||||
$ just bar::b
|
||||
B
|
||||
```
|
||||
|
||||
@ -3137,7 +3207,10 @@ mod foo 'PATH'
|
||||
|
||||
Which loads the module's source file from `PATH`, instead of from the usual
|
||||
locations. A leading `~/` in `PATH` is replaced with the current user's home
|
||||
directory.
|
||||
directory. `PATH` may point to the module source file itself, or to a directory
|
||||
containing the module source file with the name `mod.just`, `justfile`, or
|
||||
`.justfile`. In the latter two cases, the module file may have any
|
||||
capitalization.
|
||||
|
||||
Environment files are only loaded for the root justfile, and loaded environment
|
||||
variables are available in submodules. Settings in submodules that affect
|
||||
@ -3167,6 +3240,20 @@ mod? foo 'bar.just'
|
||||
mod? foo 'baz.just'
|
||||
```
|
||||
|
||||
Modules may be given doc comments which appear in `--list`
|
||||
output<sup>1.30.0</sup>:
|
||||
|
||||
```mf
|
||||
# foo is a great module!
|
||||
mod foo
|
||||
```
|
||||
|
||||
```sh
|
||||
$ just --list
|
||||
Available recipes:
|
||||
foo ... # foo is a great module!
|
||||
```
|
||||
|
||||
See the
|
||||
[module stabilization tracking issue](https://github.com/casey/just/issues/929)
|
||||
for more information.
|
||||
@ -3330,9 +3417,9 @@ foo argument:
|
||||
touch "$1"
|
||||
```
|
||||
|
||||
This defeats `just`'s ability to catch typos, for example if you type `$2`, but
|
||||
works for all possible values of `argument`, including those with double
|
||||
quotes.
|
||||
This defeats `just`'s ability to catch typos, for example if you type `$2`
|
||||
instead of `$1`, but works for all possible values of `argument`, including
|
||||
those with double quotes.
|
||||
|
||||
#### Exported Arguments
|
||||
|
||||
@ -3463,18 +3550,18 @@ complete -F _just -o bashdefault -o default j
|
||||
|
||||
### Shell Completion Scripts
|
||||
|
||||
Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are
|
||||
available in the
|
||||
[completions](https://github.com/casey/just/tree/master/completions) directory.
|
||||
Please refer to your shell's documentation for how to install them.
|
||||
Shell completion scripts for Bash, Elvish, Fish, Nushell, PowerShell, and Zsh
|
||||
are available [release archives](https://github.com/casey/just/releases).
|
||||
|
||||
The `just` binary can also generate the same completion scripts at runtime,
|
||||
using the `--completions` command:
|
||||
The `just` binary can also generate the same completion scripts at runtime
|
||||
using `just --completions SHELL`:
|
||||
|
||||
```sh
|
||||
$ 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
|
||||
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
|
||||
@ -3597,6 +3684,33 @@ Node.js `package.json` files:
|
||||
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
|
||||
|
||||
There is no shortage of command runners! Some more or less similar alternatives
|
||||
@ -3635,6 +3749,106 @@ permissive
|
||||
domain dedication and fallback license, so your changes must also be released
|
||||
under this license.
|
||||
|
||||
### Getting Started
|
||||
|
||||
`just` is written in Rust. Use
|
||||
[rustup](https://www.rust-lang.org/tools/install) to install a Rust toolchain.
|
||||
|
||||
`just` is extensively tested. All new features must be covered by unit or
|
||||
integration tests. Unit tests are under
|
||||
[src](https://github.com/casey/just/blob/master/src), live alongside the code
|
||||
being tested, and test code in isolation. Integration tests are in the [tests
|
||||
directory](https://github.com/casey/just/blob/master/tests) and test the `just`
|
||||
binary from the outside by invoking `just` on a given `justfile` and set of
|
||||
command-line arguments, and checking the output.
|
||||
|
||||
You should write whichever type of tests are easiest to write for your feature
|
||||
while still providing good test coverage.
|
||||
|
||||
Unit tests are useful for testing new Rust functions that are used internally
|
||||
and as an aid for development. A good example are the unit tests which cover
|
||||
the
|
||||
[`unindent()` function](https://github.com/casey/just/blob/master/src/unindent.rs),
|
||||
used to unindent triple-quoted strings and backticks. `unindent()` has a bunch
|
||||
of tricky edge cases which are easy to exercise with unit tests that call
|
||||
`unindent()` directly.
|
||||
|
||||
Integration tests are useful for making sure that the final behavior of the
|
||||
`just` binary is correct. `unindent()` is also covered by integration tests
|
||||
which make sure that evaluating a triple-quoted string produces the correct
|
||||
unindented value. However, there are not integration tests for all possible
|
||||
cases. These are covered by faster, more concise unit tests that call
|
||||
`unindent()` directly.
|
||||
|
||||
Existing integration tests are in two forms, those that use the `test!` macro
|
||||
and those that use the `Test` struct directly. The `test!` macro, while often
|
||||
concise, is less flexible and harder to understand, so new tests should use the
|
||||
`Test` struct. The `Test` struct is a builder which allows for easily invoking
|
||||
`just` with a given `justfile`, arguments, and environment variables, and
|
||||
checking the program's stdout, stderr, and exit code .
|
||||
|
||||
### Contribution Workflow
|
||||
|
||||
1. Make sure the feature is wanted. There should be an open issue about the
|
||||
feature with a comment from [@casey](https://github.com/casey) saying that
|
||||
it's a good idea or seems reasonable. If there isn't, open a new issue and
|
||||
ask for feedback.
|
||||
|
||||
There are lots of good features which can't be merged, either because they
|
||||
aren't backwards compatible, have an implementation which would
|
||||
overcomplicate the codebase, or go against `just`'s design philosophy.
|
||||
|
||||
2. Settle on the design of the feature. If the feature has multiple possible
|
||||
implementations or syntaxes, make sure to nail down the details in the
|
||||
issue.
|
||||
|
||||
3. Clone `just` and start hacking. The best workflow is to have the code you're
|
||||
working on in an editor alongside a job that re-runs tests whenever a file
|
||||
changes. You can run such a job by installing
|
||||
[cargo-watch](https://github.com/watchexec/cargo-watch) with `cargo install
|
||||
cargo-watch` and running `just watch test`.
|
||||
|
||||
4. Add a failing test for your feature. Most of the time this will be an
|
||||
integration test which exercises the feature end-to-end. Look for an
|
||||
appropriate file to put the test in in
|
||||
[tests](https://github.com/casey/just/blob/master/tests), or add a new file
|
||||
in [tests](https://github.com/casey/just/blob/master/tests) and add a `mod`
|
||||
statement importing that file in
|
||||
[tests/lib.rs](https://github.com/casey/just/blob/master/tests/lib.rs).
|
||||
|
||||
5. Implement the feature.
|
||||
|
||||
6. Run `just ci` to make sure that all tests, lints, and checks pass.
|
||||
|
||||
7. Open a PR with the new code that is editable by maintainers. PRs often
|
||||
require rebasing and minor tweaks. If the PR is not editable by maintainers,
|
||||
each rebase and tweak will require a round trip of code review. Your PR may
|
||||
be summarily closed if it is not editable by maintainers.
|
||||
|
||||
8. Incorporate feedback.
|
||||
|
||||
9. Enjoy the sweet feeling of your PR getting merged!
|
||||
|
||||
Feel free to open a draft PR at any time for discussion and feedback.
|
||||
|
||||
### Hints
|
||||
|
||||
Here are some hints to get you started with specific kinds of new features,
|
||||
which you can use in addition to the contribution workflow above.
|
||||
|
||||
#### Adding a New Attribute
|
||||
|
||||
1. Write a new integration test in
|
||||
[tests/attributes.rs](https://github.com/casey/just/blob/master/tests/attributes.rs).
|
||||
|
||||
2. Add a new variant to the
|
||||
[`Attribute`](https://github.com/casey/just/blob/master/src/attribute.rs)
|
||||
enum.
|
||||
|
||||
3. Implement the functionality of the new attribute.
|
||||
|
||||
4. Run `just ci` to make sure that all tests pass.
|
||||
|
||||
### Janus
|
||||
|
||||
[Janus](https://github.com/casey/janus) is a tool for checking whether a change
|
||||
@ -3664,7 +3878,6 @@ Release x.y.z
|
||||
- Update changelog
|
||||
- Update changelog contributor credits
|
||||
- Update dependencies
|
||||
- Update man page
|
||||
- Update version references in readme
|
||||
```
|
||||
|
||||
|
124
README.中文.md
124
README.中文.md
@ -50,7 +50,7 @@ Yay, all your tests passed!
|
||||
|
||||
- 错误会尽可能被静态地解决。未知的配方和循环依赖关系会在运行之前被报告。
|
||||
|
||||
- `just` 可以 [加载`.env`文件](#env-集成),简化环境变量注入。
|
||||
- `just` 可以 [加载`.env`文件](#环境变量加载),简化环境变量注入。
|
||||
|
||||
- 配方可以在 [命令行中列出](#列出可用的配方)。
|
||||
|
||||
@ -641,18 +641,22 @@ foo:
|
||||
|
||||
#### 设置一览表
|
||||
|
||||
| 名称 | 值 | 默认 | 描述 |
|
||||
| ------------------------- | ------------------ | --------|------------------------------------------------------------------------------- |
|
||||
| `allow-duplicate-recipes` | boolean | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方 |
|
||||
| `dotenv-load` | boolean | False | 如果有`.env` 环境变量文件的话,则将其加载 |
|
||||
| `export` | boolean | False | 将所有变量导出为环境变量 |
|
||||
| `fallback` | boolean | False | 如果命令行中的第一个配方没有找到,则在父目录中搜索 `justfile` |
|
||||
| `ignore-comments` | boolean | False | 忽略以`#`开头的配方行 |
|
||||
| `positional-arguments` | boolean | False | 传递位置参数 |
|
||||
| `shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 |
|
||||
| `tempdir` | string | - | 在 `tempdir` 位置创建临时目录,而不是系统默认的临时目录 |
|
||||
| `windows-powershell` | boolean | False | 在 Windows 上使用 PowerShell 作为默认 Shell(废弃,建议使用 `windows-shell`) |
|
||||
| `windows-shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 |
|
||||
| 名称 | 值 | 默认 | 描述 |
|
||||
| --------------------------- | ------------------ | ----- | --------------------------------------------------------------------------------------- |
|
||||
| `allow-duplicate-recipes` | boolean | False | 允许在 `justfile` 后面出现的配方覆盖之前的同名配方 |
|
||||
| `allow-duplicate-variables` | boolean | False | 允许在 `justfile` 后面出现的变量覆盖之前的同名变量 |
|
||||
| `dotenv-filename` | string | - | 如果有自定义名称的 `.env` 环境变量文件的话,则将其加载 |
|
||||
| `dotenv-load` | boolean | False | 如果有`.env` 环境变量文件的话,则将其加载 |
|
||||
| `dotenv-path` | string | - | 从自定义路径中加载 `.env` 环境变量文件, 文件不存在将会报错。可以覆盖 `dotenv-filename` |
|
||||
| `dotenv-required` | boolean | False | 如果 `.env` 环境变量文件不存在的话,需要报错 |
|
||||
| `export` | boolean | False | 将所有变量导出为环境变量 |
|
||||
| `fallback` | boolean | False | 如果命令行中的第一个配方没有找到,则在父目录中搜索 `justfile` |
|
||||
| `ignore-comments` | boolean | False | 忽略以`#`开头的配方行 |
|
||||
| `positional-arguments` | boolean | False | 传递位置参数 |
|
||||
| `shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 |
|
||||
| `tempdir` | string | - | 在 `tempdir` 位置创建临时目录,而不是系统默认的临时目录 |
|
||||
| `windows-powershell` | boolean | False | 在 Windows 上使用 PowerShell 作为默认 Shell(废弃,建议使用 `windows-shell`) |
|
||||
| `windows-shell` | `[COMMAND, ARGS…]` | - | 设置用于调用配方和评估反引号内包裹内容的命令 |
|
||||
|
||||
Bool 类型设置可以写成:
|
||||
|
||||
@ -685,9 +689,69 @@ $ just foo
|
||||
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
|
||||
```
|
||||
|
||||
### `.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` 文件加载环境变量
|
||||
|
||||
如果 [dotenv-load](#环境变量加载) 被设置,`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#env-集成) 以获得更多信息。
|
||||
如果 [dotenv-load](#环境变量加载) 被设置,`just` 将从 `.env` 文件中加载环境变量。该文件中的变量将作为环境变量提供给配方。参见 [环境变量集成](#环境变量加载) 以获得更多信息。
|
||||
|
||||
#### 从环境变量中设置 `just` 变量
|
||||
|
||||
|
@ -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
|
@ -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
|
@ -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]
|
||||
}
|
@ -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'
|
@ -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)
|
||||
]
|
@ -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
|
||||
}
|
@ -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
|
@ -30,13 +30,9 @@ fn main() {
|
||||
.replace_all(
|
||||
&fs::read_to_string("CHANGELOG.md").unwrap(),
|
||||
|captures: &Captures| {
|
||||
let pr = captures[1].parse().unwrap();
|
||||
match author(pr).as_str() {
|
||||
"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}))")
|
||||
}
|
||||
}
|
||||
let pr = captures[1].parse().unwrap();
|
||||
let contributor = author(pr);
|
||||
format!("([#{pr}](https://github.com/casey/just/pull/{pr}) by [{contributor}](https://github.com/{contributor}))")
|
||||
},
|
||||
),
|
||||
)
|
||||
|
61
justfile
61
justfile
@ -13,16 +13,24 @@ export JUST_LOG := log
|
||||
watch +args='test':
|
||||
cargo watch --clear --exec '{{ args }}'
|
||||
|
||||
[group: "testing"]
|
||||
[doc: "Run just test suite"]
|
||||
test:
|
||||
cargo test
|
||||
|
||||
ci: build-book
|
||||
ci: lint build-book
|
||||
cargo test --all
|
||||
|
||||
# Run lint checks
|
||||
[group: "code-checking"]
|
||||
lint:
|
||||
cargo clippy --all --all-targets -- --deny warnings
|
||||
cargo fmt --all -- --check
|
||||
./bin/forbid
|
||||
cargo fmt --all -- --check
|
||||
cargo update --locked --package just
|
||||
|
||||
[group: "testing"]
|
||||
[doc: "Run fuzz tests"]
|
||||
fuzz:
|
||||
cargo +nightly fuzz run fuzz-compiler
|
||||
|
||||
@ -30,32 +38,46 @@ run:
|
||||
cargo run
|
||||
|
||||
# only run tests matching PATTERN
|
||||
[group: "testing"]
|
||||
filter PATTERN:
|
||||
cargo test {{PATTERN}}
|
||||
|
||||
# Build just
|
||||
[group: "developer-workflow"]
|
||||
build:
|
||||
cargo build
|
||||
|
||||
[group: "code-checking"]
|
||||
fmt:
|
||||
cargo fmt --all
|
||||
|
||||
[group: "code-checking"]
|
||||
shellcheck:
|
||||
shellcheck www/install.sh
|
||||
|
||||
# Generate the just manpage
|
||||
[group: "documentation"]
|
||||
man:
|
||||
mkdir -p man
|
||||
cargo run -- --man > man/just.1
|
||||
|
||||
# View the Just manpage
|
||||
[group: "documentation"]
|
||||
view-man: man
|
||||
man man/just.1
|
||||
|
||||
# add git log messages to changelog
|
||||
[group: "developer-workflow"]
|
||||
update-changelog:
|
||||
echo >> CHANGELOG.md
|
||||
git log --pretty='format:- %s' >> CHANGELOG.md
|
||||
|
||||
# Update the contributors file
|
||||
[group: "developer-workflow"]
|
||||
update-contributors:
|
||||
cargo run --release --package update-contributors
|
||||
|
||||
[group: "code-checking"]
|
||||
check: fmt clippy test forbid
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
@ -63,7 +85,11 @@ check: fmt clippy test forbid
|
||||
VERSION=`sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1`
|
||||
grep "^\[$VERSION\]" CHANGELOG.md
|
||||
|
||||
outdated:
|
||||
cargo outdated -R
|
||||
|
||||
# publish current GitHub master branch
|
||||
[group: "developer-workflow"]
|
||||
publish:
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
@ -78,13 +104,19 @@ publish:
|
||||
cd ../..
|
||||
rm -rf tmp/release
|
||||
|
||||
[group: "documentation"]
|
||||
[doc: "Search for master version note subscripts in the README"]
|
||||
readme-version-notes:
|
||||
grep '<sup>master</sup>' README.md
|
||||
|
||||
[group: "developer-workflow"]
|
||||
[doc: "Push to GitHub"]
|
||||
push: check
|
||||
! git branch | grep '* master'
|
||||
git push github
|
||||
|
||||
[group: "developer-workflow"]
|
||||
[doc: "Create a pull request"]
|
||||
pr: push
|
||||
gh pr create --web
|
||||
|
||||
@ -110,6 +142,7 @@ install-dev-deps:
|
||||
cargo install mdbook mdbook-linkcheck
|
||||
|
||||
# everyone's favorite animate paper clip
|
||||
[group: "code-checking"]
|
||||
clippy:
|
||||
cargo clippy --all --all-targets --all-features
|
||||
|
||||
@ -117,16 +150,19 @@ forbid:
|
||||
./bin/forbid
|
||||
|
||||
# count non-empty lines of code
|
||||
[group: "developer-workflow"]
|
||||
sloc:
|
||||
@cat src/*.rs | sed '/^\s*$/d' | wc -l
|
||||
|
||||
replace FROM TO:
|
||||
sd '{{FROM}}' '{{TO}}' src/*.rs
|
||||
|
||||
[group: "demo-recipes"]
|
||||
test-quine:
|
||||
cargo run -- quine
|
||||
|
||||
# make a quine, compile it, and verify it
|
||||
[group: "demo-recipes"]
|
||||
quine:
|
||||
mkdir -p tmp
|
||||
@echo '{{quine-text}}' > tmp/gen0.c
|
||||
@ -154,22 +190,24 @@ quine-text := '
|
||||
}
|
||||
'
|
||||
|
||||
[group: "documentation"]
|
||||
render-readme:
|
||||
#!/usr/bin/env ruby
|
||||
require 'github/markup'
|
||||
$rendered = GitHub::Markup.render("README.adoc", File.read("README.adoc"))
|
||||
File.write('tmp/README.html', $rendered)
|
||||
|
||||
[group: "documentation"]
|
||||
watch-readme:
|
||||
just render-readme
|
||||
fswatch -ro README.adoc | xargs -n1 -I{} just render-readme
|
||||
|
||||
update-completions:
|
||||
./bin/update-completions
|
||||
|
||||
# Test shell completions
|
||||
[group: "testing"]
|
||||
test-completions:
|
||||
./tests/completions/just.bash
|
||||
|
||||
[group: "documentation"]
|
||||
build-book:
|
||||
cargo run --package generate-book
|
||||
mdbook build book/en
|
||||
@ -187,6 +225,7 @@ convert-integration-test test:
|
||||
-e 's/\.run\(\)/.run();/'
|
||||
|
||||
# run all polyglot recipes
|
||||
[group: "demo-recipes"]
|
||||
polyglot: _python _js _perl _sh _ruby
|
||||
|
||||
_python:
|
||||
@ -216,9 +255,21 @@ _ruby:
|
||||
puts "Hello from ruby!"
|
||||
|
||||
# Print working directory, for demonstration purposes!
|
||||
[group: "demo-recipes"]
|
||||
pwd:
|
||||
echo {{invocation_directory()}}
|
||||
|
||||
[group: "testing"]
|
||||
test-bash-completions:
|
||||
rm -rf tmp
|
||||
mkdir -p tmp/bin
|
||||
cargo build
|
||||
cp target/debug/just tmp/bin
|
||||
./tmp/bin/just --completions bash > tmp/just.bash
|
||||
echo 'mod foo' > tmp/justfile
|
||||
echo 'bar:' > tmp/foo.just
|
||||
cd tmp && PATH="`realpath bin`:$PATH" bash --init-file just.bash
|
||||
|
||||
# Local Variables:
|
||||
# mode: makefile
|
||||
# End:
|
||||
|
195
man/just.1
195
man/just.1
@ -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>
|
@ -9,22 +9,24 @@ pub(crate) struct Analyzer<'src> {
|
||||
|
||||
impl<'src> Analyzer<'src> {
|
||||
pub(crate) fn analyze(
|
||||
loaded: &[PathBuf],
|
||||
paths: &HashMap<PathBuf, PathBuf>,
|
||||
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||
root: &Path,
|
||||
doc: Option<String>,
|
||||
loaded: &[PathBuf],
|
||||
name: Option<Name<'src>>,
|
||||
paths: &HashMap<PathBuf, PathBuf>,
|
||||
root: &Path,
|
||||
) -> CompileResult<'src, Justfile<'src>> {
|
||||
Self::default().justfile(loaded, paths, asts, root, name)
|
||||
Self::default().justfile(asts, doc, loaded, name, paths, root)
|
||||
}
|
||||
|
||||
fn justfile(
|
||||
mut self,
|
||||
loaded: &[PathBuf],
|
||||
paths: &HashMap<PathBuf, PathBuf>,
|
||||
asts: &HashMap<PathBuf, Ast<'src>>,
|
||||
root: &Path,
|
||||
doc: Option<String>,
|
||||
loaded: &[PathBuf],
|
||||
name: Option<Name<'src>>,
|
||||
paths: &HashMap<PathBuf, PathBuf>,
|
||||
root: &Path,
|
||||
) -> CompileResult<'src, Justfile<'src>> {
|
||||
let mut recipes = Vec::new();
|
||||
|
||||
@ -37,6 +39,8 @@ impl<'src> Analyzer<'src> {
|
||||
|
||||
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 define = |name: Name<'src>,
|
||||
@ -82,10 +86,36 @@ impl<'src> Analyzer<'src> {
|
||||
stack.push(asts.get(absolute).unwrap());
|
||||
}
|
||||
}
|
||||
Item::Module { absolute, name, .. } => {
|
||||
Item::Module {
|
||||
absolute,
|
||||
name,
|
||||
doc,
|
||||
attributes,
|
||||
..
|
||||
} => {
|
||||
let mut doc_attr: Option<&str> = None;
|
||||
for attribute in attributes {
|
||||
if let Attribute::Doc(ref doc) = attribute {
|
||||
doc_attr = Some(doc.as_ref().map(|s| s.cooked.as_ref()).unwrap_or_default());
|
||||
} else {
|
||||
return Err(name.token.error(InvalidAttribute {
|
||||
item_kind: "Module",
|
||||
item_name: name.lexeme(),
|
||||
attribute: attribute.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(absolute) = absolute {
|
||||
define(*name, "module", false)?;
|
||||
modules.insert(Self::analyze(loaded, paths, asts, absolute, Some(*name))?);
|
||||
modules.insert(Self::analyze(
|
||||
asts,
|
||||
doc_attr.or(*doc).map(ToOwned::to_owned),
|
||||
loaded,
|
||||
Some(*name),
|
||||
paths,
|
||||
absolute,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
Item::Recipe(recipe) => {
|
||||
@ -98,6 +128,13 @@ impl<'src> Analyzer<'src> {
|
||||
self.analyze_set(set)?;
|
||||
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 +146,23 @@ impl<'src> Analyzer<'src> {
|
||||
let mut recipe_table: Table<'src, UnresolvedRecipe<'src>> = Table::default();
|
||||
|
||||
for assignment in assignments {
|
||||
if !settings.allow_duplicate_variables
|
||||
&& self.assignments.contains_key(assignment.name.lexeme())
|
||||
{
|
||||
return Err(assignment.name.token.error(DuplicateVariable {
|
||||
variable: assignment.name.lexeme(),
|
||||
}));
|
||||
let variable = assignment.name.lexeme();
|
||||
|
||||
if !settings.allow_duplicate_variables && self.assignments.contains_key(variable) {
|
||||
return Err(assignment.name.token.error(DuplicateVariable { variable }));
|
||||
}
|
||||
|
||||
if self
|
||||
.assignments
|
||||
.get(assignment.name.lexeme())
|
||||
.get(variable)
|
||||
.map_or(true, |original| assignment.depth <= original.depth)
|
||||
{
|
||||
self.assignments.insert(assignment.clone());
|
||||
}
|
||||
|
||||
if unexports.contains(variable) {
|
||||
return Err(assignment.name.token.error(ExportUnexported { variable }));
|
||||
}
|
||||
}
|
||||
|
||||
AssignmentResolver::resolve_assignments(&self.assignments)?;
|
||||
@ -138,7 +177,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();
|
||||
while let Some(alias) = self.aliases.pop() {
|
||||
@ -161,12 +200,15 @@ impl<'src> Analyzer<'src> {
|
||||
Rc::clone(next)
|
||||
}),
|
||||
}),
|
||||
doc,
|
||||
loaded: loaded.into(),
|
||||
modules,
|
||||
name,
|
||||
recipes,
|
||||
settings,
|
||||
source: root.into(),
|
||||
unexports,
|
||||
unstable_features: BTreeSet::new(),
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
@ -213,16 +255,29 @@ impl<'src> Analyzer<'src> {
|
||||
continued = line.is_continuation();
|
||||
}
|
||||
|
||||
if !recipe.shebang {
|
||||
if let Some(attribute) = recipe
|
||||
.attributes
|
||||
.iter()
|
||||
.find(|attribute| matches!(attribute, Attribute::Extension(_)))
|
||||
{
|
||||
return Err(recipe.name.error(InvalidAttribute {
|
||||
item_kind: "Recipe",
|
||||
item_name: recipe.name.lexeme(),
|
||||
attribute: attribute.clone(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn analyze_alias(alias: &Alias<'src, Name<'src>>) -> CompileResult<'src> {
|
||||
let name = alias.name.lexeme();
|
||||
|
||||
for attribute in &alias.attributes {
|
||||
if *attribute != Attribute::Private {
|
||||
return Err(alias.name.token.error(AliasInvalidAttribute {
|
||||
alias: name,
|
||||
return Err(alias.name.token.error(InvalidAttribute {
|
||||
item_kind: "Alias",
|
||||
item_name: alias.name.lexeme(),
|
||||
attribute: attribute.clone(),
|
||||
}));
|
||||
}
|
||||
|
403
src/argument_parser.rs
Normal file
403
src/argument_parser.rs
Normal file
@ -0,0 +1,403 @@
|
||||
use super::*;
|
||||
|
||||
#[allow(clippy::doc_markdown)]
|
||||
/// The argument parser is responsible for grouping positional arguments into
|
||||
/// argument groups, which consist of a path to a recipe and its arguments.
|
||||
///
|
||||
/// Argument parsing is substantially complicated by the fact that recipe paths
|
||||
/// can be given on the command line as multiple arguments, i.e., "foo" "bar"
|
||||
/// baz", or as a single "::"-separated argument.
|
||||
///
|
||||
/// Error messages produced by the argument parser should use the format of the
|
||||
/// recipe path as passed on the command line.
|
||||
///
|
||||
/// Additionally, if a recipe is specified with a "::"-separated path, extra
|
||||
/// components of that path after a valid recipe must not be used as arguments,
|
||||
/// whereas arguments after multiple argument path may be used as arguments. As
|
||||
/// an example, `foo bar baz` may refer to recipe `foo::bar` with argument
|
||||
/// `baz`, but `foo::bar::baz` is an error, since `bar` is a recipe, not a
|
||||
/// module.
|
||||
pub(crate) struct ArgumentParser<'src: 'run, 'run> {
|
||||
arguments: &'run [&'run str],
|
||||
next: usize,
|
||||
root: &'run Justfile<'src>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct ArgumentGroup<'run> {
|
||||
pub(crate) arguments: Vec<&'run str>,
|
||||
pub(crate) path: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
|
||||
pub(crate) fn parse_arguments(
|
||||
root: &'run Justfile<'src>,
|
||||
arguments: &'run [&'run str],
|
||||
) -> RunResult<'src, Vec<ArgumentGroup<'run>>> {
|
||||
let mut groups = Vec::new();
|
||||
|
||||
let mut invocation_parser = Self {
|
||||
arguments,
|
||||
next: 0,
|
||||
root,
|
||||
};
|
||||
|
||||
loop {
|
||||
groups.push(invocation_parser.parse_group()?);
|
||||
|
||||
if invocation_parser.next == arguments.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn parse_group(&mut self) -> RunResult<'src, ArgumentGroup<'run>> {
|
||||
let (recipe, path) = if let Some(next) = self.next() {
|
||||
if next.contains(':') {
|
||||
let module_path =
|
||||
ModulePath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {
|
||||
recipe: next.into(),
|
||||
suggestion: None,
|
||||
})?;
|
||||
let (recipe, path, _) = self.resolve_recipe(true, &module_path.path)?;
|
||||
self.next += 1;
|
||||
(recipe, path)
|
||||
} else {
|
||||
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||
self.next += consumed;
|
||||
(recipe, path)
|
||||
}
|
||||
} else {
|
||||
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
|
||||
assert_eq!(consumed, 0);
|
||||
(recipe, path)
|
||||
};
|
||||
|
||||
let rest = self.rest();
|
||||
|
||||
let argument_range = recipe.argument_range();
|
||||
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
||||
if !argument_range.range_contains(&argument_count) {
|
||||
return Err(Error::ArgumentCountMismatch {
|
||||
recipe: recipe.name(),
|
||||
parameters: recipe.parameters.clone(),
|
||||
found: rest.len(),
|
||||
min: recipe.min_arguments(),
|
||||
max: recipe.max_arguments(),
|
||||
});
|
||||
}
|
||||
|
||||
let arguments = rest[..argument_count].to_vec();
|
||||
|
||||
self.next += argument_count;
|
||||
|
||||
Ok(ArgumentGroup { arguments, path })
|
||||
}
|
||||
|
||||
fn resolve_recipe(
|
||||
&self,
|
||||
module_path: bool,
|
||||
args: &[impl AsRef<str>],
|
||||
) -> RunResult<'src, (&'run Recipe<'src>, Vec<String>, usize)> {
|
||||
let mut current = self.root;
|
||||
let mut path = Vec::new();
|
||||
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
let arg = arg.as_ref();
|
||||
|
||||
path.push(arg.to_string());
|
||||
|
||||
if let Some(module) = current.modules.get(arg) {
|
||||
current = module;
|
||||
} else if let Some(recipe) = current.get_recipe(arg) {
|
||||
if module_path && i + 1 < args.len() {
|
||||
return Err(Error::ExpectedSubmoduleButFoundRecipe {
|
||||
path: if module_path {
|
||||
path.join("::")
|
||||
} else {
|
||||
path.join(" ")
|
||||
},
|
||||
});
|
||||
}
|
||||
return Ok((recipe, path, i + 1));
|
||||
} else {
|
||||
if module_path && i + 1 < args.len() {
|
||||
return Err(Error::UnknownSubmodule {
|
||||
path: path.join("::"),
|
||||
});
|
||||
}
|
||||
|
||||
return Err(Error::UnknownRecipe {
|
||||
recipe: if module_path {
|
||||
path.join("::")
|
||||
} else {
|
||||
path.join(" ")
|
||||
},
|
||||
suggestion: current.suggest_recipe(arg),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(recipe) = ¤t.default {
|
||||
recipe.check_can_be_default_recipe()?;
|
||||
path.push(recipe.name().into());
|
||||
Ok((recipe, path, args.len()))
|
||||
} else if current.recipes.is_empty() {
|
||||
Err(Error::NoRecipes)
|
||||
} else {
|
||||
Err(Error::NoDefaultRecipe)
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&self) -> Option<&'run str> {
|
||||
self.arguments.get(self.next).copied()
|
||||
}
|
||||
|
||||
fn rest(&self) -> &[&'run str] {
|
||||
&self.arguments[self.next..]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {super::*, tempfile::TempDir};
|
||||
|
||||
trait TempDirExt {
|
||||
fn write(&self, path: &str, content: &str);
|
||||
}
|
||||
|
||||
impl TempDirExt for TempDir {
|
||||
fn write(&self, path: &str, content: &str) {
|
||||
let path = self.path().join(path);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, content).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_no_arguments() {
|
||||
let justfile = testing::compile("foo:");
|
||||
|
||||
assert_eq!(
|
||||
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap(),
|
||||
vec![ArgumentGroup {
|
||||
path: vec!["foo".into()],
|
||||
arguments: Vec::new()
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_with_argument() {
|
||||
let justfile = testing::compile("foo bar:");
|
||||
|
||||
assert_eq!(
|
||||
ArgumentParser::parse_arguments(&justfile, &["foo", "baz"]).unwrap(),
|
||||
vec![ArgumentGroup {
|
||||
path: vec!["foo".into()],
|
||||
arguments: vec!["baz"],
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_argument_count_mismatch() {
|
||||
let justfile = testing::compile("foo bar:");
|
||||
|
||||
assert_matches!(
|
||||
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap_err(),
|
||||
Error::ArgumentCountMismatch {
|
||||
recipe: "foo",
|
||||
found: 0,
|
||||
min: 1,
|
||||
max: 1,
|
||||
..
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_unknown() {
|
||||
let justfile = testing::compile("foo:");
|
||||
|
||||
assert_matches!(
|
||||
ArgumentParser::parse_arguments(&justfile, &["bar"]).unwrap_err(),
|
||||
Error::UnknownRecipe {
|
||||
recipe,
|
||||
suggestion: None
|
||||
} if recipe == "bar",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_unknown() {
|
||||
let justfile = testing::compile("foo:");
|
||||
|
||||
assert_matches!(
|
||||
ArgumentParser::parse_arguments(&justfile, &["bar", "baz"]).unwrap_err(),
|
||||
Error::UnknownRecipe {
|
||||
recipe,
|
||||
suggestion: None
|
||||
} if recipe == "bar",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipe_in_submodule() {
|
||||
let loader = Loader::new();
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let path = tempdir.path().join("justfile");
|
||||
fs::write(&path, "mod foo").unwrap();
|
||||
fs::create_dir(tempdir.path().join("foo")).unwrap();
|
||||
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
|
||||
let compilation = Compiler::compile(&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(&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(&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(&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(&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(&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(&loader, &tempdir.path().join("justfile")).unwrap();
|
||||
|
||||
assert_matches!(
|
||||
ArgumentParser::parse_arguments(&compilation.justfile, &[]).unwrap_err(),
|
||||
Error::NoDefaultRecipe,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_grouping() {
|
||||
let justfile = testing::compile(
|
||||
"
|
||||
FOO A B='blarg':
|
||||
echo foo: {{A}} {{B}}
|
||||
|
||||
BAR X:
|
||||
echo bar: {{X}}
|
||||
|
||||
BAZ +Z:
|
||||
echo baz: {{Z}}
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ArgumentParser::parse_arguments(
|
||||
&justfile,
|
||||
&["BAR", "0", "FOO", "1", "2", "BAZ", "3", "4", "5"]
|
||||
)
|
||||
.unwrap(),
|
||||
vec![
|
||||
ArgumentGroup {
|
||||
path: vec!["BAR".into()],
|
||||
arguments: vec!["0"],
|
||||
},
|
||||
ArgumentGroup {
|
||||
path: vec!["FOO".into()],
|
||||
arguments: vec!["1", "2"],
|
||||
},
|
||||
ArgumentGroup {
|
||||
path: vec!["BAZ".into()],
|
||||
arguments: vec!["3", "4", "5"],
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use super::*;
|
||||
pub(crate) type Assignment<'src> = Binding<'src, Expression<'src>>;
|
||||
|
||||
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 {
|
||||
write!(f, "export ")?;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ pub(crate) struct 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();
|
||||
|
||||
while let Some(item) = iter.next() {
|
||||
|
@ -11,12 +11,14 @@ use super::*;
|
||||
pub(crate) enum Attribute<'src> {
|
||||
Confirm(Option<StringLiteral<'src>>),
|
||||
Doc(Option<StringLiteral<'src>>),
|
||||
Extension(StringLiteral<'src>),
|
||||
Group(StringLiteral<'src>),
|
||||
Linux,
|
||||
Macos,
|
||||
NoCd,
|
||||
NoExitMessage,
|
||||
NoQuiet,
|
||||
PositionalArguments,
|
||||
Private,
|
||||
Unix,
|
||||
Windows,
|
||||
@ -26,12 +28,13 @@ impl AttributeDiscriminant {
|
||||
fn argument_range(self) -> RangeInclusive<usize> {
|
||||
match self {
|
||||
Self::Confirm | Self::Doc => 0..=1,
|
||||
Self::Group => 1..=1,
|
||||
Self::Group | Self::Extension => 1..=1,
|
||||
Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
| Self::PositionalArguments
|
||||
| Self::Private
|
||||
| Self::Unix
|
||||
| Self::Windows => 0..=0,
|
||||
@ -44,8 +47,6 @@ impl<'src> Attribute<'src> {
|
||||
name: Name<'src>,
|
||||
argument: Option<StringLiteral<'src>>,
|
||||
) -> CompileResult<'src, Self> {
|
||||
use AttributeDiscriminant::*;
|
||||
|
||||
let discriminant = name
|
||||
.lexeme()
|
||||
.parse::<AttributeDiscriminant>()
|
||||
@ -57,9 +58,7 @@ impl<'src> Attribute<'src> {
|
||||
})?;
|
||||
|
||||
let found = argument.as_ref().iter().count();
|
||||
|
||||
let range = discriminant.argument_range();
|
||||
|
||||
if !range.contains(&found) {
|
||||
return Err(
|
||||
name.error(CompileErrorKind::AttributeArgumentCountMismatch {
|
||||
@ -72,17 +71,19 @@ impl<'src> Attribute<'src> {
|
||||
}
|
||||
|
||||
Ok(match discriminant {
|
||||
Confirm => Self::Confirm(argument),
|
||||
Doc => Self::Doc(argument),
|
||||
Group => Self::Group(argument.unwrap()),
|
||||
Linux => Self::Linux,
|
||||
Macos => Self::Macos,
|
||||
NoCd => Self::NoCd,
|
||||
NoExitMessage => Self::NoExitMessage,
|
||||
NoQuiet => Self::NoQuiet,
|
||||
Private => Self::Private,
|
||||
Unix => Self::Unix,
|
||||
Windows => Self::Windows,
|
||||
AttributeDiscriminant::Confirm => Self::Confirm(argument),
|
||||
AttributeDiscriminant::Doc => Self::Doc(argument),
|
||||
AttributeDiscriminant::Extension => Self::Extension(argument.unwrap()),
|
||||
AttributeDiscriminant::Group => Self::Group(argument.unwrap()),
|
||||
AttributeDiscriminant::Linux => Self::Linux,
|
||||
AttributeDiscriminant::Macos => Self::Macos,
|
||||
AttributeDiscriminant::NoCd => Self::NoCd,
|
||||
AttributeDiscriminant::NoExitMessage => Self::NoExitMessage,
|
||||
AttributeDiscriminant::NoQuiet => Self::NoQuiet,
|
||||
AttributeDiscriminant::PositionalArguments => Self::PositionalArguments,
|
||||
AttributeDiscriminant::Private => Self::Private,
|
||||
AttributeDiscriminant::Unix => Self::Unix,
|
||||
AttributeDiscriminant::Windows => Self::Windows,
|
||||
})
|
||||
}
|
||||
|
||||
@ -92,14 +93,14 @@ impl<'src> Attribute<'src> {
|
||||
|
||||
fn argument(&self) -> Option<&StringLiteral> {
|
||||
match self {
|
||||
Self::Confirm(prompt) => prompt.as_ref(),
|
||||
Self::Doc(doc) => doc.as_ref(),
|
||||
Self::Group(group) => Some(group),
|
||||
Self::Confirm(argument) | Self::Doc(argument) => argument.as_ref(),
|
||||
Self::Extension(argument) | Self::Group(argument) => Some(argument),
|
||||
Self::Linux
|
||||
| Self::Macos
|
||||
| Self::NoCd
|
||||
| Self::NoExitMessage
|
||||
| Self::NoQuiet
|
||||
| Self::PositionalArguments
|
||||
| Self::Private
|
||||
| Self::Unix
|
||||
| Self::Windows => None,
|
||||
@ -108,7 +109,7 @@ impl<'src> 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())?;
|
||||
if let Some(argument) = self.argument() {
|
||||
write!(f, "({argument})")?;
|
||||
|
10
src/color.rs
10
src/color.rs
@ -38,6 +38,7 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn always() -> Self {
|
||||
Self {
|
||||
use_color: UseColor::Always,
|
||||
@ -133,6 +134,15 @@ impl Color {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UseColor> for Color {
|
||||
fn from(use_color: UseColor) -> Self {
|
||||
Self {
|
||||
use_color,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Color {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
26
src/command_color.rs
Normal file
26
src/command_color.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, ValueEnum)]
|
||||
pub(crate) enum CommandColor {
|
||||
Black,
|
||||
Blue,
|
||||
Cyan,
|
||||
Green,
|
||||
Purple,
|
||||
Red,
|
||||
Yellow,
|
||||
}
|
||||
|
||||
impl From<CommandColor> for ansi_term::Color {
|
||||
fn from(command_color: CommandColor) -> Self {
|
||||
match command_color {
|
||||
CommandColor::Black => Self::Black,
|
||||
CommandColor::Blue => Self::Blue,
|
||||
CommandColor::Cyan => Self::Cyan,
|
||||
CommandColor::Green => Self::Green,
|
||||
CommandColor::Purple => Self::Purple,
|
||||
CommandColor::Red => Self::Red,
|
||||
CommandColor::Yellow => Self::Yellow,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +1,41 @@
|
||||
use super::*;
|
||||
|
||||
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 {
|
||||
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 {
|
||||
self.env(name, value);
|
||||
}
|
||||
|
||||
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() {
|
||||
self.export_scope(settings, parent);
|
||||
self.export_scope(settings, parent, unexports);
|
||||
}
|
||||
|
||||
for unexport in unexports {
|
||||
self.env_remove(unexport);
|
||||
}
|
||||
|
||||
for binding in scope.bindings() {
|
||||
|
@ -28,17 +28,10 @@ fn capitalize(s: &str) -> String {
|
||||
}
|
||||
|
||||
impl Display for CompileError<'_> {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
use CompileErrorKind::*;
|
||||
|
||||
match &*self.kind {
|
||||
AliasInvalidAttribute { alias, attribute } => {
|
||||
write!(
|
||||
f,
|
||||
"Alias `{alias}` has invalid attribute `{}`",
|
||||
attribute.name(),
|
||||
)
|
||||
}
|
||||
AliasShadowsRecipe { alias, recipe_line } => write!(
|
||||
f,
|
||||
"Alias `{alias}` defined on line {} shadows recipe `{alias}` defined on line {}",
|
||||
@ -131,6 +124,9 @@ impl Display for CompileError<'_> {
|
||||
DuplicateVariable { variable } => {
|
||||
write!(f, "Variable `{variable}` has multiple definitions")
|
||||
}
|
||||
DuplicateUnexport { variable } => {
|
||||
write!(f, "Variable `{variable}` is unexported multiple times")
|
||||
}
|
||||
ExpectedKeyword { expected, found } => {
|
||||
let expected = List::or_ticked(expected);
|
||||
if found.kind == TokenKind::Identifier {
|
||||
@ -143,7 +139,13 @@ impl Display for CompileError<'_> {
|
||||
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"),
|
||||
ExtraneousAttributes { count } => {
|
||||
write!(f, "Extraneous {}", Count("attribute", *count))
|
||||
}
|
||||
FunctionArgumentCountMismatch {
|
||||
function,
|
||||
found,
|
||||
@ -170,6 +172,15 @@ impl Display for CompileError<'_> {
|
||||
"Internal error, this may indicate a bug in just: {message}\n\
|
||||
consider filing an issue: https://github.com/casey/just/issues/new"
|
||||
),
|
||||
InvalidAttribute {
|
||||
item_name,
|
||||
item_kind,
|
||||
attribute,
|
||||
} => write!(
|
||||
f,
|
||||
"{item_kind} `{item_name}` has invalid attribute `{}`",
|
||||
attribute.name(),
|
||||
),
|
||||
InvalidEscapeSequence { character } => write!(
|
||||
f,
|
||||
"`\\{}` is not a valid escape sequence",
|
||||
|
@ -2,10 +2,6 @@ use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum CompileErrorKind<'src> {
|
||||
AliasInvalidAttribute {
|
||||
alias: &'src str,
|
||||
attribute: Attribute<'src>,
|
||||
},
|
||||
AliasShadowsRecipe {
|
||||
alias: &'src str,
|
||||
recipe_line: usize,
|
||||
@ -52,15 +48,24 @@ pub(crate) enum CompileErrorKind<'src> {
|
||||
DuplicateVariable {
|
||||
variable: &'src str,
|
||||
},
|
||||
DuplicateUnexport {
|
||||
variable: &'src str,
|
||||
},
|
||||
ExpectedKeyword {
|
||||
expected: Vec<Keyword>,
|
||||
found: Token<'src>,
|
||||
},
|
||||
ExportUnexported {
|
||||
variable: &'src str,
|
||||
},
|
||||
ExtraLeadingWhitespace,
|
||||
ExtraneousAttributes {
|
||||
count: usize,
|
||||
},
|
||||
FunctionArgumentCountMismatch {
|
||||
function: &'src str,
|
||||
found: usize,
|
||||
expected: Range<usize>,
|
||||
expected: RangeInclusive<usize>,
|
||||
},
|
||||
Include,
|
||||
InconsistentLeadingWhitespace {
|
||||
@ -70,6 +75,11 @@ pub(crate) enum CompileErrorKind<'src> {
|
||||
Internal {
|
||||
message: String,
|
||||
},
|
||||
InvalidAttribute {
|
||||
item_kind: &'static str,
|
||||
item_name: &'src str,
|
||||
attribute: Attribute<'src>,
|
||||
},
|
||||
InvalidEscapeSequence {
|
||||
character: char,
|
||||
},
|
||||
|
211
src/compiler.rs
211
src/compiler.rs
@ -4,14 +4,13 @@ pub(crate) struct Compiler;
|
||||
|
||||
impl Compiler {
|
||||
pub(crate) fn compile<'src>(
|
||||
unstable: bool,
|
||||
loader: &'src Loader,
|
||||
root: &Path,
|
||||
) -> RunResult<'src, Compilation<'src>> {
|
||||
let mut asts = HashMap::<PathBuf, Ast>::new();
|
||||
let mut loaded = Vec::new();
|
||||
let mut paths = HashMap::<PathBuf, PathBuf>::new();
|
||||
let mut srcs = HashMap::<PathBuf, &str>::new();
|
||||
let mut loaded = Vec::new();
|
||||
|
||||
let mut stack = Vec::new();
|
||||
stack.push(Source::root(root));
|
||||
@ -40,26 +39,16 @@ impl Compiler {
|
||||
name,
|
||||
optional,
|
||||
relative,
|
||||
..
|
||||
} => {
|
||||
if !unstable {
|
||||
return Err(Error::Unstable {
|
||||
message: "Modules are currently unstable.".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let parent = current.path.parent().unwrap();
|
||||
|
||||
let import = if let Some(relative) = relative {
|
||||
let path = parent.join(Self::expand_tilde(&relative.cooked)?);
|
||||
let relative = relative
|
||||
.as_ref()
|
||||
.map(|relative| Self::expand_tilde(&relative.cooked))
|
||||
.transpose()?;
|
||||
|
||||
if path.is_file() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Self::find_module_file(parent, *name)?
|
||||
};
|
||||
let import = Self::find_module_file(parent, *name, relative.as_deref())?;
|
||||
|
||||
if let Some(import) = import {
|
||||
if current.file_path.contains(&import) {
|
||||
@ -107,29 +96,73 @@ impl Compiler {
|
||||
asts.insert(current.path, ast.clone());
|
||||
}
|
||||
|
||||
let justfile = Analyzer::analyze(&loaded, &paths, &asts, root, None)?;
|
||||
let justfile = Analyzer::analyze(&asts, None, &loaded, None, &paths, root)?;
|
||||
|
||||
Ok(Compilation {
|
||||
asts,
|
||||
srcs,
|
||||
justfile,
|
||||
root: root.into(),
|
||||
srcs,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, Option<PathBuf>> {
|
||||
let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
|
||||
.into_iter()
|
||||
.filter(|path| parent.join(path).is_file())
|
||||
.collect::<Vec<String>>();
|
||||
fn find_module_file<'src>(
|
||||
parent: &Path,
|
||||
module: Name<'src>,
|
||||
path: Option<&Path>,
|
||||
) -> RunResult<'src, Option<PathBuf>> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
let directory = parent.join(module.lexeme());
|
||||
if let Some(path) = path {
|
||||
let full = parent.join(path);
|
||||
|
||||
if directory.exists() {
|
||||
let entries = fs::read_dir(&directory).map_err(|io_error| SearchError::Io {
|
||||
io_error,
|
||||
directory: directory.clone(),
|
||||
})?;
|
||||
if full.is_file() {
|
||||
return Ok(Some(full));
|
||||
}
|
||||
|
||||
candidates.push((path.join("mod.just"), true));
|
||||
|
||||
for name in search::JUSTFILE_NAMES {
|
||||
candidates.push((path.join(name), false));
|
||||
}
|
||||
} else {
|
||||
candidates.push((format!("{module}.just").into(), true));
|
||||
candidates.push((format!("{module}/mod.just").into(), true));
|
||||
|
||||
for name in search::JUSTFILE_NAMES {
|
||||
candidates.push((format!("{module}/{name}").into(), false));
|
||||
}
|
||||
}
|
||||
|
||||
let mut grouped = BTreeMap::<PathBuf, Vec<(PathBuf, bool)>>::new();
|
||||
|
||||
for (candidate, case_sensitive) in candidates {
|
||||
let candidate = parent.join(candidate).lexiclean();
|
||||
grouped
|
||||
.entry(candidate.parent().unwrap().into())
|
||||
.or_default()
|
||||
.push((candidate, case_sensitive));
|
||||
}
|
||||
|
||||
let mut found = Vec::new();
|
||||
|
||||
for (directory, candidates) in grouped {
|
||||
let entries = match fs::read_dir(&directory) {
|
||||
Ok(entries) => entries,
|
||||
Err(io_error) => {
|
||||
if io_error.kind() == io::ErrorKind::NotFound {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(
|
||||
SearchError::Io {
|
||||
io_error,
|
||||
directory,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|io_error| SearchError::Io {
|
||||
@ -138,22 +171,34 @@ impl Compiler {
|
||||
})?;
|
||||
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
for justfile_name in search::JUSTFILE_NAMES {
|
||||
if name.eq_ignore_ascii_case(justfile_name) {
|
||||
candidates.push(format!("{module}/{name}"));
|
||||
for (candidate, case_sensitive) in &candidates {
|
||||
let candidate_name = candidate.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
let eq = if *case_sensitive {
|
||||
name == candidate_name
|
||||
} else {
|
||||
name.eq_ignore_ascii_case(candidate_name)
|
||||
};
|
||||
|
||||
if eq {
|
||||
found.push(candidate.parent().unwrap().join(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match candidates.as_slice() {
|
||||
[] => Ok(None),
|
||||
[file] => Ok(Some(parent.join(file).lexiclean())),
|
||||
found => Err(Error::AmbiguousModuleFile {
|
||||
found: found.into(),
|
||||
if found.len() > 1 {
|
||||
found.sort();
|
||||
Err(Error::AmbiguousModuleFile {
|
||||
found: found
|
||||
.into_iter()
|
||||
.map(|found| found.strip_prefix(parent).unwrap().into())
|
||||
.collect(),
|
||||
module,
|
||||
}),
|
||||
})
|
||||
} else {
|
||||
Ok(found.into_iter().next())
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +229,7 @@ impl Compiler {
|
||||
asts.insert(root.clone(), ast);
|
||||
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||
paths.insert(root.clone(), root.clone());
|
||||
Analyzer::analyze(&[], &paths, &asts, &root, None)
|
||||
Analyzer::analyze(&asts, None, &[], None, &paths, &root)
|
||||
}
|
||||
}
|
||||
|
||||
@ -224,7 +269,7 @@ recipe_b: recipe_c
|
||||
let loader = Loader::new();
|
||||
|
||||
let justfile_a_path = tmp.path().join("justfile");
|
||||
let compilation = Compiler::compile(false, &loader, &justfile_a_path).unwrap();
|
||||
let compilation = Compiler::compile(&loader, &justfile_a_path).unwrap();
|
||||
|
||||
assert_eq!(compilation.root_src(), justfile_a);
|
||||
}
|
||||
@ -241,11 +286,91 @@ recipe_b: recipe_c
|
||||
let loader = Loader::new();
|
||||
|
||||
let justfile_a_path = tmp.path().join("justfile");
|
||||
let loader_output = Compiler::compile(false, &loader, &justfile_a_path).unwrap_err();
|
||||
let loader_output = Compiler::compile(&loader, &justfile_a_path).unwrap_err();
|
||||
|
||||
assert_matches!(loader_output, Error::CircularImport { current, import }
|
||||
if current == tmp.path().join("subdir").join("b").lexiclean() &&
|
||||
import == tmp.path().join("justfile").lexiclean()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_module_file() {
|
||||
#[track_caller]
|
||||
fn case(path: Option<&str>, files: &[&str], expected: Result<Option<&str>, &[&str]>) {
|
||||
let module = Name {
|
||||
token: Token {
|
||||
column: 0,
|
||||
kind: TokenKind::Identifier,
|
||||
length: 3,
|
||||
line: 0,
|
||||
offset: 0,
|
||||
path: Path::new(""),
|
||||
src: "foo",
|
||||
},
|
||||
};
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
|
||||
for file in files {
|
||||
if let Some(parent) = Path::new(file).parent() {
|
||||
fs::create_dir_all(tempdir.path().join(parent)).unwrap();
|
||||
}
|
||||
|
||||
fs::write(tempdir.path().join(file), "").unwrap();
|
||||
}
|
||||
|
||||
let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new));
|
||||
|
||||
match expected {
|
||||
Err(expected) => match actual.unwrap_err() {
|
||||
Error::AmbiguousModuleFile { found, .. } => {
|
||||
assert_eq!(
|
||||
found,
|
||||
expected
|
||||
.iter()
|
||||
.map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into())
|
||||
.collect::<Vec<PathBuf>>()
|
||||
);
|
||||
}
|
||||
_ => panic!("unexpected error"),
|
||||
},
|
||||
Ok(Some(expected)) => assert_eq!(
|
||||
actual.unwrap().unwrap(),
|
||||
tempdir
|
||||
.path()
|
||||
.join(expected.replace('/', std::path::MAIN_SEPARATOR_STR))
|
||||
),
|
||||
Ok(None) => assert_eq!(actual.unwrap(), None),
|
||||
}
|
||||
}
|
||||
|
||||
case(None, &["foo.just"], Ok(Some("foo.just")));
|
||||
case(None, &["FOO.just"], Ok(None));
|
||||
case(None, &["foo/mod.just"], Ok(Some("foo/mod.just")));
|
||||
case(None, &["foo/MOD.just"], Ok(None));
|
||||
case(None, &["foo/justfile"], Ok(Some("foo/justfile")));
|
||||
case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
|
||||
case(None, &["foo/.justfile"], Ok(Some("foo/.justfile")));
|
||||
case(None, &["foo/.JUSTFILE"], Ok(Some("foo/.JUSTFILE")));
|
||||
case(
|
||||
None,
|
||||
&["foo/.justfile", "foo/justfile"],
|
||||
Err(&["foo/.justfile", "foo/justfile"]),
|
||||
);
|
||||
case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
|
||||
|
||||
case(Some("bar"), &["bar"], Ok(Some("bar")));
|
||||
case(Some("bar"), &["bar/mod.just"], Ok(Some("bar/mod.just")));
|
||||
case(Some("bar"), &["bar/justfile"], Ok(Some("bar/justfile")));
|
||||
case(Some("bar"), &["bar/JUSTFILE"], Ok(Some("bar/JUSTFILE")));
|
||||
case(Some("bar"), &["bar/.justfile"], Ok(Some("bar/.justfile")));
|
||||
case(Some("bar"), &["bar/.JUSTFILE"], Ok(Some("bar/.JUSTFILE")));
|
||||
|
||||
case(
|
||||
Some("bar"),
|
||||
&["bar/justfile", "bar/mod.just"],
|
||||
Err(&["bar/justfile", "bar/mod.just"]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,102 @@
|
||||
pub(crate) const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
|
||||
use super::*;
|
||||
|
||||
#[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 '{
|
||||
command = $1;
|
||||
args = $0;
|
||||
@ -37,9 +135,9 @@ complete -c just -a '(__fish_just_complete_recipes)'
|
||||
# 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=(",
|
||||
),
|
||||
(
|
||||
@ -151,13 +249,13 @@ _just "$@""#,
|
||||
),
|
||||
];
|
||||
|
||||
pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
||||
const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
||||
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
|
||||
Sort-Object -Property ListItemText"#,
|
||||
r#"function Get-JustFileRecipes([string[]]$CommandElements) {
|
||||
$justFileIndex = $commandElements.IndexOf("--justfile");
|
||||
|
||||
if ($justFileIndex -ne -1 && $justFileIndex + 1 -le $commandElements.Length) {
|
||||
if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {
|
||||
$justFileLocation = $commandElements[$justFileIndex + 1]
|
||||
}
|
||||
|
||||
@ -178,7 +276,7 @@ pub(crate) const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
||||
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
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
|
@ -8,7 +8,7 @@ pub(crate) struct 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)
|
||||
}
|
||||
}
|
||||
|
498
src/config.rs
498
src/config.rs
@ -1,24 +1,12 @@
|
||||
use {
|
||||
super::*,
|
||||
clap::{
|
||||
builder::{styling::AnsiColor, FalseyValueParser, PossibleValuesParser, Styles},
|
||||
builder::{styling::AnsiColor, FalseyValueParser, Styles},
|
||||
parser::ValuesRef,
|
||||
value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command,
|
||||
},
|
||||
};
|
||||
|
||||
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)]
|
||||
pub(crate) struct Config {
|
||||
pub(crate) check: bool,
|
||||
@ -32,6 +20,7 @@ pub(crate) struct Config {
|
||||
pub(crate) invocation_directory: PathBuf,
|
||||
pub(crate) list_heading: String,
|
||||
pub(crate) list_prefix: String,
|
||||
pub(crate) list_submodules: bool,
|
||||
pub(crate) load_dotenv: bool,
|
||||
pub(crate) no_aliases: bool,
|
||||
pub(crate) no_dependencies: bool,
|
||||
@ -97,11 +86,12 @@ mod arg {
|
||||
pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH";
|
||||
pub(crate) const DRY_RUN: &str = "DRY-RUN";
|
||||
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 JUSTFILE: &str = "JUSTFILE";
|
||||
pub(crate) const LIST_HEADING: &str = "LIST-HEADING";
|
||||
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_DEPS: &str = "NO-DEPS";
|
||||
pub(crate) const NO_DOTENV: &str = "NO-DOTENV";
|
||||
@ -112,38 +102,12 @@ mod arg {
|
||||
pub(crate) const SHELL_ARG: &str = "SHELL-ARG";
|
||||
pub(crate) const SHELL_COMMAND: &str = "SHELL-COMMAND";
|
||||
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 UNSTABLE: &str = "UNSTABLE";
|
||||
pub(crate) const VERBOSE: &str = "VERBOSE";
|
||||
pub(crate) const WORKING_DIRECTORY: &str = "WORKING-DIRECTORY";
|
||||
pub(crate) const YES: &str = "YES";
|
||||
|
||||
pub(crate) const COLOR_ALWAYS: &str = "always";
|
||||
pub(crate) const COLOR_AUTO: &str = "auto";
|
||||
pub(crate) const COLOR_NEVER: &str = "never";
|
||||
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
||||
|
||||
pub(crate) const COMMAND_COLOR_BLACK: &str = "black";
|
||||
pub(crate) const COMMAND_COLOR_BLUE: &str = "blue";
|
||||
pub(crate) const COMMAND_COLOR_CYAN: &str = "cyan";
|
||||
pub(crate) const COMMAND_COLOR_GREEN: &str = "green";
|
||||
pub(crate) const COMMAND_COLOR_PURPLE: &str = "purple";
|
||||
pub(crate) const COMMAND_COLOR_RED: &str = "red";
|
||||
pub(crate) const COMMAND_COLOR_YELLOW: &str = "yellow";
|
||||
pub(crate) const COMMAND_COLOR_VALUES: &[&str] = &[
|
||||
COMMAND_COLOR_BLACK,
|
||||
COMMAND_COLOR_BLUE,
|
||||
COMMAND_COLOR_CYAN,
|
||||
COMMAND_COLOR_GREEN,
|
||||
COMMAND_COLOR_PURPLE,
|
||||
COMMAND_COLOR_RED,
|
||||
COMMAND_COLOR_YELLOW,
|
||||
];
|
||||
|
||||
pub(crate) const DUMP_FORMAT_JSON: &str = "json";
|
||||
pub(crate) const DUMP_FORMAT_JUST: &str = "just";
|
||||
pub(crate) const DUMP_FORMAT_VALUES: &[&str] = &[DUMP_FORMAT_JUST, DUMP_FORMAT_JSON];
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -160,17 +124,20 @@ impl Config {
|
||||
.trailing_var_arg(true)
|
||||
.styles(
|
||||
Styles::styled()
|
||||
.header(AnsiColor::Yellow.on_default())
|
||||
.usage(AnsiColor::Yellow.on_default())
|
||||
.literal(AnsiColor::Green.on_default())
|
||||
.placeholder(AnsiColor::Green.on_default())
|
||||
.header(AnsiColor::Yellow.on_default())
|
||||
.literal(AnsiColor::Green.on_default())
|
||||
.placeholder(AnsiColor::Green.on_default())
|
||||
.usage(AnsiColor::Yellow.on_default()),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(arg::CHECK)
|
||||
.long("check")
|
||||
.action(ArgAction::SetTrue)
|
||||
.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::new(arg::CHOOSER)
|
||||
@ -179,13 +146,20 @@ impl Config {
|
||||
.action(ArgAction::Set)
|
||||
.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::new(arg::COLOR)
|
||||
.long("color")
|
||||
.env("JUST_COLOR")
|
||||
.action(ArgAction::Set)
|
||||
.value_parser(PossibleValuesParser::new(arg::COLOR_VALUES))
|
||||
.default_value(arg::COLOR_AUTO)
|
||||
.value_parser(clap::value_parser!(UseColor))
|
||||
.default_value("auto")
|
||||
.help("Print colorful output"),
|
||||
)
|
||||
.arg(
|
||||
@ -193,10 +167,24 @@ impl Config {
|
||||
.long("command-color")
|
||||
.env("JUST_COMMAND_COLOR")
|
||||
.action(ArgAction::Set)
|
||||
.value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES))
|
||||
.value_parser(clap::value_parser!(CommandColor))
|
||||
.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::new(arg::DRY_RUN)
|
||||
.short('n')
|
||||
@ -209,59 +197,30 @@ impl Config {
|
||||
.arg(
|
||||
Arg::new(arg::DUMP_FORMAT)
|
||||
.long("dump-format")
|
||||
.env("JUST_DUMP_FORMAT")
|
||||
.action(ArgAction::Set)
|
||||
.value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES))
|
||||
.default_value(arg::DUMP_FORMAT_JUST)
|
||||
.value_parser(clap::value_parser!(DumpFormat))
|
||||
.default_value("just")
|
||||
.value_name("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::new(arg::HIGHLIGHT)
|
||||
.long("highlight")
|
||||
.env("JUST_HIGHLIGHT")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Highlight echoed recipe lines in bold")
|
||||
.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::new(arg::JUSTFILE)
|
||||
.short('f')
|
||||
@ -271,6 +230,62 @@ impl Config {
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.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")
|
||||
.default_value("Available recipes:\n")
|
||||
.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")
|
||||
.default_value(" ")
|
||||
.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::new(arg::QUIET)
|
||||
.short('q')
|
||||
@ -310,15 +325,24 @@ impl Config {
|
||||
.help("Invoke <COMMAND> with the shell used to run recipe lines and backticks"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(arg::CLEAR_SHELL_ARGS)
|
||||
.long("clear-shell-args")
|
||||
Arg::new(arg::TIMESTAMP)
|
||||
.action(ArgAction::SetTrue)
|
||||
.overrides_with(arg::SHELL_ARG)
|
||||
.help("Clear shell arguments"),
|
||||
.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"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(arg::UNSORTED)
|
||||
.long("unsorted")
|
||||
.env("JUST_UNSORTED")
|
||||
.short('u')
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Return list and summary entries in source order"),
|
||||
@ -349,13 +373,28 @@ impl Config {
|
||||
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
|
||||
.requires(arg::JUSTFILE),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(arg::YES)
|
||||
.long("yes")
|
||||
.env("JUST_YES")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Automatically confirm all recipes."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::CHANGELOG)
|
||||
.long("changelog")
|
||||
.action(ArgAction::SetTrue)
|
||||
.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::new(cmd::COMMAND)
|
||||
.long("command")
|
||||
@ -372,10 +411,9 @@ impl Config {
|
||||
.arg(
|
||||
Arg::new(cmd::COMPLETIONS)
|
||||
.long("completions")
|
||||
.action(ArgAction::Append)
|
||||
.num_args(1..)
|
||||
.action(ArgAction::Set)
|
||||
.value_name("SHELL")
|
||||
.value_parser(value_parser!(clap_complete::Shell))
|
||||
.value_parser(value_parser!(completions::Shell))
|
||||
.ignore_case(true)
|
||||
.help("Print shell completion script for <SHELL>"),
|
||||
)
|
||||
@ -395,6 +433,7 @@ impl Config {
|
||||
.arg(
|
||||
Arg::new(cmd::EVALUATE)
|
||||
.long("evaluate")
|
||||
.alias("eval")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help(
|
||||
"Evaluate and print all variables. If a variable name is given as an argument, only \
|
||||
@ -408,6 +447,12 @@ impl Config {
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Format and overwrite justfile"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::GROUPS)
|
||||
.long("groups")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("List recipe groups"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::INIT)
|
||||
.long("init")
|
||||
@ -425,12 +470,6 @@ impl Config {
|
||||
.conflicts_with(arg::ARGUMENTS)
|
||||
.help("List available recipes"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::GROUPS)
|
||||
.long("groups")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("List recipe groups")
|
||||
)
|
||||
.arg(
|
||||
Arg::new(cmd::MAN)
|
||||
.long("man")
|
||||
@ -459,21 +498,6 @@ impl Config {
|
||||
.action(ArgAction::SetTrue)
|
||||
.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))
|
||||
.arg(
|
||||
Arg::new(arg::ARGUMENTS)
|
||||
@ -481,94 +505,22 @@ impl Config {
|
||||
.action(ArgAction::Append)
|
||||
.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> {
|
||||
let value = matches
|
||||
.get_one::<String>(arg::COLOR)
|
||||
.ok_or_else(|| ConfigError::Internal {
|
||||
message: "`--color` had no value".to_string(),
|
||||
})?;
|
||||
fn parse_module_path(values: ValuesRef<String>) -> ConfigResult<ModulePath> {
|
||||
let path = values.clone().map(|s| (*s).as_str()).collect::<Vec<&str>>();
|
||||
|
||||
match value.as_str() {
|
||||
arg::COLOR_AUTO => Ok(Color::auto()),
|
||||
arg::COLOR_ALWAYS => Ok(Color::always()),
|
||||
arg::COLOR_NEVER => Ok(Color::never()),
|
||||
_ => Err(ConfigError::Internal {
|
||||
message: format!("Invalid argument `{value}` to --color."),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult<Option<ansi_term::Color>> {
|
||||
if let Some(value) = matches.get_one::<String>(arg::COMMAND_COLOR) {
|
||||
match value.as_str() {
|
||||
arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)),
|
||||
arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)),
|
||||
arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)),
|
||||
arg::COMMAND_COLOR_GREEN => Ok(Some(ansi_term::Color::Green)),
|
||||
arg::COMMAND_COLOR_PURPLE => Ok(Some(ansi_term::Color::Purple)),
|
||||
arg::COMMAND_COLOR_RED => Ok(Some(ansi_term::Color::Red)),
|
||||
arg::COMMAND_COLOR_YELLOW => Ok(Some(ansi_term::Color::Yellow)),
|
||||
value => Err(ConfigError::Internal {
|
||||
message: format!("Invalid argument `{value}` to --command-color."),
|
||||
}),
|
||||
}
|
||||
let path = if path.len() == 1 && path[0].contains(' ') {
|
||||
path[0].split_whitespace().collect::<Vec<&str>>()
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
path
|
||||
};
|
||||
|
||||
fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult<DumpFormat> {
|
||||
let value =
|
||||
matches
|
||||
.get_one::<String>(arg::DUMP_FORMAT)
|
||||
.ok_or_else(|| ConfigError::Internal {
|
||||
message: "`--dump-format` had no value".to_string(),
|
||||
})?;
|
||||
|
||||
match value.as_str() {
|
||||
arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json),
|
||||
arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just),
|
||||
_ => Err(ConfigError::Internal {
|
||||
message: format!("Invalid argument `{value}` to --dump-format."),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_module_path(path: ValuesRef<String>) -> ConfigResult<ModulePath> {
|
||||
path
|
||||
.clone()
|
||||
.map(|s| (*s).as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|()| ConfigError::ModulePath {
|
||||
path: path.cloned().collect(),
|
||||
path: values.cloned().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -606,17 +558,6 @@ impl Config {
|
||||
}
|
||||
|
||||
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();
|
||||
if let Some(mut values) = matches.get_many::<String>(arg::SET) {
|
||||
while let (Some(k), Some(v)) = (values.next(), values.next()) {
|
||||
@ -677,30 +618,12 @@ impl Config {
|
||||
arguments,
|
||||
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 }
|
||||
} 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) {
|
||||
Subcommand::Dump
|
||||
} else if matches.get_flag(cmd::FORMAT) {
|
||||
Subcommand::Format
|
||||
} 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::EDIT) {
|
||||
Subcommand::Edit
|
||||
} else if matches.get_flag(cmd::EVALUATE) {
|
||||
if positional.arguments.len() > 1 {
|
||||
return Err(ConfigError::SubcommandArguments {
|
||||
@ -717,6 +640,24 @@ impl Config {
|
||||
variable: positional.arguments.into_iter().next(),
|
||||
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) {
|
||||
Subcommand::Variables
|
||||
} else {
|
||||
@ -726,40 +667,41 @@ 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);
|
||||
let unstable = matches.get_flag(arg::UNSTABLE) || subcommand == Subcommand::Summary;
|
||||
|
||||
Ok(Self {
|
||||
check: matches.get_flag(arg::CHECK),
|
||||
color,
|
||||
command_color,
|
||||
color: (*matches.get_one::<UseColor>(arg::COLOR).unwrap()).into(),
|
||||
command_color: matches
|
||||
.get_one::<CommandColor>(arg::COMMAND_COLOR)
|
||||
.copied()
|
||||
.map(CommandColor::into),
|
||||
dotenv_filename: matches
|
||||
.get_one::<String>(arg::DOTENV_FILENAME)
|
||||
.map(Into::into),
|
||||
dotenv_path: matches.get_one::<PathBuf>(arg::DOTENV_PATH).map(Into::into),
|
||||
dry_run: matches.get_flag(arg::DRY_RUN),
|
||||
dump_format: Self::dump_format_from_matches(matches)?,
|
||||
dump_format: matches
|
||||
.get_one::<DumpFormat>(arg::DUMP_FORMAT)
|
||||
.unwrap()
|
||||
.clone(),
|
||||
highlight: !matches.get_flag(arg::NO_HIGHLIGHT),
|
||||
invocation_directory,
|
||||
list_heading: matches
|
||||
.get_one::<String>(arg::LIST_HEADING)
|
||||
.map_or_else(|| "Available recipes:\n".into(), Into::into),
|
||||
list_prefix: matches
|
||||
.get_one::<String>(arg::LIST_PREFIX)
|
||||
.map_or_else(|| " ".into(), Into::into),
|
||||
invocation_directory: env::current_dir().context(config_error::CurrentDirContext)?,
|
||||
list_heading: matches.get_one::<String>(arg::LIST_HEADING).unwrap().into(),
|
||||
list_prefix: matches.get_one::<String>(arg::LIST_PREFIX).unwrap().into(),
|
||||
list_submodules: matches.get_flag(arg::LIST_SUBMODULES),
|
||||
load_dotenv: !matches.get_flag(arg::NO_DOTENV),
|
||||
no_aliases: matches.get_flag(arg::NO_ALIASES),
|
||||
no_dependencies: matches.get_flag(arg::NO_DEPS),
|
||||
search_config,
|
||||
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),
|
||||
subcommand,
|
||||
timestamp: matches.get_flag(arg::TIMESTAMP),
|
||||
@ -769,22 +711,28 @@ impl Config {
|
||||
.into(),
|
||||
unsorted: matches.get_flag(arg::UNSORTED),
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> {
|
||||
if self.unstable {
|
||||
pub(crate) fn require_unstable(
|
||||
&self,
|
||||
justfile: &Justfile,
|
||||
unstable_feature: UnstableFeature,
|
||||
) -> RunResult<'static> {
|
||||
if self.unstable || justfile.settings.unstable {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Unstable {
|
||||
message: message.to_owned(),
|
||||
})
|
||||
Err(Error::UnstableFeature { unstable_feature })
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
warn!("Failed to set CTRL-C handler: {error}");
|
||||
}
|
||||
@ -815,6 +763,7 @@ mod tests {
|
||||
$(shell_args: $shell_args:expr,)?
|
||||
$(subcommand: $subcommand:expr,)?
|
||||
$(unsorted: $unsorted:expr,)?
|
||||
$(unstable: $unstable:expr,)?
|
||||
$(verbosity: $verbosity:expr,)?
|
||||
} => {
|
||||
#[test]
|
||||
@ -835,6 +784,7 @@ mod tests {
|
||||
$(shell_args: $shell_args,)?
|
||||
$(subcommand: $subcommand,)?
|
||||
$(unsorted: $unsorted,)?
|
||||
$(unstable: $unstable,)?
|
||||
$(verbosity: $verbosity,)?
|
||||
..testing::config(&[])
|
||||
};
|
||||
@ -1245,13 +1195,13 @@ mod tests {
|
||||
test! {
|
||||
name: subcommand_completions,
|
||||
args: ["--completions", "bash"],
|
||||
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash },
|
||||
subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
|
||||
}
|
||||
|
||||
test! {
|
||||
name: subcommand_completions_uppercase,
|
||||
args: ["--completions", "BASH"],
|
||||
subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash },
|
||||
subcommand: Subcommand::Completions{ shell: completions::Shell::Bash },
|
||||
}
|
||||
|
||||
error! {
|
||||
@ -1349,6 +1299,7 @@ mod tests {
|
||||
name: subcommand_summary,
|
||||
args: ["--summary"],
|
||||
subcommand: Subcommand::Summary,
|
||||
unstable: true,
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -1534,15 +1485,30 @@ mod tests {
|
||||
}
|
||||
|
||||
error_matches! {
|
||||
name: completions_arguments,
|
||||
args: ["--completions", "zsh", "foo"],
|
||||
name: completions_argument,
|
||||
args: ["--completions", "foo"],
|
||||
error: error,
|
||||
check: {
|
||||
assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue);
|
||||
assert_eq!(error.context().collect::<Vec<_>>(), vec![
|
||||
(ContextKind::InvalidArg, &ContextValue::String("--completions <SHELL>...".into())),
|
||||
(ContextKind::InvalidValue, &ContextValue::String("foo".into())),
|
||||
(ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())),
|
||||
(
|
||||
ContextKind::InvalidArg,
|
||||
&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()
|
||||
),
|
||||
),
|
||||
]);
|
||||
},
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ pub(crate) struct 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() {
|
||||
write!(f, "{}", self.recipe.name())
|
||||
} else {
|
||||
|
@ -1,4 +1,6 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, ValueEnum)]
|
||||
pub(crate) enum DumpFormat {
|
||||
Json,
|
||||
Just,
|
||||
|
42
src/error.rs
42
src/error.rs
@ -4,7 +4,7 @@ use super::*;
|
||||
pub(crate) enum Error<'src> {
|
||||
AmbiguousModuleFile {
|
||||
module: Name<'src>,
|
||||
found: Vec<String>,
|
||||
found: Vec<PathBuf>,
|
||||
},
|
||||
ArgumentCountMismatch {
|
||||
recipe: &'src str,
|
||||
@ -20,7 +20,7 @@ pub(crate) enum Error<'src> {
|
||||
token: Token<'src>,
|
||||
output_error: OutputError,
|
||||
},
|
||||
CacheDirIo {
|
||||
RuntimeDirIo {
|
||||
io_error: io::Error,
|
||||
path: PathBuf,
|
||||
},
|
||||
@ -79,6 +79,7 @@ pub(crate) enum Error<'src> {
|
||||
Dotenv {
|
||||
dotenv_error: dotenvy::Error,
|
||||
},
|
||||
DotenvRequired,
|
||||
DumpJson {
|
||||
serde_json_error: serde_json::Error,
|
||||
},
|
||||
@ -94,6 +95,9 @@ pub(crate) enum Error<'src> {
|
||||
variable: String,
|
||||
suggestion: Option<Suggestion<'src>>,
|
||||
},
|
||||
ExpectedSubmoduleButFoundRecipe {
|
||||
path: String,
|
||||
},
|
||||
FormatCheckFoundDiff,
|
||||
FunctionCall {
|
||||
function: Name<'src>,
|
||||
@ -161,17 +165,17 @@ pub(crate) enum Error<'src> {
|
||||
line_number: Option<usize>,
|
||||
},
|
||||
UnknownSubmodule {
|
||||
path: ModulePath,
|
||||
path: String,
|
||||
},
|
||||
UnknownOverrides {
|
||||
overrides: Vec<String>,
|
||||
},
|
||||
UnknownRecipes {
|
||||
recipes: Vec<String>,
|
||||
UnknownRecipe {
|
||||
recipe: String,
|
||||
suggestion: Option<Suggestion<'src>>,
|
||||
},
|
||||
Unstable {
|
||||
message: String,
|
||||
UnstableFeature {
|
||||
unstable_feature: UnstableFeature,
|
||||
},
|
||||
WriteJustfile {
|
||||
justfile: PathBuf,
|
||||
@ -258,7 +262,7 @@ impl<'src> ColorDisplay for Error<'src> {
|
||||
AmbiguousModuleFile { module, found } =>
|
||||
write!(f,
|
||||
"Found multiple source files for module `{module}`: {}",
|
||||
List::and_ticked(found),
|
||||
List::and_ticked(found.iter().map(|path| path.display())),
|
||||
)?,
|
||||
ArgumentCountMismatch { recipe, found, min, max, .. } => {
|
||||
let count = Count("argument", *found);
|
||||
@ -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}")?,
|
||||
}
|
||||
CacheDirIo { io_error, path } => {
|
||||
write!(f, "I/O error in cache dir `{}`: {io_error}", path.display())?;
|
||||
}
|
||||
ChooserInvoke { shell_binary, shell_arguments, chooser, io_error} => {
|
||||
let chooser = chooser.to_string_lossy();
|
||||
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 } => {
|
||||
write!(f, "Failed to load environment file: {dotenv_error}")?;
|
||||
}
|
||||
DotenvRequired => {
|
||||
write!(f, "Dotenv file not found")?;
|
||||
}
|
||||
DumpJson { 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}")?;
|
||||
}
|
||||
}
|
||||
ExpectedSubmoduleButFoundRecipe { path } => {
|
||||
write!(f, "Expected submodule at `{path}` but found recipe.")?;
|
||||
},
|
||||
FormatCheckFoundDiff => {
|
||||
write!(f, "Formatted justfile differs from original.")?;
|
||||
}
|
||||
@ -403,6 +410,9 @@ impl<'src> ColorDisplay for Error<'src> {
|
||||
write!(f, "Recipe `{recipe}` was not confirmed")?;
|
||||
}
|
||||
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)?,
|
||||
Shebang { recipe, command, argument, io_error} => {
|
||||
if let Some(argument) = argument {
|
||||
@ -443,16 +453,14 @@ impl<'src> ColorDisplay for Error<'src> {
|
||||
let overrides = List::and_ticked(overrides);
|
||||
write!(f, "{count} {overrides} overridden on the command line but not present in justfile")?;
|
||||
}
|
||||
UnknownRecipes { recipes, suggestion } => {
|
||||
let count = Count("recipe", recipes.len());
|
||||
let recipes = List::or_ticked(recipes);
|
||||
write!(f, "Justfile does not contain {count} {recipes}.")?;
|
||||
UnknownRecipe { recipe, suggestion } => {
|
||||
write!(f, "Justfile does not contain recipe `{recipe}`.")?;
|
||||
if let Some(suggestion) = suggestion {
|
||||
write!(f, "\n{suggestion}")?;
|
||||
}
|
||||
}
|
||||
Unstable { message } => {
|
||||
write!(f, "{message} Invoke `just` with the `--unstable` flag to enable unstable features.")?;
|
||||
UnstableFeature { unstable_feature } => {
|
||||
write!(f, "{unstable_feature} Invoke `just` with `--unstable`, set the `JUST_UNSTABLE` environment variable, or add `set unstable` to your `justfile` to enable unstable features.")?;
|
||||
}
|
||||
WriteJustfile { justfile, io_error } => {
|
||||
let justfile = justfile.display();
|
||||
|
114
src/evaluator.rs
114
src/evaluator.rs
@ -2,35 +2,58 @@ use super::*;
|
||||
|
||||
pub(crate) struct Evaluator<'src: 'run, 'run> {
|
||||
pub(crate) assignments: Option<&'run Table<'src, Assignment<'src>>>,
|
||||
pub(crate) config: &'run Config,
|
||||
pub(crate) dotenv: &'run BTreeMap<String, String>,
|
||||
pub(crate) module_source: &'run Path,
|
||||
pub(crate) context: ExecutionContext<'src, 'run>,
|
||||
pub(crate) is_dependency: bool,
|
||||
pub(crate) scope: Scope<'src, 'run>,
|
||||
pub(crate) search: &'run Search,
|
||||
pub(crate) settings: &'run Settings<'run>,
|
||||
}
|
||||
|
||||
impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
pub(crate) fn evaluate_assignments(
|
||||
assignments: &'run Table<'src, Assignment<'src>>,
|
||||
config: &'run Config,
|
||||
dotenv: &'run BTreeMap<String, String>,
|
||||
module_source: &'run Path,
|
||||
scope: Scope<'src, 'run>,
|
||||
module: &'run Justfile<'src>,
|
||||
overrides: &BTreeMap<String, String>,
|
||||
parent: &'run Scope<'src, 'run>,
|
||||
search: &'run Search,
|
||||
settings: &'run Settings<'run>,
|
||||
) -> RunResult<'src, Scope<'src, 'run>> {
|
||||
let mut evaluator = Self {
|
||||
assignments: Some(assignments),
|
||||
) -> RunResult<'src, Scope<'src, 'run>>
|
||||
where
|
||||
'src: 'run,
|
||||
{
|
||||
let context = ExecutionContext {
|
||||
config,
|
||||
dotenv,
|
||||
module_source,
|
||||
scope,
|
||||
module_source: &module.source,
|
||||
scope: parent,
|
||||
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)?;
|
||||
}
|
||||
|
||||
@ -152,7 +175,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
}
|
||||
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.clone()),
|
||||
Expression::Backtick { contents, token } => {
|
||||
if self.config.dry_run {
|
||||
if self.context.config.dry_run {
|
||||
Ok(format!("`{contents}`"))
|
||||
} else {
|
||||
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> {
|
||||
let mut cmd = self.settings.shell_command(self.config);
|
||||
let mut cmd = self.context.settings.shell_command(self.context.config);
|
||||
cmd.arg(command);
|
||||
cmd.args(args);
|
||||
cmd.current_dir(&self.search.working_directory);
|
||||
cmd.export(self.settings, self.dotenv, &self.scope);
|
||||
cmd.current_dir(&self.context.search.working_directory);
|
||||
cmd.export(
|
||||
self.context.settings,
|
||||
self.context.dotenv,
|
||||
&self.scope,
|
||||
self.context.unexports,
|
||||
);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stderr(if self.config.verbosity.quiet() {
|
||||
cmd.stderr(if self.context.config.verbosity.quiet() {
|
||||
Stdio::null()
|
||||
} else {
|
||||
Stdio::inherit()
|
||||
@ -253,26 +281,12 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
}
|
||||
|
||||
pub(crate) fn evaluate_parameters(
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
is_dependency: bool,
|
||||
arguments: &[String],
|
||||
config: &'run Config,
|
||||
dotenv: &'run BTreeMap<String, String>,
|
||||
module_source: &'run Path,
|
||||
parameters: &[Parameter<'src>],
|
||||
scope: &'run Scope<'src, 'run>,
|
||||
search: &'run Search,
|
||||
settings: &'run Settings,
|
||||
) -> RunResult<'src, (Scope<'src, 'run>, Vec<String>)> {
|
||||
let mut evaluator = Self {
|
||||
assignments: None,
|
||||
config,
|
||||
dotenv,
|
||||
module_source,
|
||||
scope: scope.child(),
|
||||
search,
|
||||
settings,
|
||||
};
|
||||
|
||||
let mut scope = scope.child();
|
||||
let mut evaluator = Self::new(context, is_dependency, context.scope);
|
||||
|
||||
let mut positional = Vec::new();
|
||||
|
||||
@ -303,28 +317,24 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
||||
rest = &rest[1..];
|
||||
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(
|
||||
config: &'run Config,
|
||||
dotenv: &'run BTreeMap<String, String>,
|
||||
module_source: &'run Path,
|
||||
pub(crate) fn new(
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
is_dependency: bool,
|
||||
scope: &'run Scope<'src, 'run>,
|
||||
search: &'run Search,
|
||||
settings: &'run Settings,
|
||||
) -> Self {
|
||||
Self {
|
||||
assignments: None,
|
||||
config,
|
||||
dotenv,
|
||||
module_source,
|
||||
scope: Scope::child(scope),
|
||||
search,
|
||||
settings,
|
||||
context: *context,
|
||||
is_dependency,
|
||||
scope: scope.child(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
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) dotenv: &'run BTreeMap<String, String>,
|
||||
pub(crate) module_source: &'run Path,
|
||||
pub(crate) scope: &'run Scope<'src, 'run>,
|
||||
pub(crate) search: &'run Search,
|
||||
pub(crate) settings: &'run Settings<'src>,
|
||||
pub(crate) unexports: &'run HashSet<String>,
|
||||
}
|
@ -51,7 +51,7 @@ impl<'src> 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 {
|
||||
Self::Assert { condition, error } => write!(f, "assert({condition}, {error})"),
|
||||
Self::Backtick { token, .. } => write!(f, "{}", token.lexeme()),
|
||||
|
253
src/function.rs
253
src/function.rs
@ -11,13 +11,13 @@ use {
|
||||
};
|
||||
|
||||
pub(crate) enum Function {
|
||||
Nullary(fn(Context) -> Result<String, String>),
|
||||
Unary(fn(Context, &str) -> Result<String, String>),
|
||||
UnaryOpt(fn(Context, &str, Option<&str>) -> Result<String, String>),
|
||||
UnaryPlus(fn(Context, &str, &[String]) -> Result<String, String>),
|
||||
Binary(fn(Context, &str, &str) -> Result<String, String>),
|
||||
BinaryPlus(fn(Context, &str, &str, &[String]) -> Result<String, String>),
|
||||
Ternary(fn(Context, &str, &str, &str) -> Result<String, String>),
|
||||
Nullary(fn(Context) -> FunctionResult),
|
||||
Unary(fn(Context, &str) -> FunctionResult),
|
||||
UnaryOpt(fn(Context, &str, Option<&str>) -> FunctionResult),
|
||||
UnaryPlus(fn(Context, &str, &[String]) -> FunctionResult),
|
||||
Binary(fn(Context, &str, &str) -> FunctionResult),
|
||||
BinaryPlus(fn(Context, &str, &str, &[String]) -> FunctionResult),
|
||||
Ternary(fn(Context, &str, &str, &str) -> FunctionResult),
|
||||
}
|
||||
|
||||
pub(crate) struct Context<'src: 'run, 'run> {
|
||||
@ -32,7 +32,15 @@ impl<'src: 'run, 'run> Context<'src, 'run> {
|
||||
}
|
||||
|
||||
pub(crate) fn get(name: &str) -> Option<Function> {
|
||||
let function = match name {
|
||||
let name = if let Some(prefix) = name.strip_suffix("_dir") {
|
||||
format!("{prefix}_directory")
|
||||
} else if let Some(prefix) = name.strip_suffix("_dir_native") {
|
||||
format!("{prefix}_directory_native")
|
||||
} else {
|
||||
name.into()
|
||||
};
|
||||
|
||||
let function = match name.as_str() {
|
||||
"absolute_path" => Unary(absolute_path),
|
||||
"append" => Binary(append),
|
||||
"arch" => Nullary(arch),
|
||||
@ -47,6 +55,8 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
||||
"config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)),
|
||||
"data_directory" => Nullary(|_| dir("data", dirs::data_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),
|
||||
"env" => UnaryOpt(env),
|
||||
"env_var" => Unary(env_var),
|
||||
@ -59,6 +69,7 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
||||
"home_directory" => Nullary(|_| dir("home", dirs::home_dir)),
|
||||
"invocation_directory" => Nullary(invocation_directory),
|
||||
"invocation_directory_native" => Nullary(invocation_directory_native),
|
||||
"is_dependency" => Nullary(is_dependency),
|
||||
"join" => BinaryPlus(join),
|
||||
"just_executable" => Nullary(just_executable),
|
||||
"just_pid" => Nullary(just_pid),
|
||||
@ -105,22 +116,23 @@ pub(crate) fn get(name: &str) -> Option<Function> {
|
||||
}
|
||||
|
||||
impl Function {
|
||||
pub(crate) fn argc(&self) -> Range<usize> {
|
||||
pub(crate) fn argc(&self) -> RangeInclusive<usize> {
|
||||
match *self {
|
||||
Nullary(_) => 0..0,
|
||||
Unary(_) => 1..1,
|
||||
UnaryOpt(_) => 1..2,
|
||||
UnaryPlus(_) => 1..usize::MAX,
|
||||
Binary(_) => 2..2,
|
||||
BinaryPlus(_) => 2..usize::MAX,
|
||||
Ternary(_) => 3..3,
|
||||
Nullary(_) => 0..=0,
|
||||
Unary(_) => 1..=1,
|
||||
UnaryOpt(_) => 1..=2,
|
||||
UnaryPlus(_) => 1..=usize::MAX,
|
||||
Binary(_) => 2..=2,
|
||||
BinaryPlus(_) => 2..=usize::MAX,
|
||||
Ternary(_) => 3..=3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_path(context: Context, path: &str) -> Result<String, String> {
|
||||
fn absolute_path(context: Context, path: &str) -> FunctionResult {
|
||||
let abs_path_unchecked = context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.working_directory
|
||||
.join(path)
|
||||
@ -129,12 +141,12 @@ fn absolute_path(context: Context, path: &str) -> Result<String, String> {
|
||||
Some(absolute_path) => Ok(absolute_path.to_owned()),
|
||||
None => Err(format!(
|
||||
"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(
|
||||
s.split_whitespace()
|
||||
.map(|s| format!("{s}{suffix}"))
|
||||
@ -143,16 +155,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())
|
||||
}
|
||||
|
||||
fn blake3(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn blake3(_context: Context, s: &str) -> FunctionResult {
|
||||
Ok(blake3::hash(s.as_bytes()).to_string())
|
||||
}
|
||||
|
||||
fn blake3_file(context: Context, path: &str) -> Result<String, String> {
|
||||
let path = context.evaluator.search.working_directory.join(path);
|
||||
fn blake3_file(context: Context, path: &str) -> FunctionResult {
|
||||
let path = context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.working_directory
|
||||
.join(path);
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher
|
||||
.update_mmap_rayon(&path)
|
||||
@ -160,7 +177,7 @@ fn blake3_file(context: Context, path: &str) -> Result<String, String> {
|
||||
Ok(hasher.finalize().to_string())
|
||||
}
|
||||
|
||||
fn canonicalize(_context: Context, path: &str) -> Result<String, String> {
|
||||
fn canonicalize(_context: Context, path: &str) -> FunctionResult {
|
||||
let canonical =
|
||||
std::fs::canonicalize(path).map_err(|err| format!("I/O error canonicalizing path: {err}"))?;
|
||||
|
||||
@ -172,7 +189,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();
|
||||
for (i, c) in s.chars().enumerate() {
|
||||
if i == 0 {
|
||||
@ -184,7 +201,7 @@ fn capitalize(_context: Context, s: &str) -> Result<String, String> {
|
||||
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() {
|
||||
return Err("empty alphabet".into());
|
||||
}
|
||||
@ -208,11 +225,11 @@ fn choose(_context: Context, n: &str, alphabet: &str) -> Result<String, String>
|
||||
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())
|
||||
}
|
||||
|
||||
fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> Result<String, String> {
|
||||
fn dir(name: &'static str, f: fn() -> Option<PathBuf>) -> FunctionResult {
|
||||
match f() {
|
||||
Some(path) => path
|
||||
.as_os_str()
|
||||
@ -228,7 +245,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
|
||||
.remove(b'-')
|
||||
.remove(b'_')
|
||||
@ -242,10 +267,10 @@ fn encode_uri_component(_context: Context, s: &str) -> Result<String, 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::*;
|
||||
|
||||
if let Some(value) = context.evaluator.dotenv.get(key) {
|
||||
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
|
||||
@ -258,10 +283,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::*;
|
||||
|
||||
if let Some(value) = context.evaluator.dotenv.get(key) {
|
||||
if let Some(value) = context.evaluator.context.dotenv.get(key) {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
|
||||
@ -274,49 +299,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 {
|
||||
Some(val) => env_var_or_default(context, key, val),
|
||||
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())
|
||||
}
|
||||
|
||||
fn extension(_context: Context, path: &str) -> Result<String, String> {
|
||||
fn extension(_context: Context, path: &str) -> FunctionResult {
|
||||
Utf8Path::new(path)
|
||||
.extension()
|
||||
.map(str::to_owned)
|
||||
.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)
|
||||
.file_name()
|
||||
.map(str::to_owned)
|
||||
.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)
|
||||
.file_stem()
|
||||
.map(str::to_owned)
|
||||
.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(
|
||||
&context.evaluator.search.working_directory,
|
||||
&context.evaluator.config.invocation_directory,
|
||||
&context.evaluator.context.search.working_directory,
|
||||
&context.evaluator.context.config.invocation_directory,
|
||||
)
|
||||
.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
|
||||
.evaluator
|
||||
.context
|
||||
.config
|
||||
.invocation_directory
|
||||
.to_str()
|
||||
@ -324,12 +350,21 @@ fn invocation_directory_native(context: Context) -> Result<String, String> {
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"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(
|
||||
s.split_whitespace()
|
||||
.map(|s| format!("{prefix}{s}"))
|
||||
@ -338,7 +373,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);
|
||||
for arg in and {
|
||||
result.push(arg);
|
||||
@ -346,7 +381,7 @@ fn join(_context: Context, base: &str, with: &str, and: &[String]) -> Result<Str
|
||||
Ok(result.to_string())
|
||||
}
|
||||
|
||||
fn just_executable(_context: Context) -> Result<String, String> {
|
||||
fn just_executable(_context: Context) -> FunctionResult {
|
||||
let exe_path =
|
||||
env::current_exe().map_err(|e| format!("Error getting current executable: {e}"))?;
|
||||
|
||||
@ -358,13 +393,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())
|
||||
}
|
||||
|
||||
fn justfile(context: Context) -> Result<String, String> {
|
||||
fn justfile(context: Context) -> FunctionResult {
|
||||
context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.to_str()
|
||||
@ -372,18 +408,24 @@ fn justfile(context: Context) -> Result<String, String> {
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"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> {
|
||||
let justfile_directory = context.evaluator.search.justfile.parent().ok_or_else(|| {
|
||||
format!(
|
||||
"Could not resolve justfile directory. Justfile `{}` had no parent.",
|
||||
context.evaluator.search.justfile.display()
|
||||
)
|
||||
})?;
|
||||
fn justfile_directory(context: Context) -> FunctionResult {
|
||||
let justfile_directory = context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Could not resolve justfile directory. Justfile `{}` had no parent.",
|
||||
context.evaluator.context.search.justfile.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
justfile_directory
|
||||
.to_str()
|
||||
@ -396,26 +438,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())
|
||||
}
|
||||
|
||||
fn lowercamelcase(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn lowercamelcase(_context: Context, s: &str) -> FunctionResult {
|
||||
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())
|
||||
}
|
||||
|
||||
fn module_directory(context: Context) -> Result<String, String> {
|
||||
fn module_directory(context: Context) -> FunctionResult {
|
||||
context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(context.evaluator.module_source)
|
||||
.join(context.evaluator.context.module_source)
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
@ -423,53 +466,61 @@ fn module_directory(context: Context) -> Result<String, String> {
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"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
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(context.evaluator.module_source)
|
||||
.join(context.evaluator.context.module_source)
|
||||
.to_str()
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"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();
|
||||
Ok(num.to_string())
|
||||
}
|
||||
|
||||
fn os(_context: Context) -> Result<String, String> {
|
||||
fn os(_context: Context) -> FunctionResult {
|
||||
Ok(target::os().to_owned())
|
||||
}
|
||||
|
||||
fn os_family(_context: Context) -> Result<String, String> {
|
||||
fn os_family(_context: Context) -> FunctionResult {
|
||||
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)
|
||||
.parent()
|
||||
.map(Utf8Path::to_string)
|
||||
.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(
|
||||
context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.working_directory
|
||||
.join(path)
|
||||
@ -478,20 +529,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('\'', "'\\''")))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
fn replace_regex(
|
||||
_context: Context,
|
||||
s: &str,
|
||||
regex: &str,
|
||||
replacement: &str,
|
||||
) -> Result<String, String> {
|
||||
fn replace_regex(_context: Context, s: &str, regex: &str, replacement: &str) -> FunctionResult {
|
||||
Ok(
|
||||
Regex::new(regex)
|
||||
.map_err(|err| err.to_string())?
|
||||
@ -500,7 +546,7 @@ fn replace_regex(
|
||||
)
|
||||
}
|
||||
|
||||
fn sha256(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn sha256(_context: Context, s: &str) -> FunctionResult {
|
||||
use sha2::{Digest, Sha256};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(s);
|
||||
@ -508,9 +554,14 @@ fn sha256(_context: Context, s: &str) -> Result<String, String> {
|
||||
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};
|
||||
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 file =
|
||||
fs::File::open(&path).map_err(|err| format!("Failed to open `{}`: {err}", path.display()))?;
|
||||
@ -520,7 +571,7 @@ fn sha256_file(context: Context, path: &str) -> Result<String, String> {
|
||||
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)
|
||||
.chain(args.iter().map(String::as_str))
|
||||
.collect::<Vec<&str>>();
|
||||
@ -531,21 +582,22 @@ fn shell(context: Context, command: &str, args: &[String]) -> Result<String, Str
|
||||
.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())
|
||||
}
|
||||
|
||||
fn shoutysnakecase(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn shoutysnakecase(_context: Context, s: &str) -> FunctionResult {
|
||||
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())
|
||||
}
|
||||
|
||||
fn source_directory(context: Context) -> Result<String, String> {
|
||||
fn source_directory(context: Context) -> FunctionResult {
|
||||
context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.parent()
|
||||
@ -563,9 +615,10 @@ fn source_directory(context: Context) -> Result<String, String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn source_file(context: Context) -> Result<String, String> {
|
||||
fn source_file(context: Context) -> FunctionResult {
|
||||
context
|
||||
.evaluator
|
||||
.context
|
||||
.search
|
||||
.justfile
|
||||
.parent()
|
||||
@ -581,51 +634,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())
|
||||
}
|
||||
|
||||
fn trim(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn trim(_context: Context, s: &str) -> FunctionResult {
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn trim_start(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn trim_start(_context: Context, s: &str) -> FunctionResult {
|
||||
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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn uppercamelcase(_context: Context, s: &str) -> Result<String, String> {
|
||||
fn uppercamelcase(_context: Context, s: &str) -> FunctionResult {
|
||||
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())
|
||||
}
|
||||
|
||||
fn uuid(_context: Context) -> Result<String, String> {
|
||||
fn uuid(_context: Context) -> FunctionResult {
|
||||
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)
|
||||
.parent()
|
||||
.ok_or_else(|| format!("Could not extract parent from `{path}`"))?;
|
||||
@ -639,7 +692,7 @@ fn without_extension(_context: Context, path: &str) -> Result<String, String> {
|
||||
|
||||
/// Check whether a string processes properly as semver (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(
|
||||
requirement
|
||||
.parse::<VersionReq>()
|
||||
|
@ -13,13 +13,18 @@ pub(crate) enum Item<'src> {
|
||||
relative: StringLiteral<'src>,
|
||||
},
|
||||
Module {
|
||||
attributes: BTreeSet<Attribute<'src>>,
|
||||
absolute: Option<PathBuf>,
|
||||
doc: Option<&'src str>,
|
||||
name: Name<'src>,
|
||||
optional: bool,
|
||||
relative: Option<StringLiteral<'src>>,
|
||||
},
|
||||
Recipe(UnresolvedRecipe<'src>),
|
||||
Set(Set<'src>),
|
||||
Unexport {
|
||||
name: Name<'src>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'src> Display for Item<'src> {
|
||||
@ -61,6 +66,7 @@ impl<'src> Display for Item<'src> {
|
||||
}
|
||||
Self::Recipe(recipe) => write!(f, "{}", recipe.color_display(Color::never())),
|
||||
Self::Set(set) => write!(f, "{set}"),
|
||||
Self::Unexport { name } => write!(f, "unexport {name}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
354
src/justfile.rs
354
src/justfile.rs
@ -13,6 +13,7 @@ struct Invocation<'src: 'run, 'run> {
|
||||
pub(crate) struct Justfile<'src> {
|
||||
pub(crate) aliases: Table<'src, Alias<'src>>,
|
||||
pub(crate) assignments: Table<'src, Assignment<'src>>,
|
||||
pub(crate) doc: Option<String>,
|
||||
#[serde(rename = "first", serialize_with = "keyed::serialize_option")]
|
||||
pub(crate) default: Option<Rc<Recipe<'src>>>,
|
||||
#[serde(skip)]
|
||||
@ -24,6 +25,9 @@ pub(crate) struct Justfile<'src> {
|
||||
pub(crate) settings: Settings<'src>,
|
||||
#[serde(skip)]
|
||||
pub(crate) source: PathBuf,
|
||||
pub(crate) unexports: HashSet<String>,
|
||||
#[serde(skip)]
|
||||
pub(crate) unstable_features: BTreeSet<UnstableFeature>,
|
||||
pub(crate) warnings: Vec<Warning>,
|
||||
}
|
||||
|
||||
@ -77,45 +81,6 @@ impl<'src> Justfile<'src> {
|
||||
.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(
|
||||
&self,
|
||||
config: &Config,
|
||||
@ -143,7 +108,7 @@ impl<'src> Justfile<'src> {
|
||||
|
||||
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 {
|
||||
Subcommand::Command {
|
||||
@ -163,7 +128,7 @@ impl<'src> Justfile<'src> {
|
||||
|
||||
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| {
|
||||
Error::CommandInvoke {
|
||||
@ -194,11 +159,7 @@ impl<'src> Justfile<'src> {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let mut width = 0;
|
||||
|
||||
for name in scope.names() {
|
||||
width = cmp::max(name.len(), width);
|
||||
}
|
||||
let width = scope.names().fold(0, |max, name| name.len().max(max));
|
||||
|
||||
for binding in scope.bindings() {
|
||||
println!(
|
||||
@ -215,77 +176,38 @@ impl<'src> Justfile<'src> {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut remaining: Vec<&str> = if !arguments.is_empty() {
|
||||
arguments.iter().map(String::as_str).collect()
|
||||
} else if let Some(recipe) = &self.default {
|
||||
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 arguments = arguments.iter().map(String::as_str).collect::<Vec<&str>>();
|
||||
|
||||
let groups = ArgumentParser::parse_arguments(self, &arguments)?;
|
||||
|
||||
let mut missing = Vec::new();
|
||||
let mut invocations = Vec::new();
|
||||
let mut scopes = BTreeMap::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() {
|
||||
if first.contains("::")
|
||||
&& !(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(),
|
||||
for group in &groups {
|
||||
invocations.push(self.invocation(
|
||||
&arena,
|
||||
&mut scopes,
|
||||
&group.arguments,
|
||||
config,
|
||||
&dotenv,
|
||||
search,
|
||||
&scope,
|
||||
first,
|
||||
rest,
|
||||
)? {
|
||||
remaining = rest[consumed..].to_vec();
|
||||
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,
|
||||
});
|
||||
&group.path,
|
||||
0,
|
||||
&mut scopes,
|
||||
search,
|
||||
)?);
|
||||
}
|
||||
|
||||
let mut ran = Ran::default();
|
||||
for invocation in invocations {
|
||||
let context = RecipeContext {
|
||||
let context = ExecutionContext {
|
||||
config,
|
||||
dotenv: &dotenv,
|
||||
module_source: invocation.module_source,
|
||||
scope: invocation.scope,
|
||||
search,
|
||||
settings: invocation.settings,
|
||||
unexports: &self.unexports,
|
||||
};
|
||||
|
||||
Self::run_recipe(
|
||||
@ -298,13 +220,25 @@ impl<'src> Justfile<'src> {
|
||||
&context,
|
||||
&mut ran,
|
||||
invocation.recipe,
|
||||
search,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn check_unstable(&self, config: &Config) -> RunResult<'src> {
|
||||
if let Some(&unstable_feature) = self.unstable_features.iter().next() {
|
||||
config.require_unstable(self, unstable_feature)?;
|
||||
}
|
||||
|
||||
for module in self.modules.values() {
|
||||
module.check_unstable(config)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
|
||||
self.aliases.get(name)
|
||||
}
|
||||
@ -319,95 +253,55 @@ impl<'src> Justfile<'src> {
|
||||
|
||||
fn invocation<'run>(
|
||||
&'run self,
|
||||
depth: usize,
|
||||
path: &mut Vec<&'run str>,
|
||||
arena: &'run Arena<Scope<'src, 'run>>,
|
||||
scopes: &mut BTreeMap<Vec<&'run str>, &'run Scope<'src, 'run>>,
|
||||
arguments: &[&'run str],
|
||||
config: &'run Config,
|
||||
dotenv: &'run BTreeMap<String, String>,
|
||||
search: &'run Search,
|
||||
parent: &'run Scope<'src, 'run>,
|
||||
first: &'run str,
|
||||
rest: &[&'run str],
|
||||
) -> RunResult<'src, Option<(Invocation<'src, 'run>, usize)>> {
|
||||
if let Some(module) = self.modules.get(first) {
|
||||
path.push(first);
|
||||
path: &'run [String],
|
||||
position: usize,
|
||||
scopes: &mut BTreeMap<&'run [String], &'run Scope<'src, 'run>>,
|
||||
search: &'run Search,
|
||||
) -> 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
|
||||
} 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);
|
||||
scopes.insert(path.clone(), scope);
|
||||
scopes.insert(path, scope);
|
||||
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(
|
||||
depth + 1,
|
||||
path,
|
||||
arena,
|
||||
scopes,
|
||||
config,
|
||||
dotenv,
|
||||
search,
|
||||
scope,
|
||||
rest[0],
|
||||
&rest[1..],
|
||||
)
|
||||
}
|
||||
} else if let Some(recipe) = self.get_recipe(first) {
|
||||
if recipe.parameters.is_empty() {
|
||||
Ok(Some((
|
||||
Invocation {
|
||||
arguments: Vec::new(),
|
||||
recipe,
|
||||
scope: parent,
|
||||
settings: &self.settings,
|
||||
module_source: &self.source,
|
||||
},
|
||||
depth,
|
||||
)))
|
||||
} else {
|
||||
let argument_range = recipe.argument_range();
|
||||
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
|
||||
if !argument_range.range_contains(&argument_count) {
|
||||
return Err(Error::ArgumentCountMismatch {
|
||||
recipe: recipe.name(),
|
||||
parameters: recipe.parameters.clone(),
|
||||
found: rest.len(),
|
||||
min: recipe.min_arguments(),
|
||||
max: recipe.max_arguments(),
|
||||
});
|
||||
}
|
||||
Ok(Some((
|
||||
Invocation {
|
||||
arguments: rest[..argument_count].to_vec(),
|
||||
recipe,
|
||||
scope: parent,
|
||||
settings: &self.settings,
|
||||
module_source: &self.source,
|
||||
},
|
||||
depth + argument_count,
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
module.invocation(
|
||||
arena,
|
||||
arguments,
|
||||
config,
|
||||
dotenv,
|
||||
scope,
|
||||
path,
|
||||
position + 1,
|
||||
scopes,
|
||||
search,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,10 +311,10 @@ impl<'src> Justfile<'src> {
|
||||
|
||||
fn run_recipe(
|
||||
arguments: &[String],
|
||||
context: &RecipeContext<'src, '_>,
|
||||
context: &ExecutionContext<'src, '_>,
|
||||
ran: &mut Ran<'src>,
|
||||
recipe: &Recipe<'src>,
|
||||
search: &Search,
|
||||
is_dependency: bool,
|
||||
) -> RunResult<'src> {
|
||||
if ran.has_run(&recipe.namepath, arguments) {
|
||||
return Ok(());
|
||||
@ -432,27 +326,12 @@ impl<'src> Justfile<'src> {
|
||||
});
|
||||
}
|
||||
|
||||
let (outer, positional) = Evaluator::evaluate_parameters(
|
||||
arguments,
|
||||
context.config,
|
||||
context.dotenv,
|
||||
context.module_source,
|
||||
&recipe.parameters,
|
||||
context.scope,
|
||||
search,
|
||||
context.settings,
|
||||
)?;
|
||||
let (outer, positional) =
|
||||
Evaluator::evaluate_parameters(context, is_dependency, arguments, &recipe.parameters)?;
|
||||
|
||||
let scope = outer.child();
|
||||
|
||||
let mut evaluator = Evaluator::recipe_evaluator(
|
||||
context.config,
|
||||
context.dotenv,
|
||||
context.module_source,
|
||||
&scope,
|
||||
search,
|
||||
context.settings,
|
||||
);
|
||||
let mut evaluator = Evaluator::new(context, true, &scope);
|
||||
|
||||
if !context.config.no_dependencies {
|
||||
for Dependency { recipe, arguments } in recipe.dependencies.iter().take(recipe.priors) {
|
||||
@ -461,11 +340,11 @@ impl<'src> Justfile<'src> {
|
||||
.map(|argument| evaluator.evaluate_expression(argument))
|
||||
.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 {
|
||||
let mut ran = Ran::default();
|
||||
@ -477,7 +356,7 @@ impl<'src> Justfile<'src> {
|
||||
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 +380,13 @@ impl<'src> Justfile<'src> {
|
||||
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
|
||||
.recipes
|
||||
.values()
|
||||
.map(AsRef::as_ref)
|
||||
.filter(|recipe| recipe.is_public())
|
||||
.collect::<Vec<&Recipe<Dependency>>>();
|
||||
.collect::<Vec<&Recipe>>();
|
||||
|
||||
if config.unsorted {
|
||||
recipes.sort_by_key(|recipe| (&recipe.import_offsets, recipe.name.offset));
|
||||
@ -516,19 +395,33 @@ impl<'src> Justfile<'src> {
|
||||
recipes
|
||||
}
|
||||
|
||||
pub(crate) fn public_groups(&self) -> BTreeSet<String> {
|
||||
self
|
||||
.recipes
|
||||
.values()
|
||||
.map(AsRef::as_ref)
|
||||
.filter(|recipe| recipe.is_public())
|
||||
.flat_map(Recipe::groups)
|
||||
.collect()
|
||||
pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {
|
||||
let mut groups = Vec::new();
|
||||
|
||||
for recipe in self.recipes.values() {
|
||||
if recipe.is_public() {
|
||||
for group in recipe.groups() {
|
||||
groups.push((&recipe.import_offsets, recipe.name.offset, group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
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();
|
||||
for (name, assignment) in &self.assignments {
|
||||
if assignment.export {
|
||||
@ -572,21 +465,38 @@ mod tests {
|
||||
use Error::*;
|
||||
|
||||
run_error! {
|
||||
name: unknown_recipes,
|
||||
name: unknown_recipe_no_suggestion,
|
||||
src: "a:\nb:\nc:",
|
||||
args: ["a", "x", "y", "z"],
|
||||
error: UnknownRecipes {
|
||||
recipes,
|
||||
args: ["a", "xyz", "y", "z"],
|
||||
error: UnknownRecipe {
|
||||
recipe,
|
||||
suggestion,
|
||||
},
|
||||
check: {
|
||||
assert_eq!(recipes, &["x", "y", "z"]);
|
||||
assert_eq!(recipe, "xyz");
|
||||
assert_eq!(suggestion, None);
|
||||
}
|
||||
}
|
||||
|
||||
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: "
|
||||
foo:
|
||||
echo foo
|
||||
@ -594,12 +504,12 @@ mod tests {
|
||||
alias z := foo
|
||||
",
|
||||
args: ["zz"],
|
||||
error: UnknownRecipes {
|
||||
recipes,
|
||||
error: UnknownRecipe {
|
||||
recipe,
|
||||
suggestion,
|
||||
},
|
||||
check: {
|
||||
assert_eq!(recipes, &["zz"]);
|
||||
assert_eq!(recipe, "zz");
|
||||
assert_eq!(suggestion, Some(Suggestion {
|
||||
name: "z",
|
||||
target: Some("foo"),
|
||||
|
@ -10,6 +10,7 @@ pub(crate) enum Keyword {
|
||||
DotenvFilename,
|
||||
DotenvLoad,
|
||||
DotenvPath,
|
||||
DotenvRequired,
|
||||
Else,
|
||||
Export,
|
||||
Fallback,
|
||||
@ -24,6 +25,8 @@ pub(crate) enum Keyword {
|
||||
Shell,
|
||||
Tempdir,
|
||||
True,
|
||||
Unexport,
|
||||
Unstable,
|
||||
WindowsPowershell,
|
||||
WindowsShell,
|
||||
X,
|
||||
|
92
src/lib.rs
92
src/lib.rs
@ -13,41 +13,60 @@
|
||||
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 {
|
||||
crate::{
|
||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
||||
alias::Alias, analyzer::Analyzer, argument_parser::ArgumentParser, assignment::Assignment,
|
||||
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
|
||||
color::Color, color_display::ColorDisplay, command_ext::CommandExt, compilation::Compilation,
|
||||
compile_error::CompileError, compile_error_kind::CompileErrorKind, compiler::Compiler,
|
||||
condition::Condition, conditional_operator::ConditionalOperator, config::Config,
|
||||
config_error::ConfigError, constants::constants, count::Count, delimiter::Delimiter,
|
||||
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error,
|
||||
evaluator::Evaluator, 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,
|
||||
color::Color, color_display::ColorDisplay, command_color::CommandColor,
|
||||
command_ext::CommandExt, compilation::Compilation, compile_error::CompileError,
|
||||
compile_error_kind::CompileErrorKind, compiler::Compiler, condition::Condition,
|
||||
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
|
||||
constants::constants, count::Count, delimiter::Delimiter, 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_context::RecipeContext,
|
||||
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,
|
||||
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,
|
||||
unresolved_recipe::UnresolvedRecipe, unstable_feature::UnstableFeature, use_color::UseColor,
|
||||
variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||
},
|
||||
camino::Utf8Path,
|
||||
clap::ValueEnum,
|
||||
derivative::Derivative,
|
||||
edit_distance::edit_distance,
|
||||
lexiclean::Lexiclean,
|
||||
libc::EXIT_FAILURE,
|
||||
log::{info, warn},
|
||||
regex::Regex,
|
||||
serde::{
|
||||
ser::{SerializeMap, SerializeSeq},
|
||||
Serialize, Serializer,
|
||||
},
|
||||
snafu::{ResultExt, Snafu},
|
||||
std::{
|
||||
borrow::Cow,
|
||||
cmp,
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
env,
|
||||
ffi::OsString,
|
||||
fmt::{self, Debug, Display, Formatter},
|
||||
fs,
|
||||
io::{self, Write},
|
||||
io::{self, Read, Seek, Write},
|
||||
iter::{self, FromIterator},
|
||||
mem,
|
||||
ops::Deref,
|
||||
@ -59,23 +78,10 @@ pub(crate) use {
|
||||
sync::{Mutex, MutexGuard, OnceLock},
|
||||
vec,
|
||||
},
|
||||
{
|
||||
camino::Utf8Path,
|
||||
derivative::Derivative,
|
||||
edit_distance::edit_distance,
|
||||
lexiclean::Lexiclean,
|
||||
libc::EXIT_FAILURE,
|
||||
log::{info, warn},
|
||||
regex::Regex,
|
||||
serde::{
|
||||
ser::{SerializeMap, SerializeSeq},
|
||||
Serialize, Serializer,
|
||||
},
|
||||
snafu::{ResultExt, Snafu},
|
||||
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
|
||||
typed_arena::Arena,
|
||||
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
|
||||
},
|
||||
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
|
||||
tempfile::tempfile,
|
||||
typed_arena::Arena,
|
||||
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
@ -87,10 +93,11 @@ pub use crate::run::run;
|
||||
#[doc(hidden)]
|
||||
pub use unindent::unindent;
|
||||
|
||||
pub(crate) type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
|
||||
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
||||
pub(crate) type RunResult<'a, T = ()> = Result<T, Error<'a>>;
|
||||
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
||||
type CompileResult<'a, T = ()> = Result<T, CompileError<'a>>;
|
||||
type ConfigResult<T> = Result<T, ConfigError>;
|
||||
type FunctionResult = Result<String, String>;
|
||||
type RunResult<'a, T = ()> = Result<T, Error<'a>>;
|
||||
type SearchResult<T> = Result<T, SearchError>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
@ -114,6 +121,7 @@ pub mod summary;
|
||||
|
||||
mod alias;
|
||||
mod analyzer;
|
||||
mod argument_parser;
|
||||
mod assignment;
|
||||
mod assignment_resolver;
|
||||
mod ast;
|
||||
@ -121,6 +129,7 @@ mod attribute;
|
||||
mod binding;
|
||||
mod color;
|
||||
mod color_display;
|
||||
mod command_color;
|
||||
mod command_ext;
|
||||
mod compilation;
|
||||
mod compile_error;
|
||||
@ -139,6 +148,7 @@ mod dump_format;
|
||||
mod enclosure;
|
||||
mod error;
|
||||
mod evaluator;
|
||||
mod execution_context;
|
||||
mod expression;
|
||||
mod fragment;
|
||||
mod function;
|
||||
@ -169,7 +179,6 @@ mod positional;
|
||||
mod ran;
|
||||
mod range_ext;
|
||||
mod recipe;
|
||||
mod recipe_context;
|
||||
mod recipe_resolver;
|
||||
mod recipe_signature;
|
||||
mod run;
|
||||
@ -195,6 +204,7 @@ mod token_kind;
|
||||
mod unindent;
|
||||
mod unresolved_dependency;
|
||||
mod unresolved_recipe;
|
||||
mod unstable_feature;
|
||||
mod use_color;
|
||||
mod variables;
|
||||
mod verbosity;
|
||||
|
@ -1,7 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
const DEFAULT_DOTENV_FILENAME: &str = ".env";
|
||||
|
||||
pub(crate) fn load_dotenv(
|
||||
config: &Config,
|
||||
settings: &Settings,
|
||||
@ -17,16 +15,22 @@ pub(crate) fn load_dotenv(
|
||||
.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());
|
||||
}
|
||||
|
||||
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() {
|
||||
let path = directory.join(filename);
|
||||
@ -35,7 +39,11 @@ pub(crate) fn load_dotenv(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BTreeMap::new())
|
||||
if settings.dotenv_required {
|
||||
Err(Error::DotenvRequired)
|
||||
} else {
|
||||
Ok(BTreeMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_file(path: &Path) -> RunResult<'static, BTreeMap<String, String>> {
|
||||
|
@ -1,5 +1,5 @@
|
||||
fn main() {
|
||||
if let Err(code) = just::run() {
|
||||
if let Err(code) = just::run(std::env::args_os()) {
|
||||
std::process::exit(code);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,13 @@ impl<'src> Namepath<'src> {
|
||||
pub(crate) fn join(&self, name: Name<'src>) -> Self {
|
||||
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> {
|
||||
|
@ -54,6 +54,11 @@ impl<'src> Node<'src> for Item<'src> {
|
||||
}
|
||||
Self::Recipe(recipe) => recipe.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,10 +289,12 @@ impl<'src> Node<'src> for Set<'src> {
|
||||
Setting::AllowDuplicateRecipes(value)
|
||||
| Setting::AllowDuplicateVariables(value)
|
||||
| Setting::DotenvLoad(value)
|
||||
| Setting::DotenvRequired(value)
|
||||
| Setting::Export(value)
|
||||
| Setting::Fallback(value)
|
||||
| Setting::PositionalArguments(value)
|
||||
| Setting::Quiet(value)
|
||||
| Setting::Unstable(value)
|
||||
| Setting::WindowsPowerShell(value)
|
||||
| Setting::IgnoreComments(value) => {
|
||||
set.push_mut(value.to_string());
|
||||
|
@ -15,7 +15,7 @@ pub(crate) enum OutputError {
|
||||
}
|
||||
|
||||
impl Display for OutputError {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Code(code) => write!(f, "Process exited with status code {code}"),
|
||||
Self::Io(ref io_error) => write!(f, "Error executing process: {io_error}"),
|
||||
|
@ -14,7 +14,7 @@ pub(crate) struct Parameter<'src> {
|
||||
}
|
||||
|
||||
impl<'src> ColorDisplay for Parameter<'src> {
|
||||
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() {
|
||||
write!(f, "{}", color.annotation().paint(prefix))?;
|
||||
}
|
||||
|
101
src/parser.rs
101
src/parser.rs
@ -321,6 +321,14 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
self.accept(ByteOrderMark)?;
|
||||
|
||||
loop {
|
||||
let mut attributes = self.parse_attributes()?;
|
||||
let mut take_attributes = || {
|
||||
attributes
|
||||
.take()
|
||||
.map(|(_token, attributes)| attributes)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let next = self.next()?;
|
||||
|
||||
if let Some(comment) = self.accept(Comment)? {
|
||||
@ -334,12 +342,21 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
} else if self.next_is(Identifier) {
|
||||
match Keyword::from_lexeme(next.lexeme()) {
|
||||
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(take_attributes())?));
|
||||
}
|
||||
Some(Keyword::Export) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
|
||||
self.presume_keyword(Keyword::Export)?;
|
||||
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)
|
||||
if self.next_are(&[Identifier, StringToken])
|
||||
|| self.next_are(&[Identifier, Identifier, StringToken])
|
||||
@ -356,12 +373,15 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
});
|
||||
}
|
||||
Some(Keyword::Mod)
|
||||
if self.next_are(&[Identifier, Identifier, StringToken])
|
||||
|| self.next_are(&[Identifier, Identifier, Identifier, StringToken])
|
||||
if self.next_are(&[Identifier, Identifier, Comment])
|
||||
|| self.next_are(&[Identifier, Identifier, Eof])
|
||||
|| self.next_are(&[Identifier, Identifier, Eol])
|
||||
|| self.next_are(&[Identifier, Identifier, Identifier, StringToken])
|
||||
|| self.next_are(&[Identifier, Identifier, StringToken])
|
||||
|| self.next_are(&[Identifier, QuestionMark]) =>
|
||||
{
|
||||
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
|
||||
|
||||
self.presume_keyword(Keyword::Mod)?;
|
||||
|
||||
let optional = self.accepted(QuestionMark)?;
|
||||
@ -376,7 +396,9 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
};
|
||||
|
||||
items.push(Item::Module {
|
||||
attributes: take_attributes(),
|
||||
absolute: None,
|
||||
doc,
|
||||
name,
|
||||
optional,
|
||||
relative,
|
||||
@ -399,7 +421,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
items.push(Item::Recipe(self.parse_recipe(
|
||||
doc,
|
||||
false,
|
||||
BTreeSet::new(),
|
||||
take_attributes(),
|
||||
)?));
|
||||
}
|
||||
}
|
||||
@ -409,23 +431,17 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
items.push(Item::Recipe(self.parse_recipe(
|
||||
doc,
|
||||
true,
|
||||
BTreeSet::new(),
|
||||
take_attributes(),
|
||||
)?));
|
||||
} else if let Some(attributes) = self.parse_attributes()? {
|
||||
let next_keyword = Keyword::from_lexeme(self.next()?.lexeme());
|
||||
match next_keyword {
|
||||
Some(Keyword::Alias) if self.next_are(&[Identifier, Identifier, ColonEquals]) => {
|
||||
items.push(Item::Alias(self.parse_alias(attributes)?));
|
||||
}
|
||||
_ => {
|
||||
let quiet = self.accepted(At)?;
|
||||
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
|
||||
items.push(Item::Recipe(self.parse_recipe(doc, quiet, attributes)?));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(self.unexpected_token()?);
|
||||
}
|
||||
|
||||
if let Some((token, attributes)) = attributes {
|
||||
return Err(token.error(CompileErrorKind::ExtraneousAttributes {
|
||||
count: attributes.len(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if self.next_token == self.tokens.len() {
|
||||
@ -917,11 +933,13 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
Some(Setting::AllowDuplicateVariables(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::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
|
||||
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
|
||||
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
|
||||
Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)),
|
||||
Keyword::Unstable => Some(Setting::Unstable(self.parse_set_bool()?)),
|
||||
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
|
||||
_ => None,
|
||||
};
|
||||
@ -974,22 +992,31 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
}
|
||||
|
||||
/// Parse recipe attributes
|
||||
fn parse_attributes(&mut self) -> CompileResult<'src, Option<BTreeSet<Attribute<'src>>>> {
|
||||
fn parse_attributes(
|
||||
&mut self,
|
||||
) -> CompileResult<'src, Option<(Token<'src>, BTreeSet<Attribute<'src>>)>> {
|
||||
let mut attributes = BTreeMap::new();
|
||||
|
||||
while self.accepted(BracketL)? {
|
||||
let mut token = None;
|
||||
|
||||
while let Some(bracket) = self.accept(BracketL)? {
|
||||
token.get_or_insert(bracket);
|
||||
|
||||
loop {
|
||||
let name = self.parse_name()?;
|
||||
|
||||
let argument = if self.accepted(ParenL)? {
|
||||
let argument = self.parse_string_literal()?;
|
||||
let maybe_argument = if self.accepted(Colon)? {
|
||||
let arg = self.parse_string_literal()?;
|
||||
Some(arg)
|
||||
} else if self.accepted(ParenL)? {
|
||||
let arg = self.parse_string_literal()?;
|
||||
self.expect(ParenR)?;
|
||||
Some(argument)
|
||||
Some(arg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let attribute = Attribute::new(name, argument)?;
|
||||
let attribute = Attribute::new(name, maybe_argument)?;
|
||||
|
||||
if let Some(line) = attributes.get(&attribute) {
|
||||
return Err(name.error(CompileErrorKind::DuplicateAttribute {
|
||||
@ -1011,7 +1038,7 @@ impl<'run, 'src> Parser<'run, 'src> {
|
||||
if attributes.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(attributes.into_keys().collect()))
|
||||
Ok(Some((token.unwrap(), attributes.into_keys().collect())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1152,6 +1179,18 @@ mod tests {
|
||||
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! {
|
||||
name: aliases_multiple,
|
||||
text: "alias t := test\nalias b := build",
|
||||
@ -2539,7 +2578,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "arch",
|
||||
found: 1,
|
||||
expected: 0..0,
|
||||
expected: 0..=0,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2553,7 +2592,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "env_var",
|
||||
found: 0,
|
||||
expected: 1..1,
|
||||
expected: 1..=1,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2567,7 +2606,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "env",
|
||||
found: 3,
|
||||
expected: 1..2,
|
||||
expected: 1..=2,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2581,7 +2620,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "env",
|
||||
found: 0,
|
||||
expected: 1..2,
|
||||
expected: 1..=2,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2595,7 +2634,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "env_var_or_default",
|
||||
found: 1,
|
||||
expected: 2..2,
|
||||
expected: 2..=2,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2609,7 +2648,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "join",
|
||||
found: 1,
|
||||
expected: 2..usize::MAX,
|
||||
expected: 2..=usize::MAX,
|
||||
},
|
||||
}
|
||||
|
||||
@ -2623,7 +2662,7 @@ mod tests {
|
||||
kind: FunctionArgumentCountMismatch {
|
||||
function: "replace",
|
||||
found: 1,
|
||||
expected: 3..3,
|
||||
expected: 3..=3,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ impl PlatformInterface for Platform {
|
||||
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;
|
||||
|
||||
// get current permissions
|
||||
@ -38,7 +38,7 @@ impl PlatformInterface for Platform {
|
||||
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
|
||||
.to_str()
|
||||
.map(str::to_string)
|
||||
@ -85,7 +85,7 @@ impl PlatformInterface for Platform {
|
||||
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
|
||||
// this is a nop
|
||||
Ok(())
|
||||
@ -97,7 +97,7 @@ impl PlatformInterface for Platform {
|
||||
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
|
||||
let mut cygpath = Command::new("cygpath");
|
||||
cygpath.current_dir(working_directory);
|
||||
|
@ -10,12 +10,12 @@ pub(crate) trait PlatformInterface {
|
||||
) -> Result<Command, OutputError>;
|
||||
|
||||
/// 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
|
||||
/// signal
|
||||
fn signal_from_exit_status(exit_status: ExitStatus) -> Option<i32>;
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
@ -10,16 +10,14 @@ pub(crate) trait RangeExt<T> {
|
||||
|
||||
pub(crate) struct DisplayRange<T>(T);
|
||||
|
||||
impl Display for DisplayRange<&Range<usize>> {
|
||||
impl Display for DisplayRange<&RangeInclusive<usize>> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.0.start == self.0.end {
|
||||
write!(f, "0")?;
|
||||
} else if self.0.start == self.0.end - 1 {
|
||||
write!(f, "{}", self.0.start)?;
|
||||
} else if self.0.end == usize::MAX {
|
||||
write!(f, "{} or more", self.0.start)?;
|
||||
if self.0.start() == self.0.end() {
|
||||
write!(f, "{}", self.0.start())?;
|
||||
} else if *self.0.end() == usize::MAX {
|
||||
write!(f, "{} or more", self.0.start())?;
|
||||
} else {
|
||||
write!(f, "{} to {}", self.0.start, self.0.end - 1)?;
|
||||
write!(f, "{} to {}", self.0.start(), self.0.end())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -76,10 +74,10 @@ mod tests {
|
||||
assert!(!(1..1).contains(&1));
|
||||
assert!((1..1).is_empty());
|
||||
assert!((5..5).is_empty());
|
||||
assert_eq!((1..1).display().to_string(), "0");
|
||||
assert_eq!((1..2).display().to_string(), "1");
|
||||
assert_eq!((5..6).display().to_string(), "5");
|
||||
assert_eq!((5..10).display().to_string(), "5 to 9");
|
||||
assert_eq!((1..usize::MAX).display().to_string(), "1 or more");
|
||||
assert_eq!((0..=0).display().to_string(), "0");
|
||||
assert_eq!((1..=1).display().to_string(), "1");
|
||||
assert_eq!((5..=5).display().to_string(), "5");
|
||||
assert_eq!((5..=9).display().to_string(), "5 to 9");
|
||||
assert_eq!((1..=usize::MAX).display().to_string(), "1 or more");
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,10 @@ impl<'src, D> Recipe<'src, D> {
|
||||
!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 {
|
||||
!self.attributes.contains(&Attribute::NoCd)
|
||||
}
|
||||
@ -146,9 +150,10 @@ impl<'src, D> Recipe<'src, D> {
|
||||
|
||||
pub(crate) fn run<'run>(
|
||||
&self,
|
||||
context: &RecipeContext<'src, 'run>,
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
scope: &Scope<'src, 'run>,
|
||||
positional: &[String],
|
||||
is_dependency: bool,
|
||||
) -> RunResult<'src, ()> {
|
||||
let config = &context.config;
|
||||
|
||||
@ -162,14 +167,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||
);
|
||||
}
|
||||
|
||||
let evaluator = Evaluator::recipe_evaluator(
|
||||
context.config,
|
||||
context.dotenv,
|
||||
context.module_source,
|
||||
scope,
|
||||
context.search,
|
||||
context.settings,
|
||||
);
|
||||
let evaluator = Evaluator::new(context, is_dependency, scope);
|
||||
|
||||
if self.shebang {
|
||||
self.run_shebang(context, scope, positional, config, evaluator)
|
||||
@ -180,7 +178,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||
|
||||
fn run_linewise<'run>(
|
||||
&self,
|
||||
context: &RecipeContext<'src, 'run>,
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
scope: &Scope<'src, 'run>,
|
||||
positional: &[String],
|
||||
config: &Config,
|
||||
@ -269,7 +267,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||
|
||||
cmd.arg(command);
|
||||
|
||||
if context.settings.positional_arguments {
|
||||
if self.takes_positional_arguments(context.settings) {
|
||||
cmd.arg(self.name.lexeme());
|
||||
cmd.args(positional);
|
||||
}
|
||||
@ -279,7 +277,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||
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()) {
|
||||
Ok(exit_status) => {
|
||||
@ -312,7 +310,7 @@ impl<'src, D> Recipe<'src, D> {
|
||||
|
||||
pub(crate) fn run_shebang<'run>(
|
||||
&self,
|
||||
context: &RecipeContext<'src, 'run>,
|
||||
context: &ExecutionContext<'src, 'run>,
|
||||
scope: &Scope<'src, 'run>,
|
||||
positional: &[String],
|
||||
config: &Config,
|
||||
@ -353,9 +351,9 @@ impl<'src, D> Recipe<'src, D> {
|
||||
let tempdir = match &context.settings.tempdir {
|
||||
Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)),
|
||||
None => {
|
||||
if let Some(cache_dir) = dirs::cache_dir() {
|
||||
let path = cache_dir.join("just");
|
||||
fs::create_dir_all(&path).map_err(|io_error| Error::CacheDirIo {
|
||||
if let Some(runtime_dir) = dirs::runtime_dir() {
|
||||
let path = runtime_dir.join("just");
|
||||
fs::create_dir_all(&path).map_err(|io_error| Error::RuntimeDirIo {
|
||||
io_error,
|
||||
path: path.clone(),
|
||||
})?;
|
||||
@ -370,7 +368,16 @@ impl<'src, D> Recipe<'src, D> {
|
||||
io_error: error,
|
||||
})?;
|
||||
let mut path = tempdir.path().to_path_buf();
|
||||
path.push(shebang.script_filename(self.name()));
|
||||
|
||||
let extension = self.attributes.iter().find_map(|attribute| {
|
||||
if let Attribute::Extension(extension) = attribute {
|
||||
Some(extension.cooked.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
path.push(shebang.script_filename(self.name(), extension));
|
||||
|
||||
{
|
||||
let mut f = fs::File::create(&path).map_err(|error| Error::TempdirIo {
|
||||
@ -421,11 +428,11 @@ impl<'src, D> Recipe<'src, D> {
|
||||
output_error,
|
||||
})?;
|
||||
|
||||
if context.settings.positional_arguments {
|
||||
if self.takes_positional_arguments(context.settings) {
|
||||
command.args(positional);
|
||||
}
|
||||
|
||||
command.export(context.settings, context.dotenv, scope);
|
||||
command.export(context.settings, context.dotenv, scope, context.unexports);
|
||||
|
||||
// run it!
|
||||
match InterruptHandler::guard(|| command.status()) {
|
||||
@ -478,7 +485,7 @@ impl<'src, D> 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 {
|
||||
writeln!(f, "# {doc}")?;
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ pub(crate) struct RecipeResolver<'src: 'run, 'run> {
|
||||
|
||||
impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
||||
pub(crate) fn resolve_recipes(
|
||||
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||
assignments: &'run Table<'src, Assignment<'src>>,
|
||||
settings: &Settings,
|
||||
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
||||
let mut resolver = Self {
|
||||
resolved_recipes: Table::new(),
|
||||
@ -39,6 +40,10 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
||||
}
|
||||
|
||||
for line in &recipe.body {
|
||||
if line.is_comment() && settings.ignore_comments {
|
||||
continue;
|
||||
}
|
||||
|
||||
for fragment in &line.fragments {
|
||||
if let Fragment::Interpolation { expression, .. } = fragment {
|
||||
for variable in expression.variables() {
|
||||
|
12
src/run.rs
12
src/run.rs
@ -1,8 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
/// Main entry point into just binary.
|
||||
/// Main entry point into `just`. Parse arguments from `args` and run.
|
||||
#[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)]
|
||||
ansi_term::enable_ansi_support().ok();
|
||||
|
||||
@ -11,12 +11,16 @@ pub fn run() -> Result<(), i32> {
|
||||
.filter("JUST_LOG")
|
||||
.write_style("JUST_LOG_STYLE"),
|
||||
)
|
||||
.init();
|
||||
.try_init()
|
||||
.ok();
|
||||
|
||||
let app = Config::app();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -4,6 +4,7 @@ const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0];
|
||||
pub(crate) const JUSTFILE_NAMES: [&str; 2] = ["justfile", ".justfile"];
|
||||
const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Search {
|
||||
pub(crate) justfile: PathBuf,
|
||||
pub(crate) working_directory: PathBuf,
|
||||
|
@ -13,7 +13,7 @@ impl<'src> Keyed<'src> 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)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ pub(crate) enum Setting<'src> {
|
||||
DotenvFilename(String),
|
||||
DotenvLoad(bool),
|
||||
DotenvPath(String),
|
||||
DotenvRequired(bool),
|
||||
Export(bool),
|
||||
Fallback(bool),
|
||||
IgnoreComments(bool),
|
||||
@ -14,21 +15,24 @@ pub(crate) enum Setting<'src> {
|
||||
Quiet(bool),
|
||||
Shell(Shell<'src>),
|
||||
Tempdir(String),
|
||||
Unstable(bool),
|
||||
WindowsPowerShell(bool),
|
||||
WindowsShell(Shell<'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 {
|
||||
Self::AllowDuplicateRecipes(value)
|
||||
| Self::AllowDuplicateVariables(value)
|
||||
| Self::DotenvLoad(value)
|
||||
| Self::DotenvRequired(value)
|
||||
| Self::Export(value)
|
||||
| Self::Fallback(value)
|
||||
| Self::IgnoreComments(value)
|
||||
| Self::PositionalArguments(value)
|
||||
| Self::Quiet(value)
|
||||
| Self::Unstable(value)
|
||||
| Self::WindowsPowerShell(value) => write!(f, "{value}"),
|
||||
Self::Shell(shell) | Self::WindowsShell(shell) => write!(f, "{shell}"),
|
||||
Self::DotenvFilename(value) | Self::DotenvPath(value) | Self::Tempdir(value) => {
|
||||
|
@ -10,8 +10,9 @@ pub(crate) struct Settings<'src> {
|
||||
pub(crate) allow_duplicate_recipes: bool,
|
||||
pub(crate) allow_duplicate_variables: bool,
|
||||
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_required: bool,
|
||||
pub(crate) export: bool,
|
||||
pub(crate) fallback: bool,
|
||||
pub(crate) ignore_comments: bool,
|
||||
@ -19,6 +20,7 @@ pub(crate) struct Settings<'src> {
|
||||
pub(crate) quiet: bool,
|
||||
pub(crate) shell: Option<Shell<'src>>,
|
||||
pub(crate) tempdir: Option<String>,
|
||||
pub(crate) unstable: bool,
|
||||
pub(crate) windows_powershell: bool,
|
||||
pub(crate) windows_shell: Option<Shell<'src>>,
|
||||
}
|
||||
@ -39,11 +41,14 @@ impl<'src> Settings<'src> {
|
||||
settings.dotenv_filename = Some(filename);
|
||||
}
|
||||
Setting::DotenvLoad(dotenv_load) => {
|
||||
settings.dotenv_load = Some(dotenv_load);
|
||||
settings.dotenv_load = dotenv_load;
|
||||
}
|
||||
Setting::DotenvPath(path) => {
|
||||
settings.dotenv_path = Some(PathBuf::from(path));
|
||||
}
|
||||
Setting::DotenvRequired(dotenv_required) => {
|
||||
settings.dotenv_required = dotenv_required;
|
||||
}
|
||||
Setting::Export(export) => {
|
||||
settings.export = export;
|
||||
}
|
||||
@ -62,6 +67,9 @@ impl<'src> Settings<'src> {
|
||||
Setting::Shell(shell) => {
|
||||
settings.shell = Some(shell);
|
||||
}
|
||||
Setting::Unstable(unstable) => {
|
||||
settings.unstable = unstable;
|
||||
}
|
||||
Setting::WindowsPowerShell(windows_powershell) => {
|
||||
settings.windows_powershell = windows_powershell;
|
||||
}
|
||||
|
@ -38,12 +38,14 @@ impl<'line> Shebang<'line> {
|
||||
.unwrap_or(self.interpreter)
|
||||
}
|
||||
|
||||
pub(crate) fn script_filename(&self, recipe: &str) -> String {
|
||||
match self.interpreter_filename() {
|
||||
"cmd" | "cmd.exe" => format!("{recipe}.bat"),
|
||||
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => format!("{recipe}.ps1"),
|
||||
_ => recipe.to_owned(),
|
||||
}
|
||||
pub(crate) fn script_filename(&self, recipe: &str, extension: Option<&str>) -> String {
|
||||
let extension = extension.unwrap_or_else(|| match self.interpreter_filename() {
|
||||
"cmd" | "cmd.exe" => ".bat",
|
||||
"powershell" | "powershell.exe" | "pwsh" | "pwsh.exe" => ".ps1",
|
||||
_ => "",
|
||||
});
|
||||
|
||||
format!("{recipe}{extension}")
|
||||
}
|
||||
|
||||
pub(crate) fn include_shebang_line(&self) -> bool {
|
||||
@ -138,7 +140,9 @@ mod tests {
|
||||
#[test]
|
||||
fn powershell_script_filename() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!powershell").unwrap().script_filename("foo"),
|
||||
Shebang::new("#!powershell")
|
||||
.unwrap()
|
||||
.script_filename("foo", None),
|
||||
"foo.ps1"
|
||||
);
|
||||
}
|
||||
@ -146,7 +150,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pwsh_script_filename() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!pwsh").unwrap().script_filename("foo"),
|
||||
Shebang::new("#!pwsh").unwrap().script_filename("foo", None),
|
||||
"foo.ps1"
|
||||
);
|
||||
}
|
||||
@ -156,7 +160,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
Shebang::new("#!powershell.exe")
|
||||
.unwrap()
|
||||
.script_filename("foo"),
|
||||
.script_filename("foo", None),
|
||||
"foo.ps1"
|
||||
);
|
||||
}
|
||||
@ -164,7 +168,9 @@ mod tests {
|
||||
#[test]
|
||||
fn pwsh_exe_script_filename() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!pwsh.exe").unwrap().script_filename("foo"),
|
||||
Shebang::new("#!pwsh.exe")
|
||||
.unwrap()
|
||||
.script_filename("foo", None),
|
||||
"foo.ps1"
|
||||
);
|
||||
}
|
||||
@ -172,7 +178,7 @@ mod tests {
|
||||
#[test]
|
||||
fn cmd_script_filename() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!cmd").unwrap().script_filename("foo"),
|
||||
Shebang::new("#!cmd").unwrap().script_filename("foo", None),
|
||||
"foo.bat"
|
||||
);
|
||||
}
|
||||
@ -180,14 +186,19 @@ mod tests {
|
||||
#[test]
|
||||
fn cmd_exe_script_filename() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!cmd.exe").unwrap().script_filename("foo"),
|
||||
Shebang::new("#!cmd.exe")
|
||||
.unwrap()
|
||||
.script_filename("foo", None),
|
||||
"foo.bat"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_script_filename() {
|
||||
assert_eq!(Shebang::new("#!bar").unwrap().script_filename("foo"), "foo");
|
||||
assert_eq!(
|
||||
Shebang::new("#!bar").unwrap().script_filename("foo", None),
|
||||
"foo"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -211,4 +222,26 @@ mod tests {
|
||||
fn include_shebang_line_other_windows() {
|
||||
assert!(!Shebang::new("#!foo -c").unwrap().include_shebang_line());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_with_extension() {
|
||||
assert_eq!(
|
||||
Shebang::new("#!bar")
|
||||
.unwrap()
|
||||
.script_filename("foo", Some(".sh")),
|
||||
"foo.sh"
|
||||
);
|
||||
assert_eq!(
|
||||
Shebang::new("#!pwsh.exe")
|
||||
.unwrap()
|
||||
.script_filename("foo", Some(".sh")),
|
||||
"foo.sh"
|
||||
);
|
||||
assert_eq!(
|
||||
Shebang::new("#!cmd.exe")
|
||||
.unwrap()
|
||||
.script_filename("foo", Some(".sh")),
|
||||
"foo.sh"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ pub(crate) struct 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)?;
|
||||
|
||||
for argument in &self.arguments {
|
||||
|
@ -1,9 +1,4 @@
|
||||
use {
|
||||
super::*,
|
||||
clap_mangen::Man,
|
||||
std::io::{Read, Seek},
|
||||
tempfile::tempfile,
|
||||
};
|
||||
use {super::*, clap_mangen::Man};
|
||||
|
||||
const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n";
|
||||
|
||||
@ -20,7 +15,7 @@ pub(crate) enum Subcommand {
|
||||
overrides: BTreeMap<String, String>,
|
||||
},
|
||||
Completions {
|
||||
shell: clap_complete::Shell,
|
||||
shell: completions::Shell,
|
||||
},
|
||||
Dump,
|
||||
Edit,
|
||||
@ -47,11 +42,7 @@ pub(crate) enum Subcommand {
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub(crate) fn execute<'src>(
|
||||
&self,
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
) -> Result<(), Error<'src>> {
|
||||
pub(crate) fn execute<'src>(&self, config: &Config, loader: &'src Loader) -> RunResult<'src> {
|
||||
use Subcommand::*;
|
||||
|
||||
match self {
|
||||
@ -88,7 +79,7 @@ impl Subcommand {
|
||||
justfile.run(config, &search, overrides, &[])?;
|
||||
}
|
||||
Dump => Self::dump(config, ast, justfile)?,
|
||||
Format => Self::format(config, &search, src, ast)?,
|
||||
Format => Self::format(config, &search, src, ast, justfile)?,
|
||||
Groups => Self::groups(config, justfile),
|
||||
List { path } => Self::list(config, justfile, path)?,
|
||||
Show { path } => Self::show(config, justfile, path)?,
|
||||
@ -102,7 +93,7 @@ impl Subcommand {
|
||||
|
||||
fn groups(config: &Config, justfile: &Justfile) {
|
||||
println!("Recipe groups:");
|
||||
for group in justfile.public_groups() {
|
||||
for group in justfile.public_groups(config) {
|
||||
println!("{}{group}", config.list_prefix);
|
||||
}
|
||||
}
|
||||
@ -112,16 +103,17 @@ impl Subcommand {
|
||||
loader: &'src Loader,
|
||||
arguments: &[String],
|
||||
overrides: &BTreeMap<String, String>,
|
||||
) -> Result<(), Error<'src>> {
|
||||
) -> RunResult<'src> {
|
||||
if matches!(
|
||||
config.search_config,
|
||||
SearchConfig::FromInvocationDirectory | SearchConfig::FromSearchDirectory { .. }
|
||||
) {
|
||||
let starting_path = match &config.search_config {
|
||||
SearchConfig::FromInvocationDirectory => config.invocation_directory.clone(),
|
||||
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||
env::current_dir().unwrap().join(search_directory)
|
||||
}
|
||||
SearchConfig::FromSearchDirectory { search_directory } => config
|
||||
.invocation_directory
|
||||
.join(search_directory)
|
||||
.lexiclean(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
@ -155,7 +147,7 @@ impl Subcommand {
|
||||
};
|
||||
|
||||
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() {
|
||||
Some(parent) => {
|
||||
unknown_recipes_errors.get_or_insert(err);
|
||||
@ -197,8 +189,10 @@ impl Subcommand {
|
||||
config: &Config,
|
||||
loader: &'src Loader,
|
||||
search: &Search,
|
||||
) -> Result<Compilation<'src>, Error<'src>> {
|
||||
let compilation = Compiler::compile(config.unstable, loader, &search.justfile)?;
|
||||
) -> RunResult<'src, Compilation<'src>> {
|
||||
let compilation = Compiler::compile(loader, &search.justfile)?;
|
||||
|
||||
compilation.justfile.check_unstable(config)?;
|
||||
|
||||
if config.verbosity.loud() {
|
||||
for warning in &compilation.justfile.warnings {
|
||||
@ -219,8 +213,8 @@ impl Subcommand {
|
||||
search: &Search,
|
||||
overrides: &BTreeMap<String, String>,
|
||||
chooser: Option<&str>,
|
||||
) -> Result<(), Error<'src>> {
|
||||
let mut recipes = Vec::<&Recipe<Dependency>>::new();
|
||||
) -> RunResult<'src> {
|
||||
let mut recipes = Vec::<&Recipe>::new();
|
||||
let mut stack = vec![justfile];
|
||||
while let Some(module) = stack.pop() {
|
||||
recipes.extend(
|
||||
@ -236,7 +230,15 @@ impl Subcommand {
|
||||
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
|
||||
.settings
|
||||
@ -261,14 +263,15 @@ impl Subcommand {
|
||||
};
|
||||
|
||||
for recipe in recipes {
|
||||
if let Err(io_error) = child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.expect("Child was created with piped stdio")
|
||||
.write_all(format!("{}\n", recipe.namepath).as_bytes())
|
||||
{
|
||||
return Err(Error::ChooserWrite { io_error, chooser });
|
||||
}
|
||||
writeln!(
|
||||
child.stdin.as_mut().unwrap(),
|
||||
"{}",
|
||||
recipe.namepath.spaced()
|
||||
)
|
||||
.map_err(|io_error| Error::ChooserWrite {
|
||||
io_error,
|
||||
chooser: chooser.clone(),
|
||||
})?;
|
||||
}
|
||||
|
||||
let output = match child.wait_with_output() {
|
||||
@ -295,72 +298,12 @@ impl Subcommand {
|
||||
justfile.run(config, search, overrides, &recipes)
|
||||
}
|
||||
|
||||
fn completions(shell: clap_complete::Shell) -> RunResult<'static, ()> {
|
||||
use clap_complete::Shell;
|
||||
|
||||
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());
|
||||
|
||||
fn completions(shell: completions::Shell) -> RunResult<'static, ()> {
|
||||
println!("{}", shell.script()?);
|
||||
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 {
|
||||
DumpFormat::Json => {
|
||||
serde_json::to_writer(io::stdout(), justfile)
|
||||
@ -372,7 +315,7 @@ impl Subcommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edit(search: &Search) -> Result<(), Error<'static>> {
|
||||
fn edit(search: &Search) -> RunResult<'static> {
|
||||
let editor = env::var_os("VISUAL")
|
||||
.or_else(|| env::var_os("EDITOR"))
|
||||
.unwrap_or_else(|| "vim".into());
|
||||
@ -394,8 +337,14 @@ impl Subcommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format(config: &Config, search: &Search, src: &str, ast: &Ast) -> Result<(), Error<'static>> {
|
||||
config.require_unstable("The `--fmt` command is currently unstable.")?;
|
||||
fn format(
|
||||
config: &Config,
|
||||
search: &Search,
|
||||
src: &str,
|
||||
ast: &Ast,
|
||||
justfile: &Justfile,
|
||||
) -> RunResult<'static> {
|
||||
config.require_unstable(justfile, UnstableFeature::FormatSubcommand)?;
|
||||
|
||||
let formatted = ast.to_string();
|
||||
|
||||
@ -439,7 +388,7 @@ impl Subcommand {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init(config: &Config) -> Result<(), Error<'static>> {
|
||||
fn init(config: &Config) -> RunResult<'static> {
|
||||
let search = Search::init(&config.search_config, &config.invocation_directory)?;
|
||||
|
||||
if search.justfile.is_file() {
|
||||
@ -459,7 +408,7 @@ impl Subcommand {
|
||||
}
|
||||
}
|
||||
|
||||
fn man() -> Result<(), Error<'static>> {
|
||||
fn man() -> RunResult<'static> {
|
||||
let mut buffer = Vec::<u8>::new();
|
||||
|
||||
Man::new(Config::app())
|
||||
@ -479,12 +428,41 @@ impl Subcommand {
|
||||
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 {
|
||||
module = module
|
||||
.modules
|
||||
.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) {
|
||||
fn format_doc(
|
||||
config: &Config,
|
||||
name: &str,
|
||||
doc: Option<&str>,
|
||||
max_signature_width: usize,
|
||||
signature_widths: &BTreeMap<&str, usize>,
|
||||
) {
|
||||
if let Some(doc) = doc {
|
||||
if !doc.is_empty() && doc.lines().count() <= 1 {
|
||||
print!(
|
||||
"{:padding$}{} {}",
|
||||
"",
|
||||
config.color.stdout().doc().paint("#"),
|
||||
config.color.stdout().doc().paint(doc),
|
||||
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
let aliases = if config.no_aliases {
|
||||
@ -520,6 +498,11 @@ impl Subcommand {
|
||||
);
|
||||
}
|
||||
}
|
||||
if !config.list_submodules {
|
||||
for (name, _) in &module.modules {
|
||||
signature_widths.insert(name, UnicodeWidthStr::width(format!("{name} ...").as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
signature_widths
|
||||
};
|
||||
@ -531,7 +514,11 @@ impl Subcommand {
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
|
||||
print!("{}", config.list_heading);
|
||||
let list_prefix = config.list_prefix.repeat(depth + 1);
|
||||
|
||||
if depth == 0 {
|
||||
print!("{}", config.list_heading);
|
||||
}
|
||||
|
||||
let groups = {
|
||||
let mut groups = BTreeMap::<Option<String>, Vec<&Recipe>>::new();
|
||||
@ -548,23 +535,33 @@ impl Subcommand {
|
||||
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);
|
||||
}
|
||||
|
||||
let no_groups = groups.contains_key(&None) && groups.len() == 1;
|
||||
|
||||
for (i, group) in ordered.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
println!();
|
||||
}
|
||||
|
||||
let no_groups = groups.contains_key(&None) && groups.len() == 1;
|
||||
|
||||
if !no_groups {
|
||||
print!("{}", config.list_prefix);
|
||||
if let Some(group_name) = group {
|
||||
println!("[{group_name}]");
|
||||
print!("{list_prefix}");
|
||||
if let Some(group) = &group {
|
||||
println!("[{group}]");
|
||||
} else {
|
||||
println!("(no group)");
|
||||
}
|
||||
}
|
||||
|
||||
for recipe in recipes {
|
||||
for recipe in groups.get(&group).unwrap() {
|
||||
for (i, name) in iter::once(&recipe.name())
|
||||
.chain(aliases.get(recipe.name()).unwrap_or(&Vec::new()))
|
||||
.enumerate()
|
||||
@ -579,8 +576,7 @@ impl Subcommand {
|
||||
if doc.lines().count() > 1 {
|
||||
for line in doc.lines() {
|
||||
println!(
|
||||
"{}{} {}",
|
||||
config.list_prefix,
|
||||
"{list_prefix}{} {}",
|
||||
config.color.stdout().doc().paint("#"),
|
||||
config.color.stdout().doc().paint(line),
|
||||
);
|
||||
@ -589,44 +585,61 @@ impl Subcommand {
|
||||
}
|
||||
|
||||
print!(
|
||||
"{}{}",
|
||||
config.list_prefix,
|
||||
"{list_prefix}{}",
|
||||
RecipeSignature { name, recipe }.color_display(config.color.stdout())
|
||||
);
|
||||
|
||||
if let Some(doc) = doc {
|
||||
if doc.lines().count() <= 1 {
|
||||
print!(
|
||||
"{:padding$}{} {}",
|
||||
"",
|
||||
config.color.stdout().doc().paint("#"),
|
||||
config.color.stdout().doc().paint(&doc),
|
||||
padding = max_signature_width.saturating_sub(signature_widths[name]) + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
format_doc(
|
||||
config,
|
||||
name,
|
||||
doc.as_deref(),
|
||||
max_signature_width,
|
||||
&signature_widths,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for submodule in module.modules(config) {
|
||||
println!("{}{} ...", config.list_prefix, submodule.name(),);
|
||||
}
|
||||
if config.list_submodules {
|
||||
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 (i, submodule) in module.modules(config).into_iter().enumerate() {
|
||||
if !no_groups && !groups.is_empty() && i == 0 {
|
||||
println!();
|
||||
}
|
||||
|
||||
print!("{list_prefix}{} ...", submodule.name());
|
||||
format_doc(
|
||||
config,
|
||||
submodule.name(),
|
||||
submodule.doc.as_deref(),
|
||||
max_signature_width,
|
||||
&signature_widths,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show<'src>(
|
||||
config: &Config,
|
||||
mut module: &Justfile<'src>,
|
||||
path: &ModulePath,
|
||||
) -> Result<(), Error<'src>> {
|
||||
) -> RunResult<'src> {
|
||||
for name in &path.path[0..path.path.len() - 1] {
|
||||
module = module
|
||||
.modules
|
||||
.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();
|
||||
@ -640,8 +653,8 @@ impl Subcommand {
|
||||
println!("{}", recipe.color_display(config.color.stdout()));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::UnknownRecipes {
|
||||
recipes: vec![name.to_owned()],
|
||||
Err(Error::UnknownRecipe {
|
||||
recipe: name.to_owned(),
|
||||
suggestion: module.suggest_recipe(name),
|
||||
})
|
||||
}
|
||||
|
@ -25,10 +25,10 @@ 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();
|
||||
|
||||
match Compiler::compile(false, &loader, path) {
|
||||
match Compiler::compile(&loader, path) {
|
||||
Ok(compilation) => Ok(Ok(Summary::new(&compilation.justfile))),
|
||||
Err(error) => Ok(Err(if let Error::Compile { compile_error } = error {
|
||||
compile_error.to_string()
|
||||
|
@ -77,7 +77,7 @@ pub(crate) fn analysis_error(
|
||||
let mut paths: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||
paths.insert("justfile".into(), "justfile".into());
|
||||
|
||||
match Analyzer::analyze(&[], &paths, &asts, &root, None) {
|
||||
match Analyzer::analyze(&asts, None, &[], None, &paths, &root) {
|
||||
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
||||
Err(have) => {
|
||||
let want = CompileError {
|
||||
@ -131,7 +131,7 @@ macro_rules! run_error {
|
||||
}
|
||||
|
||||
macro_rules! assert_matches {
|
||||
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )?) => {
|
||||
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => {}
|
||||
left => panic!(
|
||||
|
14
src/thunk.rs
14
src/thunk.rs
@ -6,42 +6,42 @@ pub(crate) enum Thunk<'src> {
|
||||
Nullary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
function: fn(function::Context) -> Result<String, String>,
|
||||
function: fn(function::Context) -> FunctionResult,
|
||||
},
|
||||
Unary {
|
||||
name: Name<'src>,
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
function: fn(function::Context, &str) -> Result<String, String>,
|
||||
function: fn(function::Context, &str) -> FunctionResult,
|
||||
arg: Box<Expression<'src>>,
|
||||
},
|
||||
UnaryOpt {
|
||||
name: Name<'src>,
|
||||
#[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>>>),
|
||||
},
|
||||
UnaryPlus {
|
||||
name: Name<'src>,
|
||||
#[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>>),
|
||||
},
|
||||
Binary {
|
||||
name: Name<'src>,
|
||||
#[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],
|
||||
},
|
||||
BinaryPlus {
|
||||
name: Name<'src>,
|
||||
#[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>>),
|
||||
},
|
||||
Ternary {
|
||||
name: Name<'src>,
|
||||
#[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],
|
||||
},
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ pub(crate) enum TokenKind {
|
||||
}
|
||||
|
||||
impl Display for TokenKind {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
use TokenKind::*;
|
||||
write!(
|
||||
f,
|
||||
|
@ -7,7 +7,7 @@ pub(crate) struct 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() {
|
||||
write!(f, "{}", self.recipe)
|
||||
} else {
|
||||
|
14
src/unstable_feature.rs
Normal file
14
src/unstable_feature.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Ord, Eq, PartialOrd)]
|
||||
pub(crate) enum UnstableFeature {
|
||||
FormatSubcommand,
|
||||
}
|
||||
|
||||
impl Display for UnstableFeature {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::FormatSubcommand => write!(f, "The `--fmt` command is currently unstable."),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
use super::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
|
||||
pub(crate) enum UseColor {
|
||||
Auto,
|
||||
Always,
|
||||
|
@ -72,7 +72,7 @@ fn multiple_attributes_one_line_error_message() {
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Expected ']', ',', or '(', but found identifier
|
||||
error: Expected ']', ':', ',', or '(', but found identifier
|
||||
——▶ justfile:1:17
|
||||
│
|
||||
1 │ [macos, windows linux]
|
||||
@ -193,3 +193,40 @@ fn doc_multiline() {
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[extension: '.txt']
|
||||
baz:
|
||||
#!/bin/sh
|
||||
echo $0
|
||||
",
|
||||
)
|
||||
.stdout_regex(r"*baz\.txt\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_on_linewise_error() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[extension: '.txt']
|
||||
baz:
|
||||
",
|
||||
)
|
||||
.stderr(
|
||||
"
|
||||
error: Recipe `baz` has invalid attribute `extension`
|
||||
——▶ justfile:2:1
|
||||
│
|
||||
2 │ baz:
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
@ -86,7 +86,6 @@ fn recipes_in_submodules_can_be_chosen() {
|
||||
.args(["--unstable", "--choose"])
|
||||
.env("JUST_CHOOSER", "head -n10")
|
||||
.write("bar.just", "baz:\n echo BAZ")
|
||||
.test_round_trip(false)
|
||||
.justfile(
|
||||
"
|
||||
mod bar
|
||||
@ -185,7 +184,13 @@ fn status_error() {
|
||||
"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(
|
||||
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||
|
@ -47,16 +47,21 @@ test! {
|
||||
status: 2,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: env_is_loaded,
|
||||
justfile: "
|
||||
set dotenv-load
|
||||
#[test]
|
||||
fn env_is_loaded() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load
|
||||
|
||||
x:
|
||||
echo XYZ
|
||||
",
|
||||
args: ("--command", "sh", "-c", "printf $DOTENV_KEY"),
|
||||
stdout: "dotenv-value",
|
||||
x:
|
||||
echo XYZ
|
||||
",
|
||||
)
|
||||
.args(["--command", "sh", "-c", "printf $DOTENV_KEY"])
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.stdout("dotenv-value")
|
||||
.run();
|
||||
}
|
||||
|
||||
test! {
|
||||
|
@ -1,19 +1,42 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let tempdir = tempdir();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bash() {
|
||||
let output = Command::new(executable_path("just"))
|
||||
.arg("--completions")
|
||||
.arg("bash")
|
||||
.current_dir(tempdir.path())
|
||||
.args(["--completions", "bash"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ reply_equals() {
|
||||
}
|
||||
|
||||
# --- Initial Setup ---
|
||||
source ./completions/just.bash
|
||||
source "$1"
|
||||
cd tests/completions
|
||||
cargo build
|
||||
PATH="$(git rev-parse --show-toplevel)/target/debug:$PATH"
|
||||
|
27
tests/datetime.rs
Normal file
27
tests/datetime.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn datetime() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
x := datetime('%Y-%m-%d %z')
|
||||
",
|
||||
)
|
||||
.args(["--eval", "x"])
|
||||
.stdout_regex(r"\d\d\d\d-\d\d-\d\d [+-]\d\d\d\d")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn datetime_utc() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
x := datetime_utc('%Y-%m-%d %Z')
|
||||
",
|
||||
)
|
||||
.args(["--eval", "x"])
|
||||
.stdout_regex(r"\d\d\d\d-\d\d-\d\d UTC")
|
||||
.run();
|
||||
}
|
242
tests/dotenv.rs
242
tests/dotenv.rs
@ -12,40 +12,54 @@ fn dotenv() {
|
||||
.run();
|
||||
}
|
||||
|
||||
test! {
|
||||
name: set_false,
|
||||
justfile: r#"
|
||||
set dotenv-load := false
|
||||
#[test]
|
||||
fn set_false() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
set dotenv-load := false
|
||||
|
||||
foo:
|
||||
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",
|
||||
@foo:
|
||||
if [ -n "${DOTENV_KEY+1}" ]; then echo defined; else echo undefined; fi
|
||||
"#,
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.stdout("undefined\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
test! {
|
||||
name: set_implicit,
|
||||
justfile: r#"
|
||||
set dotenv-load
|
||||
#[test]
|
||||
fn set_implicit() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load
|
||||
|
||||
foo:
|
||||
echo $DOTENV_KEY
|
||||
"#,
|
||||
stdout: "dotenv-value\n",
|
||||
stderr: "echo $DOTENV_KEY\n",
|
||||
foo:
|
||||
echo $DOTENV_KEY
|
||||
",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.stdout("dotenv-value\n")
|
||||
.stderr("echo $DOTENV_KEY\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
test! {
|
||||
name: set_true,
|
||||
justfile: r#"
|
||||
set dotenv-load := true
|
||||
#[test]
|
||||
fn set_true() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load := true
|
||||
|
||||
foo:
|
||||
echo $DOTENV_KEY
|
||||
"#,
|
||||
stdout: "dotenv-value\n",
|
||||
stderr: "echo $DOTENV_KEY\n",
|
||||
foo:
|
||||
echo $DOTENV_KEY
|
||||
",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.stdout("dotenv-value\n")
|
||||
.stderr("echo $DOTENV_KEY\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -53,32 +67,28 @@ fn no_warning() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo ${DOTENV_KEY:-unset}
|
||||
",
|
||||
foo:
|
||||
echo ${DOTENV_KEY:-unset}
|
||||
",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.stdout("unset\n")
|
||||
.stderr("echo ${DOTENV_KEY:-unset}\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_not_found() {
|
||||
fn dotenv_required() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
set dotenv-required
|
||||
|
||||
foo:
|
||||
",
|
||||
)
|
||||
.args(["--dotenv-path", ".env.prod"])
|
||||
.stderr(if cfg!(windows) {
|
||||
"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)
|
||||
.stderr("error: Dotenv file not found\n")
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -87,9 +97,9 @@ fn path_resolves() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
)
|
||||
.tree(tree! {
|
||||
subdir: {
|
||||
@ -107,9 +117,9 @@ fn filename_resolves() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
)
|
||||
.tree(tree! {
|
||||
".env.special": "JUST_TEST_VARIABLE=bar"
|
||||
@ -145,11 +155,11 @@ fn path_flag_overwrites_no_load() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load := false
|
||||
set dotenv-load := false
|
||||
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
",
|
||||
)
|
||||
.tree(tree! {
|
||||
subdir: {
|
||||
@ -227,12 +237,12 @@ fn program_argument_has_priority_for_dotenv_filename() {
|
||||
fn program_argument_has_priority_for_dotenv_path() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
r#"
|
||||
set dotenv-path := "subdir/.env"
|
||||
"
|
||||
set dotenv-path := 'subdir/.env'
|
||||
|
||||
foo:
|
||||
@echo $JUST_TEST_VARIABLE
|
||||
"#,
|
||||
",
|
||||
)
|
||||
.tree(tree! {
|
||||
subdir: {
|
||||
@ -257,8 +267,130 @@ fn dotenv_path_is_relative_to_working_directory() {
|
||||
@echo $DOTENV_KEY
|
||||
",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.tree(tree! { subdir: { } })
|
||||
.current_dir("subdir")
|
||||
.stdout("dotenv-value\n")
|
||||
.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();
|
||||
}
|
||||
|
@ -64,7 +64,13 @@ fn status_error() {
|
||||
"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(
|
||||
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||
|
13
tests/fmt.rs
13
tests/fmt.rs
@ -4,10 +4,7 @@ test! {
|
||||
name: unstable_not_passed,
|
||||
justfile: "",
|
||||
args: ("--fmt"),
|
||||
stderr: "
|
||||
error: The `--fmt` command is currently unstable. \
|
||||
Invoke `just` with the `--unstable` flag to enable unstable features.
|
||||
",
|
||||
stderr_regex: "error: The `--fmt` command is currently unstable..*",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
@ -126,7 +123,13 @@ fn write_error() {
|
||||
|
||||
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();
|
||||
|
||||
|
@ -34,10 +34,10 @@ b := env_var_or_default('ZADDY', 'HTAP')
|
||||
x := env_var_or_default('XYZ', 'ABC')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{p}}' '{{b}}' '{{x}}'
|
||||
/usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'
|
||||
"#,
|
||||
stdout: format!("{} HTAP ABC\n", env::var("USER").unwrap()).as_str(),
|
||||
stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(),
|
||||
stderr: format!("/usr/bin/env echo '{}' 'HTAP' 'ABC'\n", env::var("USER").unwrap()).as_str(),
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -52,10 +52,10 @@ ext := extension('/foo/bar/baz.hello')
|
||||
jn := join('a', 'b')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}'
|
||||
/usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}' '{{jn}}'
|
||||
"#,
|
||||
stdout: "/foo/bar/baz baz baz.hello /foo/bar hello a/b\n",
|
||||
stderr: "/bin/echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n",
|
||||
stderr: "/usr/bin/env echo '/foo/bar/baz' 'baz' 'baz.hello' '/foo/bar' 'hello' 'a/b'\n",
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -69,10 +69,10 @@ dir := parent_directory('/foo/')
|
||||
ext := extension('/foo/bar/baz.hello.ciao')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
|
||||
/usr/bin/env echo '{{we}}' '{{fs}}' '{{fn}}' '{{dir}}' '{{ext}}'
|
||||
"#,
|
||||
stdout: "/foo/bar/baz baz.hello baz.hello.ciao / ciao\n",
|
||||
stderr: "/bin/echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n",
|
||||
stderr: "/usr/bin/env echo '/foo/bar/baz' 'baz.hello' 'baz.hello.ciao' '/' 'ciao'\n",
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@ -82,7 +82,7 @@ test! {
|
||||
we := without_extension('')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
|
||||
@ -102,7 +102,7 @@ test! {
|
||||
we := extension('')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{}\n{}\n{}\n{}\n{}\n",
|
||||
@ -121,7 +121,7 @@ test! {
|
||||
we := extension('foo')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{}\n{}\n{}\n{}\n{}\n",
|
||||
@ -140,7 +140,7 @@ test! {
|
||||
we := file_stem('')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{}\n{}\n{}\n{}\n{}\n",
|
||||
@ -159,7 +159,7 @@ test! {
|
||||
we := file_name('')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{}\n{}\n{}\n{}\n{}\n",
|
||||
@ -178,7 +178,7 @@ test! {
|
||||
we := parent_directory('')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
|
||||
@ -198,7 +198,7 @@ test! {
|
||||
we := parent_directory('/')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{we}}'
|
||||
/usr/bin/env echo '{{we}}'
|
||||
"#,
|
||||
stdout: "",
|
||||
stderr: format!("{} {}\n{}\n{}\n{}\n{}\n",
|
||||
@ -220,10 +220,10 @@ b := env_var_or_default('ZADDY', 'HTAP')
|
||||
x := env_var_or_default('XYZ', 'ABC')
|
||||
|
||||
foo:
|
||||
/bin/echo '{{p}}' '{{b}}' '{{x}}'
|
||||
/usr/bin/env echo '{{p}}' '{{b}}' '{{x}}'
|
||||
"#,
|
||||
stdout: format!("{} HTAP ABC\n", env::var("USERNAME").unwrap()).as_str(),
|
||||
stderr: format!("/bin/echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(),
|
||||
stderr: format!("/usr/bin/env echo '{}' 'HTAP' 'ABC'\n", env::var("USERNAME").unwrap()).as_str(),
|
||||
}
|
||||
|
||||
test! {
|
||||
@ -864,7 +864,6 @@ fn source_file() {
|
||||
|
||||
Test::new()
|
||||
.args(["--evaluate", "x"])
|
||||
.test_round_trip(false)
|
||||
.justfile(
|
||||
"
|
||||
import 'foo.just'
|
||||
@ -875,8 +874,7 @@ fn source_file() {
|
||||
.run();
|
||||
|
||||
Test::new()
|
||||
.args(["--unstable", "foo", "bar"])
|
||||
.test_round_trip(false)
|
||||
.args(["foo", "bar"])
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
@ -890,8 +888,7 @@ fn source_file() {
|
||||
#[test]
|
||||
fn source_directory() {
|
||||
Test::new()
|
||||
.args(["--unstable", "foo", "bar"])
|
||||
.test_round_trip(false)
|
||||
.args(["foo", "bar"])
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
@ -984,9 +981,7 @@ import-outer: import-inner
|
||||
echo '{{ module_directory() }}'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args([
|
||||
"--unstable",
|
||||
"outer",
|
||||
"import-outer",
|
||||
"baz",
|
||||
@ -1027,3 +1022,74 @@ import
|
||||
)
|
||||
.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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unary_argument_count_mismamatch_error_message() {
|
||||
Test::new()
|
||||
.justfile("x := datetime()")
|
||||
.args(["--evaluate"])
|
||||
.stderr(
|
||||
"
|
||||
error: Function `datetime` called with 0 arguments but takes 1
|
||||
——▶ justfile:1:6
|
||||
│
|
||||
1 │ x := datetime()
|
||||
│ ^^^^^^^^
|
||||
",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_abbreviations_are_accepted() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
abbreviated := justfile_dir()
|
||||
unabbreviated := justfile_directory()
|
||||
|
||||
@foo:
|
||||
# {{ assert(abbreviated == unabbreviated, 'fail') }}
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invocation_dir_native_abbreviation_is_accepted() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
abbreviated := invocation_directory_native()
|
||||
unabbreviated := invocation_dir_native()
|
||||
|
||||
@foo:
|
||||
# {{ assert(abbreviated == unabbreviated, 'fail') }}
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
141
tests/groups.rs
141
tests/groups.rs
@ -96,6 +96,47 @@ fn list_with_groups_unsorted() {
|
||||
.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]
|
||||
fn list_groups() {
|
||||
Test::new()
|
||||
@ -144,3 +185,103 @@ fn list_groups_with_custom_prefix() {
|
||||
)
|
||||
.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();
|
||||
}
|
||||
|
@ -97,3 +97,41 @@ fn dont_evaluate_comments() {
|
||||
)
|
||||
.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();
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ fn import_succeeds() {
|
||||
@echo A
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("a")
|
||||
.stdout("B\nA\n")
|
||||
.run();
|
||||
@ -34,7 +33,6 @@ fn missing_import_file_error() {
|
||||
@echo A
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("a")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(
|
||||
@ -60,7 +58,6 @@ fn missing_optional_imports_are_ignored() {
|
||||
@echo A
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("a")
|
||||
.stdout("A\n")
|
||||
.run();
|
||||
@ -79,7 +76,6 @@ fn trailing_spaces_after_import_are_ignored() {
|
||||
@echo A
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.stdout("A\n")
|
||||
.run();
|
||||
}
|
||||
@ -99,7 +95,6 @@ fn import_after_recipe() {
|
||||
import './import.justfile'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.stdout("A\n")
|
||||
.run();
|
||||
}
|
||||
@ -126,7 +121,6 @@ fn import_recipes_are_not_default() {
|
||||
"import.justfile": "bar:",
|
||||
})
|
||||
.justfile("import './import.justfile'")
|
||||
.test_round_trip(false)
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: Justfile contains no default recipe.\n")
|
||||
.run();
|
||||
@ -143,7 +137,6 @@ fn listed_recipes_in_imports_are_in_load_order() {
|
||||
)
|
||||
.write("import.justfile", "bar:")
|
||||
.args(["--list", "--unsorted"])
|
||||
.test_round_trip(false)
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -190,7 +183,6 @@ fn recipes_in_import_are_overridden_by_recipes_in_parent() {
|
||||
set allow-duplicate-recipes
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("a")
|
||||
.stdout("ROOT\n")
|
||||
.run();
|
||||
@ -216,7 +208,6 @@ fn variables_in_import_are_overridden_by_variables_in_parent() {
|
||||
@echo {{f}}
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("a")
|
||||
.stdout("bar\n")
|
||||
.run();
|
||||
@ -232,7 +223,6 @@ fn import_paths_beginning_with_tilde_are_expanded_to_homdir() {
|
||||
import '~/mod.just'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("foo")
|
||||
.stdout("FOOBAR\n")
|
||||
.env("HOME", "foobar")
|
||||
@ -248,7 +238,6 @@ fn imports_dump_correctly() {
|
||||
import './import.justfile'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--dump")
|
||||
.stdout("import './import.justfile'\n")
|
||||
.run();
|
||||
@ -263,7 +252,6 @@ fn optional_imports_dump_correctly() {
|
||||
import? './import.justfile'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--dump")
|
||||
.stdout("import? './import.justfile'\n")
|
||||
.run();
|
||||
@ -279,7 +267,6 @@ fn imports_in_root_run_in_justfile_directory() {
|
||||
import 'foo/import.justfile'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("bar")
|
||||
.stdout("BAZ")
|
||||
.run();
|
||||
@ -292,8 +279,6 @@ fn imports_in_submodules_run_in_submodule_directory() {
|
||||
.write("foo/mod.just", "import 'import.just'")
|
||||
.write("foo/import.just", "bar:\n @cat baz")
|
||||
.write("foo/baz", "BAZ")
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("bar")
|
||||
.stdout("BAZ")
|
||||
@ -306,7 +291,6 @@ fn nested_import_paths_are_relative_to_containing_submodule() {
|
||||
.justfile("import 'foo/import.just'")
|
||||
.write("foo/import.just", "import 'bar.just'")
|
||||
.write("foo/bar.just", "bar:\n @echo BAR")
|
||||
.test_round_trip(false)
|
||||
.arg("bar")
|
||||
.stdout("BAR\n")
|
||||
.run();
|
||||
@ -319,8 +303,6 @@ fn recipes_in_nested_imports_run_in_parent_module() {
|
||||
.write("foo/import.just", "import 'bar/import.just'")
|
||||
.write("foo/bar/import.just", "bar:\n @cat baz")
|
||||
.write("baz", "BAZ")
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("bar")
|
||||
.stdout("BAZ")
|
||||
.run();
|
||||
@ -339,7 +321,6 @@ fn shebang_recipes_in_imports_in_root_run_in_justfile_directory() {
|
||||
import 'foo/import.justfile'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("bar")
|
||||
.stdout("BAZ")
|
||||
.run();
|
||||
@ -357,7 +338,6 @@ fn recipes_imported_in_root_run_in_command_line_provided_working_directory() {
|
||||
"--justfile",
|
||||
"subdir/a.justfile",
|
||||
])
|
||||
.test_round_trip(false)
|
||||
.stdout("BAZBAZ")
|
||||
.run();
|
||||
}
|
||||
|
119
tests/json.rs
119
tests/json.rs
@ -3,7 +3,7 @@ use super::*;
|
||||
fn case(justfile: &str, value: Value) {
|
||||
Test::new()
|
||||
.justfile(justfile)
|
||||
.args(["--dump", "--dump-format", "json", "--unstable"])
|
||||
.args(["--dump", "--dump-format", "json"])
|
||||
.stdout(format!("{}\n", serde_json::to_string(&value).unwrap()))
|
||||
.run();
|
||||
}
|
||||
@ -18,6 +18,7 @@ fn alias() {
|
||||
",
|
||||
json!({
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"aliases": {
|
||||
"f": {
|
||||
"name": "f",
|
||||
@ -46,18 +47,22 @@ fn alias() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"positional_arguments": false,
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"ignore_comments": false,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -78,14 +83,16 @@ fn assignment() {
|
||||
}
|
||||
},
|
||||
"first": null,
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {},
|
||||
"settings": {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -93,9 +100,11 @@ fn assignment() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -113,6 +122,7 @@ fn body() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -136,8 +146,9 @@ fn body() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -145,9 +156,11 @@ fn body() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -164,6 +177,7 @@ fn dependencies() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"bar": {
|
||||
@ -200,8 +214,9 @@ fn dependencies() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -209,9 +224,11 @@ fn dependencies() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -240,6 +257,7 @@ fn dependency_argument() {
|
||||
json!({
|
||||
"aliases": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"assignments": {
|
||||
"x": {
|
||||
"export": false,
|
||||
@ -302,8 +320,9 @@ fn dependency_argument() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -311,9 +330,11 @@ fn dependency_argument() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -331,6 +352,7 @@ fn duplicate_recipes() {
|
||||
",
|
||||
json!({
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"aliases": {
|
||||
"f": {
|
||||
"attributes": [],
|
||||
@ -366,8 +388,9 @@ fn duplicate_recipes() {
|
||||
"allow_duplicate_recipes": true,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -375,9 +398,11 @@ fn duplicate_recipes() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -402,14 +427,16 @@ fn duplicate_variables() {
|
||||
}
|
||||
},
|
||||
"first": null,
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {},
|
||||
"settings": {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": true,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -417,9 +444,11 @@ fn duplicate_variables() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -432,6 +461,7 @@ fn doc_comment() {
|
||||
json!({
|
||||
"aliases": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"assignments": {},
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
@ -453,8 +483,9 @@ fn doc_comment() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -462,9 +493,11 @@ fn doc_comment() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -478,14 +511,16 @@ fn empty_justfile() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": null,
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {},
|
||||
"settings": {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -493,9 +528,11 @@ fn empty_justfile() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -515,6 +552,7 @@ fn parameters() {
|
||||
json!({
|
||||
"aliases": {},
|
||||
"first": "a",
|
||||
"doc": null,
|
||||
"assignments": {},
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
@ -636,8 +674,9 @@ fn parameters() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -645,9 +684,11 @@ fn parameters() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -665,6 +706,7 @@ fn priors() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "a",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"a": {
|
||||
@ -721,8 +763,9 @@ fn priors() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -730,9 +773,11 @@ fn priors() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -746,6 +791,7 @@ fn private() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "_foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"_foo": {
|
||||
@ -766,8 +812,9 @@ fn private() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -775,9 +822,11 @@ fn private() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -791,6 +840,7 @@ fn quiet() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -811,8 +861,9 @@ fn quiet() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -820,9 +871,11 @@ fn quiet() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -848,6 +901,7 @@ fn settings() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -870,6 +924,7 @@ fn settings() {
|
||||
"dotenv_filename": "filename",
|
||||
"dotenv_load": true,
|
||||
"dotenv_path": "path",
|
||||
"dotenv_required": false,
|
||||
"export": true,
|
||||
"fallback": true,
|
||||
"ignore_comments": true,
|
||||
@ -880,9 +935,11 @@ fn settings() {
|
||||
"command": "a",
|
||||
},
|
||||
"tempdir": null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -899,6 +956,7 @@ fn shebang() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -919,8 +977,9 @@ fn shebang() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -928,9 +987,11 @@ fn shebang() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir": null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -944,6 +1005,7 @@ fn simple() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -964,8 +1026,9 @@ fn simple() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"ignore_comments": false,
|
||||
@ -973,9 +1036,11 @@ fn simple() {
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir": null,
|
||||
"unstable": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -992,6 +1057,7 @@ fn attribute() {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "foo",
|
||||
"doc": null,
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"foo": {
|
||||
@ -1012,18 +1078,21 @@ fn attribute() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"positional_arguments": false,
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"ignore_comments": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}),
|
||||
);
|
||||
@ -1034,25 +1103,27 @@ fn module() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
# hello
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.tree(tree! {
|
||||
"foo.just": "bar:",
|
||||
})
|
||||
.args(["--dump", "--dump-format", "json", "--unstable"])
|
||||
.test_round_trip(false)
|
||||
.args(["--dump", "--dump-format", "json"])
|
||||
.stdout(format!(
|
||||
"{}\n",
|
||||
serde_json::to_string(&json!({
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": null,
|
||||
"doc": null,
|
||||
"modules": {
|
||||
"foo": {
|
||||
"aliases": {},
|
||||
"assignments": {},
|
||||
"first": "bar",
|
||||
"doc": "hello",
|
||||
"modules": {},
|
||||
"recipes": {
|
||||
"bar": {
|
||||
@ -1073,18 +1144,21 @@ fn module() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"positional_arguments": false,
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"ignore_comments": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
},
|
||||
},
|
||||
@ -1093,18 +1167,21 @@ fn module() {
|
||||
"allow_duplicate_recipes": false,
|
||||
"allow_duplicate_variables": false,
|
||||
"dotenv_filename": null,
|
||||
"dotenv_load": null,
|
||||
"dotenv_load": false,
|
||||
"dotenv_path": null,
|
||||
"dotenv_required": false,
|
||||
"export": false,
|
||||
"fallback": false,
|
||||
"positional_arguments": false,
|
||||
"quiet": false,
|
||||
"shell": null,
|
||||
"tempdir" : null,
|
||||
"unstable": false,
|
||||
"ignore_comments": false,
|
||||
"windows_powershell": false,
|
||||
"windows_shell": null,
|
||||
},
|
||||
"unexports": [],
|
||||
"warnings": [],
|
||||
}))
|
||||
.unwrap()
|
||||
|
@ -5,7 +5,6 @@ pub(crate) use {
|
||||
tempdir::tempdir,
|
||||
test::{assert_eval_eq, Output, Test},
|
||||
},
|
||||
cradle::input::Input,
|
||||
executable_path::executable_path,
|
||||
just::unindent,
|
||||
libc::{EXIT_FAILURE, EXIT_SUCCESS},
|
||||
@ -20,7 +19,7 @@ pub(crate) use {
|
||||
fs,
|
||||
io::Write,
|
||||
iter,
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
|
||||
process::{Command, Stdio},
|
||||
str,
|
||||
},
|
||||
@ -47,6 +46,7 @@ mod completions;
|
||||
mod conditional;
|
||||
mod confirm;
|
||||
mod constants;
|
||||
mod datetime;
|
||||
mod delimiters;
|
||||
mod directories;
|
||||
mod dotenv;
|
||||
@ -104,7 +104,10 @@ mod summary;
|
||||
mod tempdir;
|
||||
mod timestamps;
|
||||
mod undefined_variables;
|
||||
mod unexport;
|
||||
mod unstable;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
#[cfg(target_family = "windows")]
|
||||
mod windows_shell;
|
||||
mod working_directory;
|
||||
|
235
tests/list.rs
235
tests/list.rs
@ -12,8 +12,7 @@ fn modules_unsorted() {
|
||||
mod bar
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--list", "--unsorted"])
|
||||
.args(["--list", "--unsorted"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -156,8 +155,7 @@ fn list_submodule() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--list", "foo"])
|
||||
.args(["--list", "foo"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -177,8 +175,7 @@ fn list_nested_submodule() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--list", "foo", "bar"])
|
||||
.args(["--list", "foo", "bar"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -195,8 +192,7 @@ fn list_nested_submodule() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--list", "foo::bar"])
|
||||
.args(["--list", "foo::bar"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
@ -209,7 +205,7 @@ fn list_nested_submodule() {
|
||||
#[test]
|
||||
fn list_invalid_path() {
|
||||
Test::new()
|
||||
.args(["--unstable", "--list", "$hello"])
|
||||
.args(["--list", "$hello"])
|
||||
.stderr("error: Invalid module path `$hello`\n")
|
||||
.status(1)
|
||||
.run();
|
||||
@ -218,8 +214,227 @@ fn list_invalid_path() {
|
||||
#[test]
|
||||
fn list_unknown_submodule() {
|
||||
Test::new()
|
||||
.args(["--unstable", "--list", "hello"])
|
||||
.args(["--list", "hello"])
|
||||
.stderr("error: Justfile does not contain submodule `hello`\n")
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_with_groups_in_modules() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
[group('FOO')]
|
||||
foo:
|
||||
|
||||
mod bar
|
||||
",
|
||||
)
|
||||
.write("bar.just", "[group('BAZ')]\nbaz:")
|
||||
.args(["--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
|
||||
",
|
||||
)
|
||||
.args(["--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
|
||||
",
|
||||
)
|
||||
.args(["--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")
|
||||
.args(["--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
|
||||
",
|
||||
)
|
||||
.args(["--list", "--list-submodules"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
foo:
|
||||
bar:
|
||||
baz
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_doc_rendered() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
"
|
||||
# Module foo
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
foo ... # Module foo
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_doc_aligned() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.write("bar.just", "")
|
||||
.justfile(
|
||||
"
|
||||
# Module foo
|
||||
mod foo
|
||||
|
||||
# comment
|
||||
mod very_long_name_for_module \"bar.just\" # comment
|
||||
|
||||
# will change your world
|
||||
recipe:
|
||||
@echo Hi
|
||||
",
|
||||
)
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
recipe # will change your world
|
||||
foo ... # Module foo
|
||||
very_long_name_for_module ... # comment
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_before_submodules_following_groups() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
|
||||
[group: 'baz']
|
||||
bar:
|
||||
",
|
||||
)
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
[baz]
|
||||
bar
|
||||
|
||||
foo ...
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_space_before_submodules_not_following_groups() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.args(["--list"])
|
||||
.stdout(
|
||||
"
|
||||
Available recipes:
|
||||
foo ...
|
||||
",
|
||||
)
|
||||
.run();
|
||||
}
|
||||
|
@ -652,7 +652,7 @@ test! {
|
||||
justfile: "hello:",
|
||||
args: ("foo", "bar"),
|
||||
stdout: "",
|
||||
stderr: "error: Justfile does not contain recipes `foo` or `bar`.\n",
|
||||
stderr: "error: Justfile does not contain recipe `foo`.\n",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
@ -884,7 +884,7 @@ _private-recipe:
|
||||
args: ("--list"),
|
||||
stdout: r#"
|
||||
Available recipes:
|
||||
a Z="\t z" # something else
|
||||
a Z="\t z" # something else
|
||||
hello a b='B ' c='C' # this does a thing
|
||||
"#,
|
||||
}
|
||||
@ -1589,84 +1589,6 @@ echo:
|
||||
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! {
|
||||
name: invalid_escape_sequence_message,
|
||||
justfile: r#"
|
||||
|
290
tests/modules.rs
290
tests/modules.rs
@ -1,20 +1,16 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn modules_are_unstable() {
|
||||
fn modules_are_stable() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stderr(
|
||||
"error: Modules are currently unstable. \
|
||||
Invoke `just` with the `--unstable` flag to enable unstable features.\n",
|
||||
)
|
||||
.status(EXIT_FAILURE)
|
||||
.write("foo.just", "@bar:\n echo ok")
|
||||
.args(["foo", "bar"])
|
||||
.stdout("ok\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -27,8 +23,6 @@ fn default_recipe_in_submodule_must_have_no_arguments() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.stderr("error: Recipe `foo` cannot be used as default recipe since it requires at least 1 argument.\n")
|
||||
.status(EXIT_FAILURE)
|
||||
@ -44,8 +38,6 @@ fn module_recipes_can_be_run_as_subcommands() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -61,8 +53,6 @@ fn module_recipes_can_be_run_with_path_syntax() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo::foo")
|
||||
.stdout("FOO\n")
|
||||
.run();
|
||||
@ -78,8 +68,6 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo::bar::baz")
|
||||
.stdout("BAZ\n")
|
||||
.run();
|
||||
@ -88,21 +76,18 @@ fn nested_module_recipes_can_be_run_with_path_syntax() {
|
||||
#[test]
|
||||
fn invalid_path_syntax() {
|
||||
Test::new()
|
||||
.test_round_trip(false)
|
||||
.arg(":foo::foo")
|
||||
.stderr("error: Justfile does not contain recipe `:foo::foo`.\n")
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
|
||||
Test::new()
|
||||
.test_round_trip(false)
|
||||
.arg("foo::foo:")
|
||||
.stderr("error: Justfile does not contain recipe `foo::foo:`.\n")
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
|
||||
Test::new()
|
||||
.test_round_trip(false)
|
||||
.arg("foo:::foo")
|
||||
.stderr("error: Justfile does not contain recipe `foo:::foo`.\n")
|
||||
.status(EXIT_FAILURE)
|
||||
@ -112,10 +97,9 @@ fn invalid_path_syntax() {
|
||||
#[test]
|
||||
fn missing_recipe_after_invalid_path() {
|
||||
Test::new()
|
||||
.test_round_trip(false)
|
||||
.arg(":foo::foo")
|
||||
.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)
|
||||
.run();
|
||||
}
|
||||
@ -130,8 +114,6 @@ fn assignments_are_evaluated_in_modules() {
|
||||
bar := 'PARENT'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("CHILD\n")
|
||||
@ -147,8 +129,6 @@ fn module_subcommand_runs_default_recipe() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
.run();
|
||||
@ -164,8 +144,6 @@ fn modules_can_contain_other_modules() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("bar")
|
||||
.arg("baz")
|
||||
@ -183,8 +161,6 @@ fn circular_module_imports_are_detected() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("bar")
|
||||
.arg("baz")
|
||||
@ -211,8 +187,6 @@ foo:
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -233,9 +207,7 @@ foo:
|
||||
set allow-duplicate-recipes
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.status(EXIT_FAILURE)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stderr(
|
||||
@ -269,9 +241,7 @@ fn modules_conflict_with_recipes() {
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.status(EXIT_FAILURE)
|
||||
.arg("--unstable")
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -295,9 +265,7 @@ fn modules_conflict_with_aliases() {
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.status(EXIT_FAILURE)
|
||||
.arg("--unstable")
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -313,7 +281,6 @@ fn modules_conflict_with_other_modules() {
|
||||
bar:
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(
|
||||
"
|
||||
@ -324,7 +291,6 @@ fn modules_conflict_with_other_modules() {
|
||||
│ ^^^
|
||||
",
|
||||
)
|
||||
.arg("--unstable")
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -337,8 +303,6 @@ fn modules_are_dumped_correctly() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--dump")
|
||||
.stdout("mod foo\n")
|
||||
.run();
|
||||
@ -353,8 +317,6 @@ fn optional_modules_are_dumped_correctly() {
|
||||
mod? foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--dump")
|
||||
.stdout("mod? foo\n")
|
||||
.run();
|
||||
@ -369,8 +331,6 @@ fn modules_can_be_in_subdirectory() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -386,8 +346,6 @@ fn modules_in_subdirectory_can_be_named_justfile() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -403,8 +361,6 @@ fn modules_in_subdirectory_can_be_named_justfile_with_any_case() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -420,8 +376,6 @@ fn modules_in_subdirectory_can_have_leading_dot() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -438,17 +392,16 @@ fn modules_require_unambiguous_file() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(
|
||||
"
|
||||
error: Found multiple source files for module `foo`: `foo.just` and `foo/justfile`
|
||||
error: Found multiple source files for module `foo`: `foo/justfile` and `foo.just`
|
||||
——▶ justfile:1:5
|
||||
│
|
||||
1 │ mod foo
|
||||
│ ^^^
|
||||
",
|
||||
"
|
||||
.replace('/', MAIN_SEPARATOR_STR),
|
||||
)
|
||||
.run();
|
||||
}
|
||||
@ -461,8 +414,6 @@ fn missing_module_file_error() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr(
|
||||
"
|
||||
@ -487,8 +438,6 @@ fn missing_optional_modules_do_not_trigger_error() {
|
||||
@echo BAR
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.stdout("BAR\n")
|
||||
.run();
|
||||
}
|
||||
@ -504,8 +453,6 @@ fn missing_optional_modules_do_not_conflict() {
|
||||
",
|
||||
)
|
||||
.write("baz.just", "baz:\n @echo BAZ")
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("baz")
|
||||
.stdout("BAZ\n")
|
||||
@ -515,7 +462,6 @@ fn missing_optional_modules_do_not_conflict() {
|
||||
#[test]
|
||||
fn root_dotenv_is_available_to_submodules() {
|
||||
Test::new()
|
||||
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load
|
||||
@ -523,10 +469,9 @@ fn root_dotenv_is_available_to_submodules() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.write("foo.just", "foo:\n @echo $DOTENV_KEY")
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.args(["foo", "foo"])
|
||||
.stdout("dotenv-value\n")
|
||||
.run();
|
||||
}
|
||||
@ -534,10 +479,6 @@ fn root_dotenv_is_available_to_submodules() {
|
||||
#[test]
|
||||
fn dotenv_settings_in_submodule_are_ignored() {
|
||||
Test::new()
|
||||
.write(
|
||||
"foo.just",
|
||||
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
|
||||
)
|
||||
.justfile(
|
||||
"
|
||||
set dotenv-load
|
||||
@ -545,10 +486,12 @@ fn dotenv_settings_in_submodule_are_ignored() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.write(
|
||||
"foo.just",
|
||||
"set dotenv-load := false\nfoo:\n @echo $DOTENV_KEY",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.args(["foo", "foo"])
|
||||
.stdout("dotenv-value\n")
|
||||
.run();
|
||||
}
|
||||
@ -562,8 +505,21 @@ fn modules_may_specify_path() {
|
||||
mod foo 'commands/foo.just'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modules_may_specify_path_to_directory() {
|
||||
Test::new()
|
||||
.write("commands/bar/mod.just", "foo:\n @echo FOO")
|
||||
.justfile(
|
||||
"
|
||||
mod foo 'commands/bar'
|
||||
",
|
||||
)
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOO\n")
|
||||
@ -579,8 +535,6 @@ fn modules_with_paths_are_dumped_correctly() {
|
||||
mod foo 'commands/foo.just'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--dump")
|
||||
.stdout("mod foo 'commands/foo.just'\n")
|
||||
.run();
|
||||
@ -595,8 +549,6 @@ fn optional_modules_with_paths_are_dumped_correctly() {
|
||||
mod? foo 'commands/foo.just'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--dump")
|
||||
.stdout("mod? foo 'commands/foo.just'\n")
|
||||
.run();
|
||||
@ -611,7 +563,6 @@ fn recipes_may_be_named_mod() {
|
||||
@echo FOO
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("mod")
|
||||
.arg("bar")
|
||||
.stdout("FOO\n")
|
||||
@ -628,8 +579,6 @@ fn submodule_linewise_recipes_run_in_submodule_directory() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("BAR")
|
||||
@ -646,8 +595,6 @@ fn submodule_shebang_recipes_run_in_submodule_directory() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("BAR")
|
||||
@ -664,8 +611,6 @@ fn module_paths_beginning_with_tilde_are_expanded_to_homdir() {
|
||||
mod foo '~/mod.just'
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo")
|
||||
.arg("foo")
|
||||
.stdout("FOOBAR\n")
|
||||
@ -685,10 +630,177 @@ fn recipes_with_same_name_are_both_run() {
|
||||
@echo ROOT
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("foo::bar")
|
||||
.arg("bar")
|
||||
.stdout("MODULE\nROOT\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submodule_recipe_not_found_error_message() {
|
||||
Test::new()
|
||||
.args(["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
|
||||
",
|
||||
)
|
||||
.args(["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
|
||||
",
|
||||
)
|
||||
.args(["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(["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:")
|
||||
.args(["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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_can_follow_modules() {
|
||||
Test::new()
|
||||
.write("foo.just", "foo:\n @echo FOO")
|
||||
.justfile(
|
||||
"
|
||||
mod foo # this is foo
|
||||
",
|
||||
)
|
||||
.args(["foo", "foo"])
|
||||
.stdout("FOO\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_comment_on_module() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
"
|
||||
# Comment
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--list")
|
||||
.stdout("Available recipes:\n foo ... # Comment\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_attribute_on_module() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
r#"
|
||||
# Suppressed comment
|
||||
[doc: "Comment"]
|
||||
mod foo
|
||||
"#,
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--list")
|
||||
.stdout("Available recipes:\n foo ... # Comment\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_module_attribute_fails() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
r#"
|
||||
[no-cd]
|
||||
mod foo
|
||||
"#,
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--list")
|
||||
.stderr("error: Module `foo` has invalid attribute `no-cd`\n ——▶ justfile:2:5\n │\n2 │ mod foo\n │ ^^^\n")
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_doc_attribute_on_module() {
|
||||
Test::new()
|
||||
.write("foo.just", "")
|
||||
.justfile(
|
||||
r#"
|
||||
# Suppressed comment
|
||||
[doc]
|
||||
mod foo
|
||||
"#,
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--list")
|
||||
.stdout("Available recipes:\n foo ...\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ error: Expected identifier, but found ']'
|
||||
}
|
||||
|
||||
test! {
|
||||
name: unattached_attribute_before_comment,
|
||||
name: extraneous_attribute_before_comment,
|
||||
justfile: r#"
|
||||
[no-exit-message]
|
||||
# This is a doc comment
|
||||
@ -88,25 +88,31 @@ hello:
|
||||
@exit 100
|
||||
"#,
|
||||
stderr: r#"
|
||||
error: Expected '@', '[', or identifier, but found comment
|
||||
——▶ justfile:2:1
|
||||
error: Extraneous attribute
|
||||
——▶ justfile:1:1
|
||||
│
|
||||
2 │ # This is a doc comment
|
||||
│ ^^^^^^^^^^^^^^^^^^^^^^^
|
||||
1 │ [no-exit-message]
|
||||
│ ^
|
||||
"#,
|
||||
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
test! {
|
||||
name: unattached_attribute_before_empty_line,
|
||||
name: extraneous_attribute_before_empty_line,
|
||||
justfile: r#"
|
||||
[no-exit-message]
|
||||
|
||||
hello:
|
||||
@exit 100
|
||||
"#,
|
||||
stderr: "error: Expected '@', '[', or identifier, but found end of line\n ——▶ justfile:2:1\n │\n2 │ \n │ ^\n",
|
||||
stderr: "
|
||||
error: Extraneous attribute
|
||||
——▶ justfile:1:1
|
||||
│
|
||||
1 │ [no-exit-message]
|
||||
│ ^
|
||||
",
|
||||
status: EXIT_FAILURE,
|
||||
}
|
||||
|
||||
|
@ -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! {
|
||||
name: variadic_linewise,
|
||||
justfile: r#"
|
||||
@ -51,6 +76,18 @@ test! {
|
||||
stdout: "hello\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: shebang_with_attribute,
|
||||
justfile: "
|
||||
[positional-arguments]
|
||||
foo bar:
|
||||
#!/bin/sh
|
||||
echo $1
|
||||
",
|
||||
args: ("foo", "hello"),
|
||||
stdout: "hello\n",
|
||||
}
|
||||
|
||||
test! {
|
||||
name: variadic_shebang,
|
||||
justfile: r#"
|
||||
|
@ -143,6 +143,22 @@ fn single_upwards() {
|
||||
search_test(path, &["../"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_upwards() {
|
||||
let tmp = temptree! {
|
||||
justfile: "default:\n\techo ok",
|
||||
foo: {
|
||||
bar: {
|
||||
justfile: "default:\n\techo foo",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let path = tmp.path().join("foo/bar");
|
||||
|
||||
search_test(path, &["../default"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_dot_justfile() {
|
||||
Test::new()
|
||||
|
@ -151,3 +151,39 @@ test! {
|
||||
stderr: "echo bar\necho foo\n",
|
||||
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();
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ fn shell_expanded_strings_are_dumped_correctly() {
|
||||
",
|
||||
)
|
||||
.env("JUST_TEST_VARIABLE", "FOO")
|
||||
.args(["--dump", "--unstable"])
|
||||
.args(["--dump"])
|
||||
.stdout("x := x'$JUST_TEST_VARIABLE'\n")
|
||||
.run();
|
||||
}
|
||||
@ -82,6 +82,7 @@ fn shell_expanded_strings_can_be_used_in_settings() {
|
||||
echo $DOTENV_KEY
|
||||
",
|
||||
)
|
||||
.write(".env", "DOTENV_KEY=dotenv-value")
|
||||
.env("JUST_TEST_VARIABLE", ".env")
|
||||
.stdout("dotenv-value\n")
|
||||
.run();
|
||||
@ -113,9 +114,8 @@ fn shell_expanded_strings_can_be_used_in_mod_paths() {
|
||||
)
|
||||
.write("mod.just", "@bar:\n echo BAR")
|
||||
.env("JUST_TEST_VARIABLE", "mod.just")
|
||||
.args(["--unstable", "foo", "bar"])
|
||||
.args(["foo", "bar"])
|
||||
.stdout("BAR\n")
|
||||
.test_round_trip(false)
|
||||
.run();
|
||||
}
|
||||
|
||||
|
@ -110,8 +110,7 @@ fn show_recipe_at_path() {
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.args(["--unstable", "--show", "foo::bar"])
|
||||
.args(["--show", "foo::bar"])
|
||||
.stdout("bar:\n @echo MODULE\n")
|
||||
.run();
|
||||
}
|
||||
@ -124,3 +123,17 @@ fn show_invalid_path() {
|
||||
.status(1)
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_space_separated_path() {
|
||||
Test::new()
|
||||
.write("foo.just", "bar:\n @echo MODULE")
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.args(["--show", "foo bar"])
|
||||
.stdout("bar:\n @echo MODULE\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -65,9 +65,21 @@ fn submodule_recipes() {
|
||||
bar:
|
||||
",
|
||||
)
|
||||
.test_round_trip(false)
|
||||
.arg("--unstable")
|
||||
.arg("--summary")
|
||||
.stdout("bar foo::foo foo::bar::bar foo::bar::baz::baz foo::bar::baz::biz::biz\n")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_implies_unstable() {
|
||||
Test::new()
|
||||
.write("foo.just", "foo:")
|
||||
.justfile(
|
||||
"
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.arg("--summary")
|
||||
.stdout("foo::foo\n")
|
||||
.run();
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ pub(crate) fn tempdir() -> TempDir {
|
||||
|
||||
builder.prefix("just-test-tempdir");
|
||||
|
||||
if let Some(cache_dir) = dirs::cache_dir() {
|
||||
let path = cache_dir.join("just");
|
||||
if let Some(runtime_dir) = dirs::runtime_dir() {
|
||||
let path = runtime_dir.join("just");
|
||||
fs::create_dir_all(&path).unwrap();
|
||||
builder.tempdir_in(path)
|
||||
} else {
|
||||
|
@ -94,6 +94,11 @@ impl Test {
|
||||
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 {
|
||||
path.as_ref().clone_into(&mut self.current_dir);
|
||||
self
|
||||
@ -145,7 +150,7 @@ impl Test {
|
||||
}
|
||||
|
||||
pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef<str>) -> Self {
|
||||
self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap());
|
||||
self.stderr_regex = Some(Regex::new(&format!("^(?s){}$", stderr_regex.as_ref())).unwrap());
|
||||
self
|
||||
}
|
||||
|
||||
@ -164,6 +169,7 @@ impl Test {
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self {
|
||||
self.test_round_trip = test_round_trip;
|
||||
self
|
||||
@ -201,9 +207,8 @@ impl Test {
|
||||
} else {
|
||||
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"));
|
||||
|
||||
@ -258,7 +263,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.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr))
|
||||
{
|
||||
|
126
tests/unexport.rs
Normal file
126
tests/unexport.rs
Normal 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();
|
||||
}
|
@ -2,14 +2,9 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn set_unstable_true_with_env_var() {
|
||||
let justfile = r#"
|
||||
default:
|
||||
echo 'foo'
|
||||
"#;
|
||||
|
||||
for val in ["true", "some-arbitrary-string"] {
|
||||
Test::new()
|
||||
.justfile(justfile)
|
||||
.justfile("")
|
||||
.args(["--fmt"])
|
||||
.env("JUST_UNSTABLE", val)
|
||||
.status(EXIT_SUCCESS)
|
||||
@ -20,31 +15,54 @@ default:
|
||||
|
||||
#[test]
|
||||
fn set_unstable_false_with_env_var() {
|
||||
let justfile = r#"
|
||||
default:
|
||||
echo 'foo'
|
||||
"#;
|
||||
for val in ["0", "", "false"] {
|
||||
Test::new()
|
||||
.justfile(justfile)
|
||||
.args(["--fmt"])
|
||||
.env("JUST_UNSTABLE", val)
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
|
||||
.run();
|
||||
.justfile("")
|
||||
.args(["--fmt"])
|
||||
.env("JUST_UNSTABLE", val)
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr_regex("error: The `--fmt` command is currently unstable.*")
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_unstable_false_with_env_var_unset() {
|
||||
let justfile = r#"
|
||||
default:
|
||||
echo 'foo'
|
||||
"#;
|
||||
Test::new()
|
||||
.justfile(justfile)
|
||||
.justfile("")
|
||||
.args(["--fmt"])
|
||||
.status(EXIT_FAILURE)
|
||||
.stderr("error: The `--fmt` command is currently unstable. Invoke `just` with the `--unstable` flag to enable unstable features.\n")
|
||||
.stderr_regex("error: The `--fmt` command is currently unstable.*")
|
||||
.run();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_unstable_with_setting() {
|
||||
Test::new()
|
||||
.justfile("set unstable")
|
||||
.arg("--fmt")
|
||||
.stderr_regex("Wrote justfile to .*")
|
||||
.run();
|
||||
}
|
||||
|
||||
// This test should be re-enabled if we get a new unstable feature which is
|
||||
// encountered in source files. (As opposed to, for example, the unstable
|
||||
// `--fmt` subcommand, which is encountered on the command line.)
|
||||
#[cfg(any())]
|
||||
#[test]
|
||||
fn unstable_setting_does_not_affect_submodules() {
|
||||
Test::new()
|
||||
.justfile(
|
||||
"
|
||||
set unstable
|
||||
|
||||
mod foo
|
||||
",
|
||||
)
|
||||
.write("foo.just", "mod bar")
|
||||
.write("bar.just", "baz:\n echo hello")
|
||||
.args(["foo", "bar"])
|
||||
.stderr_regex("error: Modules are currently unstable.*")
|
||||
.status(EXIT_FAILURE)
|
||||
.run();
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user