Compare commits

...

74 Commits

Author SHA1 Message Date
Greg Shuflin
ba89f94e7e Use groups in justfile 2024-07-16 11:27:29 -07:00
Casey Rodarmor
a3693f3e8f
Add [extension: 'EXT'] attribute to set shebang recipe script file extension (#2256) 2024-07-15 13:08:28 -07:00
Casey Rodarmor
ea26e451fa
Suppress mod doc comment with empty [doc] attribute (#2254) 2024-07-15 16:27:48 +00:00
Greg Shuflin
d5ebc9515e
Allow [doc] annotation on modules (#2247) 2024-07-14 22:15:22 -07:00
Casey Rodarmor
023b126eb2
Release 1.31.0 (#2251)
- Bump version: 1.30.1 → 1.31.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
- Update version references in readme
2024-07-14 21:29:13 +00:00
Casey Rodarmor
687007a723
Stabilize modules (#2250) 2024-07-14 21:22:03 +00:00
Casey Rodarmor
6747c79082
Print space before submodules in --list with groups (#2244) 2024-07-14 02:20:35 +00:00
Casey Rodarmor
458805e283
Allow mod path to be directory containing module source (#2238) 2024-07-08 22:38:25 +00:00
Casey Rodarmor
d6669e0b97
Allow enabling unstable features with set unstable (#2237) 2024-07-08 03:45:03 +00:00
Casey Rodarmor
564814208f
Lexiclean search directory so .. does not check the current directory (#2236)
If the search directory was `..`, for example in the invocation
`just ../foo`, we would wind up checking the justfile in the current
directory since we did `INVOCATION_DIRECTORY/..`.ancestors(), which
would first return `INVOCATION_DIRECTORY`.

Instead, lexiclean the result of joining th invocation directory with
the search directory, so `..` is removed, and `ancestors()` doesn't
return the invocation directory.
2024-07-08 02:12:07 +00:00
Casey Rodarmor
f1020b4e6a
Allow abbreviating functions ending in _directory to _dir (#2235) 2024-07-07 22:47:18 +00:00
Casey Rodarmor
5e9f46e855
Release 1.30.1 (#2232)
- Bump version: 1.30.0 → 1.30.1
- Update changelog
- Update changelog contributor credits
2024-07-07 04:24:32 +00:00
Casey Rodarmor
241e7b46a5
Fix function argument count mismatch error message (#2231) 2024-07-07 04:19:36 +00:00
Casey Rodarmor
0c9b159aa4
Bump version to 1.30.0 (#2229) 2024-07-06 23:15:18 +00:00
Casey Rodarmor
d2f66815da
Release 1.30.0 (#2228)
- Bump version: 1.29.1 → 1.30.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
- Update version references in readme
2024-07-06 23:11:25 +00:00
Casey Rodarmor
50e8874e0e
Tweak readme (#2227) 2024-07-06 23:03:08 +00:00
Casey Rodarmor
8186992340
Add development guide to readme (#2226) 2024-07-06 19:21:32 +00:00
Casey Rodarmor
1242bd64aa
Add shell-expanded string syntax to grammar (#2223) 2024-07-05 00:43:17 +00:00
Casey Rodarmor
42691e1043
Add recipe for testing bash completion script (#2221) 2024-07-04 17:49:54 +00:00
Casey Rodarmor
3e0909701c
Fix use of justfile_directory() in readme (#2219) 2024-07-04 17:11:30 +00:00
Marc
5695384271
Avoid install and add 32-bit arm targets to install.sh (#2214) 2024-07-01 18:36:14 +00:00
Casey Rodarmor
39b2783c4b
Use default values for --list-heading and --list-prefix (#2213) 2024-06-30 21:14:39 +00:00
Greg Shuflin
208187fbb6
Use clap::ValueParser (#2211) 2024-06-30 19:16:10 +00:00
Casey Rodarmor
7683c81c08
Allow unstable features with --summary (#2210) 2024-06-29 18:12:31 -07:00
Casey Rodarmor
e0c031272d
Document module doc comments in readme (#2208) 2024-06-29 19:28:47 +00:00
Jacob Herbst
ef6a813dd1
Give modules doc comments for --list (#2199) 2024-06-28 21:13:11 -07:00
Casey Rodarmor
e07da79d40
Use -and instead of && in PowerShell completion script (#2204) 2024-06-28 07:52:16 +00:00
Casey Rodarmor
97c32e60ae
Fix readme formatting (#2203) 2024-06-27 23:03:05 +00:00
Mateusz Kurowski
929fd695d5
Link to justfiles on GitHub in readme (#2198) 2024-06-27 22:30:52 +00:00
Casey Rodarmor
23f1c1ca9f
Allow comments after mod statements (#2201) 2024-06-27 18:47:33 +00:00
Casey Rodarmor
570d3058cf
Link to modules when first introduced in readme (#2193) 2024-06-25 21:59:42 +00:00
dependabot[bot]
c900b6f478
Update softprops/action-gh-release (#2183) 2024-06-24 12:42:39 -07:00
Casey Rodarmor
af86a471e2
Don't analyze comments when ignore-comments is set (#2180) 2024-06-21 20:39:34 +00:00
Casey Rodarmor
e4564f45a3
Don't exit process in run() on argument parse error (#2176) 2024-06-20 03:57:46 +00:00
Casey Rodarmor
aa43a664ee
Document remote justfile workaround (#2175) 2024-06-19 17:18:03 -07:00
Casey Rodarmor
553adc1004
Document library interface (#2174) 2024-06-19 23:38:02 +00:00
Casey Rodarmor
e572b93d84
Allow passing command-line arguments into run() (#2173) 2024-06-19 23:25:36 +00:00
Ryan McGuire
fcac7ee768
Ignore env_logger initialization errors (#2170) 2024-06-19 07:50:37 +00:00
Blair Noctis
71b72c4a53
Remove dependency on cradle (#2169) 2024-06-18 02:42:16 +00:00
Casey Rodarmor
0e8f660d6d
Add datetime() and datetime_utc() functions (#2167) 2024-06-14 22:48:34 -07:00
Casey Rodarmor
1c3c1dd3c0
Add note to readme about quoting paths on Windows (#2166) 2024-06-15 05:32:07 +00:00
Casey Rodarmor
197e1002d0
List recipes by group in group justfile order with just --list --unsorted (#2164) 2024-06-15 03:04:47 +00:00
Casey Rodarmor
4a59769faa
Add missing changelog credits (#2163) 2024-06-14 23:58:31 +00:00
Casey Rodarmor
bf6ec6bf16
Credit myself in changelog (#2162) 2024-06-14 23:42:14 +00:00
Casey Rodarmor
1547af08b5
Allow setting more command-line options with environment variables (#2161) 2024-06-14 23:11:22 +00:00
Casey Rodarmor
b05a75d168
List groups in source order with just --groups --unsorted (#2160) 2024-06-14 20:35:03 +00:00
Casey Rodarmor
5f91b37c82
Release 1.29.1 (#2159)
- Bump version: 1.29.0 → 1.29.1
- Update changelog
- Update changelog contributor credits
2024-06-14 12:44:52 -07:00
Casey Rodarmor
dd9792571b
Fix unexport syntax conflicts (#2158) 2024-06-14 19:39:34 +00:00
Casey Rodarmor
e6c37aacd1
Release 1.29.0 (#2155)
- Bump version: 1.28.0 → 1.29.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
- Update version references in readme
- Fix zsh completion script
2024-06-14 02:57:12 +00:00
Casey Rodarmor
18ec9796b9
Improve argument parsing and error handling for submodules (#2154) 2024-06-14 02:41:45 +00:00
Casey Rodarmor
e1b17fe9cf
Document shell expanded string defaults (#2153) 2024-06-13 21:41:19 +00:00
Casey Rodarmor
4b5ba8f6f5
Load environment file from dotenv-path relative to working directory (#2152) 2024-06-13 20:21:00 +00:00
Casey Rodarmor
1ce7a05bef
Add [positional-arguments] attribute (#2151) 2024-06-13 19:35:14 +00:00
Casey Rodarmor
637023e86f
Test bare bash path in shebang on windows (#2144) 2024-06-13 19:19:22 +00:00
Ruben Nicolaides
4f16428bcb
Use --justfile in Fish shell completions (#2148) 2024-06-12 18:38:37 +00:00
Casey Rodarmor
8778972014
Test shell not found error messages (#2145) 2024-06-11 13:10:32 -07:00
Greg Shuflin
5ac98c020d
Add is_dependency() function (#2139) 2024-06-09 01:17:55 +00:00
Casey Rodarmor
3236154d8d
Fix fzf chooser preview with space-separated module paths (#2141) 2024-06-09 01:01:24 +00:00
Casey Rodarmor
0de971942a
Allow printing nu completion script with just --completions nushell (#2140) 2024-06-08 21:56:21 +00:00
Greg Shuflin
1ca53e8b22
Add [ATTRIBUTE: VALUE] shorthand (#2136) 2024-06-08 18:33:45 +00:00
Greg Shuflin
4fbd03735a
Refactor evaluator (#2138) 2024-06-08 14:42:16 +00:00
Greg Shuflin
38873dcb74
Allow unexporting environment variables (#2098) 2024-06-05 20:16:47 +00:00
Casey Rodarmor
3ddd1b1683
Fix man page generation in release workflow (#2132) 2024-06-05 19:35:06 +00:00
Casey Rodarmor
0eb2a0678c
Release 1.28.0 (#2131)
- Bump version: 1.27.0 → 1.28.0
- Update changelog
- Update changelog contributor credits
- Update dependencies
2024-06-05 19:29:55 +00:00
Casey Rodarmor
3c40c0c6eb
Don't check in manpage (#2130) 2024-06-05 19:17:05 +00:00
Casey Rodarmor
70d1e1b3af
Document default shell (#2129) 2024-06-05 19:06:32 +00:00
Casey Rodarmor
af249dbce1
Write shebang recipes to $XDG_RUNTIME_DIR (#2128) 2024-06-05 19:03:14 +00:00
Potter XU
7c30fb4944
Remove duplicate section in Chinese readme (#2127) 2024-06-05 20:53:36 +02:00
Potter XU
6fe068c432
Update Chinese readme (#2124) 2024-06-04 11:28:29 +02:00
Potter XU
b5ad133b93
Fix typo in readme (#2122) 2024-06-02 19:40:29 +00:00
Casey Rodarmor
8d3d88fc13
Don't check in auto-generated completion scripts (#2120) 2024-06-01 23:26:41 +00:00
Casey Rodarmor
f2201d8684
Add set dotenv-required to require an environment file (#2116) 2024-05-30 23:12:07 +00:00
Casey Rodarmor
d38c1add13
Allow listing recipes in submodules with --list-submodules (#2113) 2024-05-30 12:28:54 -05:00
Casey Rodarmor
d2b10e04d3
Use space-separated recipe paths in --choose (#2115) 2024-05-30 17:24:06 +00:00
102 changed files with 4501 additions and 2963 deletions

View File

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

View File

@ -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
View File

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

File diff suppressed because it is too large Load Diff

257
Cargo.lock generated
View File

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

View File

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

View File

@ -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
View File

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

View File

@ -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` 变量

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,403 @@
use super::*;
#[allow(clippy::doc_markdown)]
/// The argument parser is responsible for grouping positional arguments into
/// argument groups, which consist of a path to a recipe and its arguments.
///
/// Argument parsing is substantially complicated by the fact that recipe paths
/// can be given on the command line as multiple arguments, i.e., "foo" "bar"
/// baz", or as a single "::"-separated argument.
///
/// Error messages produced by the argument parser should use the format of the
/// recipe path as passed on the command line.
///
/// Additionally, if a recipe is specified with a "::"-separated path, extra
/// components of that path after a valid recipe must not be used as arguments,
/// whereas arguments after multiple argument path may be used as arguments. As
/// an example, `foo bar baz` may refer to recipe `foo::bar` with argument
/// `baz`, but `foo::bar::baz` is an error, since `bar` is a recipe, not a
/// module.
pub(crate) struct ArgumentParser<'src: 'run, 'run> {
arguments: &'run [&'run str],
next: usize,
root: &'run Justfile<'src>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct ArgumentGroup<'run> {
pub(crate) arguments: Vec<&'run str>,
pub(crate) path: Vec<String>,
}
impl<'src: 'run, 'run> ArgumentParser<'src, 'run> {
pub(crate) fn parse_arguments(
root: &'run Justfile<'src>,
arguments: &'run [&'run str],
) -> RunResult<'src, Vec<ArgumentGroup<'run>>> {
let mut groups = Vec::new();
let mut invocation_parser = Self {
arguments,
next: 0,
root,
};
loop {
groups.push(invocation_parser.parse_group()?);
if invocation_parser.next == arguments.len() {
break;
}
}
Ok(groups)
}
fn parse_group(&mut self) -> RunResult<'src, ArgumentGroup<'run>> {
let (recipe, path) = if let Some(next) = self.next() {
if next.contains(':') {
let module_path =
ModulePath::try_from([next].as_slice()).map_err(|()| Error::UnknownRecipe {
recipe: next.into(),
suggestion: None,
})?;
let (recipe, path, _) = self.resolve_recipe(true, &module_path.path)?;
self.next += 1;
(recipe, path)
} else {
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
self.next += consumed;
(recipe, path)
}
} else {
let (recipe, path, consumed) = self.resolve_recipe(false, self.rest())?;
assert_eq!(consumed, 0);
(recipe, path)
};
let rest = self.rest();
let argument_range = recipe.argument_range();
let argument_count = cmp::min(rest.len(), recipe.max_arguments());
if !argument_range.range_contains(&argument_count) {
return Err(Error::ArgumentCountMismatch {
recipe: recipe.name(),
parameters: recipe.parameters.clone(),
found: rest.len(),
min: recipe.min_arguments(),
max: recipe.max_arguments(),
});
}
let arguments = rest[..argument_count].to_vec();
self.next += argument_count;
Ok(ArgumentGroup { arguments, path })
}
fn resolve_recipe(
&self,
module_path: bool,
args: &[impl AsRef<str>],
) -> RunResult<'src, (&'run Recipe<'src>, Vec<String>, usize)> {
let mut current = self.root;
let mut path = Vec::new();
for (i, arg) in args.iter().enumerate() {
let arg = arg.as_ref();
path.push(arg.to_string());
if let Some(module) = current.modules.get(arg) {
current = module;
} else if let Some(recipe) = current.get_recipe(arg) {
if module_path && i + 1 < args.len() {
return Err(Error::ExpectedSubmoduleButFoundRecipe {
path: if module_path {
path.join("::")
} else {
path.join(" ")
},
});
}
return Ok((recipe, path, i + 1));
} else {
if module_path && i + 1 < args.len() {
return Err(Error::UnknownSubmodule {
path: path.join("::"),
});
}
return Err(Error::UnknownRecipe {
recipe: if module_path {
path.join("::")
} else {
path.join(" ")
},
suggestion: current.suggest_recipe(arg),
});
}
}
if let Some(recipe) = &current.default {
recipe.check_can_be_default_recipe()?;
path.push(recipe.name().into());
Ok((recipe, path, args.len()))
} else if current.recipes.is_empty() {
Err(Error::NoRecipes)
} else {
Err(Error::NoDefaultRecipe)
}
}
fn next(&self) -> Option<&'run str> {
self.arguments.get(self.next).copied()
}
fn rest(&self) -> &[&'run str] {
&self.arguments[self.next..]
}
}
#[cfg(test)]
mod tests {
use {super::*, tempfile::TempDir};
trait TempDirExt {
fn write(&self, path: &str, content: &str);
}
impl TempDirExt for TempDir {
fn write(&self, path: &str, content: &str) {
let path = self.path().join(path);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, content).unwrap();
}
}
#[test]
fn single_no_arguments() {
let justfile = testing::compile("foo:");
assert_eq!(
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap(),
vec![ArgumentGroup {
path: vec!["foo".into()],
arguments: Vec::new()
}],
);
}
#[test]
fn single_with_argument() {
let justfile = testing::compile("foo bar:");
assert_eq!(
ArgumentParser::parse_arguments(&justfile, &["foo", "baz"]).unwrap(),
vec![ArgumentGroup {
path: vec!["foo".into()],
arguments: vec!["baz"],
}],
);
}
#[test]
fn single_argument_count_mismatch() {
let justfile = testing::compile("foo bar:");
assert_matches!(
ArgumentParser::parse_arguments(&justfile, &["foo"]).unwrap_err(),
Error::ArgumentCountMismatch {
recipe: "foo",
found: 0,
min: 1,
max: 1,
..
},
);
}
#[test]
fn single_unknown() {
let justfile = testing::compile("foo:");
assert_matches!(
ArgumentParser::parse_arguments(&justfile, &["bar"]).unwrap_err(),
Error::UnknownRecipe {
recipe,
suggestion: None
} if recipe == "bar",
);
}
#[test]
fn multiple_unknown() {
let justfile = testing::compile("foo:");
assert_matches!(
ArgumentParser::parse_arguments(&justfile, &["bar", "baz"]).unwrap_err(),
Error::UnknownRecipe {
recipe,
suggestion: None
} if recipe == "bar",
);
}
#[test]
fn recipe_in_submodule() {
let loader = Loader::new();
let tempdir = tempfile::tempdir().unwrap();
let path = tempdir.path().join("justfile");
fs::write(&path, "mod foo").unwrap();
fs::create_dir(tempdir.path().join("foo")).unwrap();
fs::write(tempdir.path().join("foo/mod.just"), "bar:").unwrap();
let compilation = Compiler::compile(&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"],
},
],
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
#[derive(Debug, PartialEq)]
use super::*;
#[derive(Debug, PartialEq, Clone, ValueEnum)]
pub(crate) enum DumpFormat {
Json,
Just,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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!(

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
#[derive(Copy, Clone, Debug, PartialEq)]
use super::*;
#[derive(Copy, Clone, Debug, PartialEq, ValueEnum)]
pub(crate) enum UseColor {
Auto,
Always,

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

@ -0,0 +1,27 @@
use super::*;
#[test]
fn datetime() {
Test::new()
.justfile(
"
x := datetime('%Y-%m-%d %z')
",
)
.args(["--eval", "x"])
.stdout_regex(r"\d\d\d\d-\d\d-\d\d [+-]\d\d\d\d")
.run();
}
#[test]
fn datetime_utc() {
Test::new()
.justfile(
"
x := datetime_utc('%Y-%m-%d %Z')
",
)
.args(["--eval", "x"])
.stdout_regex(r"\d\d\d\d-\d\d-\d\d UTC")
.run();
}

View File

@ -12,40 +12,54 @@ fn dotenv() {
.run();
}
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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,31 @@ test! {
"#,
}
test! {
name: linewise_with_attribute,
justfile: r#"
[positional-arguments]
foo bar baz:
echo $0
echo $1
echo $2
echo "$@"
"#,
args: ("foo", "hello", "goodbye"),
stdout: "
foo
hello
goodbye
hello goodbye
",
stderr: r#"
echo $0
echo $1
echo $2
echo "$@"
"#,
}
test! {
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#"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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

View File

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