mirror of
https://github.com/casey/just.git
synced 2024-11-23 02:44:56 +03:00
Gargantuan refactor (#522)
- Instead of changing the current directory with `env::set_current_dir` to be implicitly inherited by subprocesses, we now use `Command::current_dir` to set it explicitly. This feels much better, since we aren't dependent on the implicit state of the process's current directory. - Subcommand execution is much improved. - Added a ton of tests for config parsing, config execution, working dir, and search dir. - Error messages are improved. Many more will be colored. - The Config is now onwed, instead of borrowing from the arguments and the `clap::ArgMatches` object. This is a huge ergonomic improvement, especially in tests, and I don't think anyone will notice. - `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order, matching git, which I think is what most people will expect. - Added a cute `tmptree!{}` macro, for creating temporary directories populated with directories and files for tests. - Admitted that grammer is LL(k) and I don't know what `k` is.
This commit is contained in:
parent
8279361b39
commit
aefdcea7d0
76
Cargo.lock
generated
76
Cargo.lock
generated
@ -38,6 +38,26 @@ dependencies = [
|
|||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.40"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace-sys"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -98,6 +118,11 @@ name = "difference"
|
|||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "doc-comment"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenv"
|
name = "dotenv"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@ -130,6 +155,14 @@ name = "executable-path"
|
|||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "failure"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@ -174,10 +207,12 @@ dependencies = [
|
|||||||
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"test-utilities 0.0.0",
|
"test-utilities 0.0.0",
|
||||||
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"which 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -326,6 +361,30 @@ dependencies = [
|
|||||||
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "snafu-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -415,6 +474,15 @@ name = "wasi"
|
|||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
dependencies = [
|
||||||
|
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -457,6 +525,8 @@ dependencies = [
|
|||||||
"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||||
"checksum assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5"
|
"checksum assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5"
|
||||||
"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
|
"checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90"
|
||||||
|
"checksum backtrace 0.3.40 (registry+https://github.com/rust-lang/crates.io-index)" = "924c76597f0d9ca25d762c25a4d369d51267536465dc5064bdf0eb073ed477ea"
|
||||||
|
"checksum backtrace-sys 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491"
|
||||||
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
|
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
|
||||||
"checksum cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)" = "0213d356d3c4ea2c18c40b037c3be23cd639825c18f25ee670ac7813beeef99c"
|
"checksum cc 1.0.46 (registry+https://github.com/rust-lang/crates.io-index)" = "0213d356d3c4ea2c18c40b037c3be23cd639825c18f25ee670ac7813beeef99c"
|
||||||
@ -465,11 +535,13 @@ dependencies = [
|
|||||||
"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
"checksum ctor 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
|
||||||
"checksum ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c7dfd2d8b4c82121dfdff120f818e09fc4380b0b7e17a742081a89b94853e87f"
|
"checksum ctrlc 3.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c7dfd2d8b4c82121dfdff120f818e09fc4380b0b7e17a742081a89b94853e87f"
|
||||||
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||||
|
"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97"
|
||||||
"checksum dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
"checksum dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||||
"checksum edit-distance 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
|
"checksum edit-distance 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bbbaaaf38131deb9ca518a274a45bfdb8771f139517b073b16c2d3d32ae5037b"
|
||||||
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
"checksum either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3"
|
||||||
"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
|
"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
|
||||||
"checksum executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478"
|
"checksum executable-path 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478"
|
||||||
|
"checksum failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9"
|
||||||
"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571"
|
"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571"
|
||||||
"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
|
"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
|
||||||
"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
|
"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
|
||||||
@ -492,6 +564,9 @@ dependencies = [
|
|||||||
"checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
|
"checksum regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd"
|
||||||
"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
|
"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716"
|
||||||
"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
|
"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
|
||||||
|
"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
|
||||||
|
"checksum snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41207ca11f96a62cd34e6b7fdf73d322b25ae3848eb9d38302169724bb32cf27"
|
||||||
|
"checksum snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5e338c8b0577457c9dda8e794b6ad7231c96e25b1b0dd5842d52249020c1c0"
|
||||||
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
|
"checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf"
|
||||||
"checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a"
|
"checksum target 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "10000465bb0cc031c87a44668991b284fd84c0e6bd945f62d4af04e9e52a222a"
|
||||||
@ -504,6 +579,7 @@ dependencies = [
|
|||||||
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
||||||
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
|
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
|
||||||
|
"checksum which 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5475d47078209a02e60614f7ba5e645ef3ed60f771920ac1906d7c1cc65024c8"
|
||||||
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
|
||||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
|
||||||
|
@ -26,6 +26,7 @@ itertools = "0.8"
|
|||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4.4"
|
log = "0.4.4"
|
||||||
|
snafu = "0.6"
|
||||||
target = "1"
|
target = "1"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
unicode-width = "0.1"
|
unicode-width = "0.1"
|
||||||
@ -37,6 +38,7 @@ features = ["termination"]
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
executable-path = "1"
|
executable-path = "1"
|
||||||
pretty_assertions = "0.6"
|
pretty_assertions = "0.6"
|
||||||
|
which = "3"
|
||||||
|
|
||||||
# Until github.com/rust-lang/cargo/pull/7333 makes it into stable,
|
# Until github.com/rust-lang/cargo/pull/7333 makes it into stable,
|
||||||
# this version-less dev-dependency will interfere with publishing
|
# this version-less dev-dependency will interfere with publishing
|
||||||
|
@ -2,9 +2,8 @@ justfile grammar
|
|||||||
================
|
================
|
||||||
|
|
||||||
Justfiles are processed by a mildly context-sensitive tokenizer
|
Justfiles are processed by a mildly context-sensitive tokenizer
|
||||||
and a recursive descent parser. The grammar is mostly LL(1),
|
and a recursive descent parser. The grammar is LL(k), for an
|
||||||
although an extra token of lookahead is used to distinguish between
|
unknown but hopefully reasonable value of k.
|
||||||
export assignments and recipes with parameters.
|
|
||||||
|
|
||||||
tokens
|
tokens
|
||||||
------
|
------
|
||||||
|
2
justfile
2
justfile
@ -34,7 +34,7 @@ check:
|
|||||||
cargo check
|
cargo check
|
||||||
|
|
||||||
watch +COMMAND='test':
|
watch +COMMAND='test':
|
||||||
cargo watch --clear --exec "{{COMMAND}}"
|
cargo watch --clear --exec build --exec "{{COMMAND}}"
|
||||||
|
|
||||||
man:
|
man:
|
||||||
cargo build --features help4help2man
|
cargo build --features help4help2man
|
||||||
|
@ -2,36 +2,27 @@ use crate::common::*;
|
|||||||
|
|
||||||
pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
|
pub(crate) struct AssignmentEvaluator<'a: 'b, 'b> {
|
||||||
pub(crate) assignments: &'b BTreeMap<&'a str, Assignment<'a>>,
|
pub(crate) assignments: &'b BTreeMap<&'a str, Assignment<'a>>,
|
||||||
pub(crate) invocation_directory: &'b Result<PathBuf, String>,
|
pub(crate) config: &'a Config,
|
||||||
pub(crate) dotenv: &'b BTreeMap<String, String>,
|
pub(crate) dotenv: &'b BTreeMap<String, String>,
|
||||||
pub(crate) dry_run: bool,
|
|
||||||
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
|
pub(crate) evaluated: BTreeMap<&'a str, (bool, String)>,
|
||||||
pub(crate) overrides: &'b BTreeMap<&'b str, &'b str>,
|
|
||||||
pub(crate) quiet: bool,
|
|
||||||
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
|
pub(crate) scope: &'b BTreeMap<&'a str, (bool, String)>,
|
||||||
pub(crate) shell: &'b str,
|
pub(crate) working_directory: &'b Path,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
||||||
pub(crate) fn evaluate_assignments(
|
pub(crate) fn evaluate_assignments(
|
||||||
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
config: &'a Config,
|
||||||
invocation_directory: &Result<PathBuf, String>,
|
working_directory: &'b Path,
|
||||||
dotenv: &'b BTreeMap<String, String>,
|
dotenv: &'b BTreeMap<String, String>,
|
||||||
overrides: &BTreeMap<&str, &str>,
|
assignments: &BTreeMap<&'a str, Assignment<'a>>,
|
||||||
quiet: bool,
|
|
||||||
shell: &'a str,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
|
) -> RunResult<'a, BTreeMap<&'a str, (bool, String)>> {
|
||||||
let mut evaluator = AssignmentEvaluator {
|
let mut evaluator = AssignmentEvaluator {
|
||||||
evaluated: empty(),
|
evaluated: empty(),
|
||||||
scope: &empty(),
|
scope: &empty(),
|
||||||
|
config,
|
||||||
assignments,
|
assignments,
|
||||||
invocation_directory,
|
working_directory,
|
||||||
dotenv,
|
dotenv,
|
||||||
dry_run,
|
|
||||||
overrides,
|
|
||||||
quiet,
|
|
||||||
shell,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for name in assignments.keys() {
|
for name in assignments.keys() {
|
||||||
@ -64,7 +55,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(assignment) = self.assignments.get(name) {
|
if let Some(assignment) = self.assignments.get(name) {
|
||||||
if let Some(value) = self.overrides.get(name) {
|
if let Some(value) = self.config.overrides.get(name) {
|
||||||
self
|
self
|
||||||
.evaluated
|
.evaluated
|
||||||
.insert(name, (assignment.export, value.to_string()));
|
.insert(name, (assignment.export, value.to_string()));
|
||||||
@ -113,14 +104,15 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
.map(|argument| self.evaluate_expression(argument, arguments))
|
.map(|argument| self.evaluate_expression(argument, arguments))
|
||||||
.collect::<Result<Vec<String>, RuntimeError>>()?;
|
.collect::<Result<Vec<String>, RuntimeError>>()?;
|
||||||
let context = FunctionContext {
|
let context = FunctionContext {
|
||||||
invocation_directory: &self.invocation_directory,
|
invocation_directory: &self.config.invocation_directory,
|
||||||
|
working_directory: &self.working_directory,
|
||||||
dotenv: self.dotenv,
|
dotenv: self.dotenv,
|
||||||
};
|
};
|
||||||
Function::evaluate(*function, &context, &call_arguments)
|
Function::evaluate(*function, &context, &call_arguments)
|
||||||
}
|
}
|
||||||
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
|
Expression::StringLiteral { string_literal } => Ok(string_literal.cooked.to_string()),
|
||||||
Expression::Backtick { contents, token } => {
|
Expression::Backtick { contents, token } => {
|
||||||
if self.dry_run {
|
if self.config.dry_run {
|
||||||
Ok(format!("`{}`", contents))
|
Ok(format!("`{}`", contents))
|
||||||
} else {
|
} else {
|
||||||
Ok(self.run_backtick(self.dotenv, contents, token)?)
|
Ok(self.run_backtick(self.dotenv, contents, token)?)
|
||||||
@ -139,7 +131,9 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
raw: &str,
|
raw: &str,
|
||||||
token: &Token<'a>,
|
token: &Token<'a>,
|
||||||
) -> RunResult<'a, String> {
|
) -> RunResult<'a, String> {
|
||||||
let mut cmd = Command::new(self.shell);
|
let mut cmd = Command::new(&self.config.shell);
|
||||||
|
|
||||||
|
cmd.current_dir(self.working_directory);
|
||||||
|
|
||||||
cmd.arg("-cu").arg(raw);
|
cmd.arg("-cu").arg(raw);
|
||||||
|
|
||||||
@ -147,7 +141,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
|
|
||||||
cmd.stdin(process::Stdio::inherit());
|
cmd.stdin(process::Stdio::inherit());
|
||||||
|
|
||||||
cmd.stderr(if self.quiet {
|
cmd.stderr(if self.config.quiet {
|
||||||
process::Stdio::null()
|
process::Stdio::null()
|
||||||
} else {
|
} else {
|
||||||
process::Stdio::inherit()
|
process::Stdio::inherit()
|
||||||
@ -155,7 +149,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
|
|
||||||
InterruptHandler::guard(|| {
|
InterruptHandler::guard(|| {
|
||||||
output(cmd).map_err(|output_error| RuntimeError::Backtick {
|
output(cmd).map_err(|output_error| RuntimeError::Backtick {
|
||||||
token: token.clone(),
|
token: *token,
|
||||||
output_error,
|
output_error,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -165,14 +159,14 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::testing::compile;
|
use crate::testing::{compile, config};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn backtick_code() {
|
fn backtick_code() {
|
||||||
match compile("a:\n echo {{`f() { return 100; }; f`}}")
|
let justfile = compile("a:\n echo {{`f() { return 100; }; f`}}");
|
||||||
.run(&["a"], &Default::default())
|
let config = config(&["a"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
RuntimeError::Backtick {
|
RuntimeError::Backtick {
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(code),
|
output_error: OutputError::Code(code),
|
||||||
@ -193,12 +187,12 @@ b = `echo $exported_variable`
|
|||||||
recipe:
|
recipe:
|
||||||
echo {{b}}
|
echo {{b}}
|
||||||
"#;
|
"#;
|
||||||
let config = Config {
|
|
||||||
quiet: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
match compile(text).run(&["recipe"], &config).unwrap_err() {
|
let justfile = compile(text);
|
||||||
|
let config = config(&["--quiet", "recipe"]);
|
||||||
|
let dir = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
RuntimeError::Backtick {
|
RuntimeError::Backtick {
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(_),
|
output_error: OutputError::Code(_),
|
||||||
|
@ -62,7 +62,7 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
|
|||||||
let token = self.assignments[variable].name.token();
|
let token = self.assignments[variable].name.token();
|
||||||
self.stack.push(variable);
|
self.stack.push(variable);
|
||||||
return Err(token.error(CircularVariableDependency {
|
return Err(token.error(CircularVariableDependency {
|
||||||
variable: variable,
|
variable,
|
||||||
circle: self.stack.clone(),
|
circle: self.stack.clone(),
|
||||||
}));
|
}));
|
||||||
} else if self.assignments.contains_key(variable) {
|
} else if self.assignments.contains_key(variable) {
|
||||||
|
@ -4,7 +4,7 @@ use ansi_term::Color::*;
|
|||||||
use ansi_term::{ANSIGenericString, Prefix, Style, Suffix};
|
use ansi_term::{ANSIGenericString, Prefix, Style, Suffix};
|
||||||
use atty::Stream;
|
use atty::Stream;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub(crate) struct Color {
|
pub(crate) struct Color {
|
||||||
use_color: UseColor,
|
use_color: UseColor,
|
||||||
atty: bool,
|
atty: bool,
|
||||||
@ -128,7 +128,7 @@ impl Color {
|
|||||||
impl Default for Color {
|
impl Default for Color {
|
||||||
fn default() -> Color {
|
fn default() -> Color {
|
||||||
Color {
|
Color {
|
||||||
use_color: UseColor::Never,
|
use_color: UseColor::Auto,
|
||||||
atty: false,
|
atty: false,
|
||||||
style: Style::new(),
|
style: Style::new(),
|
||||||
}
|
}
|
||||||
|
@ -16,18 +16,23 @@ pub(crate) use std::{
|
|||||||
usize, vec,
|
usize, vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// modules used in tests
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use crate::testing;
|
||||||
|
|
||||||
|
// structs and enums used in tests
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use crate::{node::Node, tree::Tree};
|
||||||
|
|
||||||
// dependencies
|
// dependencies
|
||||||
pub(crate) use edit_distance::edit_distance;
|
pub(crate) use edit_distance::edit_distance;
|
||||||
pub(crate) use libc::EXIT_FAILURE;
|
pub(crate) use libc::EXIT_FAILURE;
|
||||||
pub(crate) use log::warn;
|
pub(crate) use log::warn;
|
||||||
|
pub(crate) use snafu::{ResultExt, Snafu};
|
||||||
pub(crate) use unicode_width::UnicodeWidthChar;
|
pub(crate) use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
pub(crate) use crate::{keyword, search};
|
pub(crate) use crate::{config_error, keyword, search_error};
|
||||||
|
|
||||||
// modules used in tests
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) use crate::testing;
|
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
@ -37,8 +42,9 @@ pub(crate) use crate::{
|
|||||||
|
|
||||||
// traits
|
// traits
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
command_ext::CommandExt, compilation_result_ext::CompilationResultExt, keyed::Keyed,
|
command_ext::CommandExt, compilation_result_ext::CompilationResultExt, error::Error,
|
||||||
ordinal::Ordinal, platform_interface::PlatformInterface, range_ext::RangeExt,
|
error_result_ext::ErrorResultExt, keyed::Keyed, ordinal::Ordinal,
|
||||||
|
platform_interface::PlatformInterface, range_ext::RangeExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
// structs and enums
|
// structs and enums
|
||||||
@ -50,20 +56,17 @@ pub(crate) use crate::{
|
|||||||
enclosure::Enclosure, expression::Expression, fragment::Fragment, function::Function,
|
enclosure::Enclosure, expression::Expression, fragment::Fragment, function::Function,
|
||||||
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
function_context::FunctionContext, functions::Functions, interrupt_guard::InterruptGuard,
|
||||||
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
|
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, lexer::Lexer, line::Line,
|
||||||
list::List, module::Module, name::Name, output_error::OutputError, parameter::Parameter,
|
list::List, load_error::LoadError, module::Module, name::Name, output_error::OutputError,
|
||||||
parser::Parser, platform::Platform, position::Position, recipe::Recipe,
|
parameter::Parameter, parser::Parser, platform::Platform, position::Position, recipe::Recipe,
|
||||||
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, runtime_error::RuntimeError,
|
||||||
search_error::SearchError, shebang::Shebang, show_whitespace::ShowWhitespace, state::State,
|
search::Search, search_config::SearchConfig, search_error::SearchError, shebang::Shebang,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, table::Table, token::Token,
|
show_whitespace::ShowWhitespace, state::State, string_literal::StringLiteral,
|
||||||
token_kind::TokenKind, use_color::UseColor, variables::Variables, verbosity::Verbosity,
|
subcommand::Subcommand, table::Table, token::Token, token_kind::TokenKind, use_color::UseColor,
|
||||||
warning::Warning,
|
variables::Variables, verbosity::Verbosity, warning::Warning,
|
||||||
};
|
};
|
||||||
|
|
||||||
// structs and enums used in tests
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) use crate::{node::Node, tree::Tree};
|
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
pub(crate) type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
pub(crate) type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
||||||
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
||||||
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
|
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
|
||||||
|
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
||||||
|
@ -10,13 +10,14 @@ pub(crate) struct CompilationError<'a> {
|
|||||||
pub(crate) kind: CompilationErrorKind<'a>,
|
pub(crate) kind: CompilationErrorKind<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Display for CompilationError<'a> {
|
impl Error for CompilationError<'_> {}
|
||||||
|
|
||||||
|
impl Display for CompilationError<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
use CompilationErrorKind::*;
|
use CompilationErrorKind::*;
|
||||||
let error = Color::fmt(f).error();
|
|
||||||
let message = Color::fmt(f).message();
|
let message = Color::fmt(f).message();
|
||||||
|
|
||||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
write!(f, "{}", message.prefix())?;
|
||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
AliasShadowsRecipe { alias, recipe_line } => {
|
AliasShadowsRecipe { alias, recipe_line } => {
|
||||||
|
@ -3,8 +3,8 @@ use crate::common::*;
|
|||||||
pub(crate) struct Compiler;
|
pub(crate) struct Compiler;
|
||||||
|
|
||||||
impl Compiler {
|
impl Compiler {
|
||||||
pub(crate) fn compile(text: &str) -> CompilationResult<Justfile> {
|
pub(crate) fn compile(src: &str) -> CompilationResult<Justfile> {
|
||||||
let tokens = Lexer::lex(text)?;
|
let tokens = Lexer::lex(src)?;
|
||||||
|
|
||||||
let ast = Parser::parse(&tokens)?;
|
let ast = Parser::parse(&tokens)?;
|
||||||
|
|
||||||
|
770
src/config.rs
770
src/config.rs
@ -1,23 +1,23 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches};
|
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
pub(crate) const DEFAULT_SHELL: &str = "sh";
|
||||||
|
|
||||||
pub(crate) struct Config<'a> {
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) subcommand: Subcommand<'a>,
|
pub(crate) struct Config {
|
||||||
|
pub(crate) arguments: Vec<String>,
|
||||||
|
pub(crate) color: Color,
|
||||||
pub(crate) dry_run: bool,
|
pub(crate) dry_run: bool,
|
||||||
pub(crate) highlight: bool,
|
pub(crate) highlight: bool,
|
||||||
pub(crate) overrides: BTreeMap<&'a str, &'a str>,
|
pub(crate) invocation_directory: PathBuf,
|
||||||
|
pub(crate) overrides: BTreeMap<String, String>,
|
||||||
pub(crate) quiet: bool,
|
pub(crate) quiet: bool,
|
||||||
pub(crate) shell: &'a str,
|
pub(crate) search_config: SearchConfig,
|
||||||
pub(crate) color: Color,
|
pub(crate) shell: String,
|
||||||
|
pub(crate) subcommand: Subcommand,
|
||||||
pub(crate) verbosity: Verbosity,
|
pub(crate) verbosity: Verbosity,
|
||||||
pub(crate) arguments: Vec<&'a str>,
|
|
||||||
pub(crate) justfile: Option<&'a Path>,
|
|
||||||
pub(crate) working_directory: Option<&'a Path>,
|
|
||||||
pub(crate) invocation_directory: Result<PathBuf, String>,
|
|
||||||
pub(crate) search_directory: Option<&'a Path>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mod cmd {
|
mod cmd {
|
||||||
@ -48,7 +48,7 @@ mod arg {
|
|||||||
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
pub(crate) const COLOR_VALUES: &[&str] = &[COLOR_AUTO, COLOR_ALWAYS, COLOR_NEVER];
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Config<'a> {
|
impl Config {
|
||||||
pub(crate) fn app() -> App<'static, 'static> {
|
pub(crate) fn app() -> App<'static, 'static> {
|
||||||
let app = App::new(env!("CARGO_PKG_NAME"))
|
let app = App::new(env!("CARGO_PKG_NAME"))
|
||||||
.help_message("Print help information")
|
.help_message("Print help information")
|
||||||
@ -83,7 +83,7 @@ impl<'a> Config<'a> {
|
|||||||
Arg::with_name(cmd::EDIT)
|
Arg::with_name(cmd::EDIT)
|
||||||
.short("e")
|
.short("e")
|
||||||
.long("edit")
|
.long("edit")
|
||||||
.help("Open justfile with $EDITOR"),
|
.help("Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name(cmd::EVALUATE)
|
Arg::with_name(cmd::EVALUATE)
|
||||||
@ -205,9 +205,8 @@ impl<'a> Config<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_matches(matches: &'a ArgMatches<'a>) -> ConfigResult<Config<'a>> {
|
pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult<Config> {
|
||||||
let invocation_directory =
|
let invocation_directory = env::current_dir().context(config_error::CurrentDir)?;
|
||||||
env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
|
|
||||||
|
|
||||||
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE));
|
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE));
|
||||||
|
|
||||||
@ -217,12 +216,33 @@ impl<'a> Config<'a> {
|
|||||||
.expect("`--color` had no value"),
|
.expect("`--color` had no value"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let subcommand = if matches.is_present(cmd::EDIT) {
|
||||||
|
Subcommand::Edit
|
||||||
|
} else if matches.is_present(cmd::SUMMARY) {
|
||||||
|
Subcommand::Summary
|
||||||
|
} else if matches.is_present(cmd::DUMP) {
|
||||||
|
Subcommand::Dump
|
||||||
|
} else if matches.is_present(cmd::LIST) {
|
||||||
|
Subcommand::List
|
||||||
|
} else if matches.is_present(cmd::EVALUATE) {
|
||||||
|
Subcommand::Evaluate
|
||||||
|
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
||||||
|
Subcommand::Show {
|
||||||
|
name: name.to_owned(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Subcommand::Run
|
||||||
|
};
|
||||||
|
|
||||||
let set_count = matches.occurrences_of(arg::SET);
|
let set_count = matches.occurrences_of(arg::SET);
|
||||||
let mut overrides = BTreeMap::new();
|
let mut overrides = BTreeMap::new();
|
||||||
if set_count > 0 {
|
if set_count > 0 {
|
||||||
let mut values = matches.values_of(arg::SET).unwrap();
|
let mut values = matches.values_of(arg::SET).unwrap();
|
||||||
for _ in 0..set_count {
|
for _ in 0..set_count {
|
||||||
overrides.insert(values.next().unwrap(), values.next().unwrap());
|
overrides.insert(
|
||||||
|
values.next().unwrap().to_owned(),
|
||||||
|
values.next().unwrap().to_owned(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,8 +263,8 @@ impl<'a> Config<'a> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.0;
|
.0;
|
||||||
|
|
||||||
let name = &argument[..i];
|
let name = argument[..i].to_owned();
|
||||||
let value = &argument[i + 1..];
|
let value = argument[i + 1..].to_owned();
|
||||||
|
|
||||||
overrides.insert(name, value);
|
overrides.insert(name, value);
|
||||||
}
|
}
|
||||||
@ -258,13 +278,9 @@ impl<'a> Config<'a> {
|
|||||||
.flat_map(|(i, argument)| {
|
.flat_map(|(i, argument)| {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
if let Some(i) = argument.rfind('/') {
|
if let Some(i) = argument.rfind('/') {
|
||||||
if matches.is_present(arg::WORKING_DIRECTORY) {
|
|
||||||
die!("--working-directory and a path prefixed recipe may not be used together.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (dir, recipe) = argument.split_at(i + 1);
|
let (dir, recipe) = argument.split_at(i + 1);
|
||||||
|
|
||||||
search_directory = Some(Path::new(dir));
|
search_directory = Some(PathBuf::from(dir));
|
||||||
|
|
||||||
if recipe.is_empty() {
|
if recipe.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@ -276,32 +292,43 @@ impl<'a> Config<'a> {
|
|||||||
|
|
||||||
Some(argument)
|
Some(argument)
|
||||||
})
|
})
|
||||||
.collect::<Vec<&str>>();
|
.map(|argument| argument.to_owned())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let subcommand = if matches.is_present(cmd::EDIT) {
|
let search_config = {
|
||||||
Subcommand::Edit
|
let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from);
|
||||||
} else if matches.is_present(cmd::SUMMARY) {
|
let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from);
|
||||||
Subcommand::Summary
|
|
||||||
} else if matches.is_present(cmd::DUMP) {
|
if let Some(search_directory) = search_directory {
|
||||||
Subcommand::Dump
|
if justfile.is_some() || working_directory.is_some() {
|
||||||
} else if matches.is_present(cmd::LIST) {
|
return Err(ConfigError::SearchDirConflict);
|
||||||
Subcommand::List
|
}
|
||||||
} else if matches.is_present(cmd::EVALUATE) {
|
SearchConfig::FromSearchDirectory { search_directory }
|
||||||
Subcommand::Evaluate
|
} else {
|
||||||
} else if let Some(name) = matches.value_of(cmd::SHOW) {
|
match (justfile, working_directory) {
|
||||||
Subcommand::Show { name }
|
(None, None) => SearchConfig::FromInvocationDirectory,
|
||||||
} else {
|
(Some(justfile), None) => SearchConfig::WithJustfile { justfile },
|
||||||
Subcommand::Execute
|
(Some(justfile), Some(working_directory)) => {
|
||||||
|
SearchConfig::WithJustfileAndWorkingDirectory {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(_)) => {
|
||||||
|
return Err(ConfigError::internal(
|
||||||
|
"--working-directory set without --justfile",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
dry_run: matches.is_present(arg::DRY_RUN),
|
dry_run: matches.is_present(arg::DRY_RUN),
|
||||||
highlight: !matches.is_present(arg::NO_HIGHLIGHT),
|
highlight: !matches.is_present(arg::NO_HIGHLIGHT),
|
||||||
quiet: matches.is_present(arg::QUIET),
|
quiet: matches.is_present(arg::QUIET),
|
||||||
shell: matches.value_of(arg::SHELL).unwrap(),
|
shell: matches.value_of(arg::SHELL).unwrap().to_owned(),
|
||||||
justfile: matches.value_of(arg::JUSTFILE).map(Path::new),
|
search_config,
|
||||||
working_directory: matches.value_of(arg::WORKING_DIRECTORY).map(Path::new),
|
|
||||||
search_directory,
|
|
||||||
invocation_directory,
|
invocation_directory,
|
||||||
subcommand,
|
subcommand,
|
||||||
verbosity,
|
verbosity,
|
||||||
@ -310,26 +337,213 @@ impl<'a> Config<'a> {
|
|||||||
arguments,
|
arguments,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Default for Config<'a> {
|
pub(crate) fn run_subcommand(self) -> Result<(), i32> {
|
||||||
fn default() -> Config<'static> {
|
use Subcommand::*;
|
||||||
Config {
|
|
||||||
subcommand: Subcommand::Execute,
|
let search =
|
||||||
dry_run: false,
|
Search::search(&self.search_config, &self.invocation_directory).eprint(self.color)?;
|
||||||
highlight: false,
|
|
||||||
overrides: empty(),
|
if self.subcommand == Edit {
|
||||||
arguments: empty(),
|
return self.edit(&search);
|
||||||
quiet: false,
|
|
||||||
shell: DEFAULT_SHELL,
|
|
||||||
color: default(),
|
|
||||||
verbosity: Verbosity::from_flag_occurrences(0),
|
|
||||||
justfile: None,
|
|
||||||
working_directory: None,
|
|
||||||
invocation_directory: env::current_dir()
|
|
||||||
.map_err(|e| format!("Error getting current directory: {}", e)),
|
|
||||||
search_directory: None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let src = fs::read_to_string(&search.justfile)
|
||||||
|
.map_err(|io_error| LoadError {
|
||||||
|
io_error,
|
||||||
|
path: &search.justfile,
|
||||||
|
})
|
||||||
|
.eprint(self.color)?;
|
||||||
|
|
||||||
|
let justfile = Compiler::compile(&src).eprint(self.color)?;
|
||||||
|
|
||||||
|
for warning in &justfile.warnings {
|
||||||
|
if self.color.stderr().active() {
|
||||||
|
eprintln!("{:#}", warning);
|
||||||
|
} else {
|
||||||
|
eprintln!("{}", warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.subcommand {
|
||||||
|
Dump => self.dump(justfile),
|
||||||
|
Run | Evaluate => self.run(justfile, &search.working_directory),
|
||||||
|
List => self.list(justfile),
|
||||||
|
Show { ref name } => self.show(&name, justfile),
|
||||||
|
Summary => self.summary(justfile),
|
||||||
|
Edit => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dump(&self, justfile: Justfile) -> Result<(), i32> {
|
||||||
|
println!("{}", justfile);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> {
|
||||||
|
let editor = env::var_os("VISUAL")
|
||||||
|
.or_else(|| env::var_os("EDITOR"))
|
||||||
|
.unwrap_or_else(|| "vim".into());
|
||||||
|
|
||||||
|
let error = Command::new(&editor)
|
||||||
|
.current_dir(&search.working_directory)
|
||||||
|
.arg(&search.justfile)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match error {
|
||||||
|
Ok(status) => {
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
eprintln!("Editor `{}` failed: {}", editor.to_string_lossy(), status);
|
||||||
|
Err(status.code().unwrap_or(EXIT_FAILURE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!(
|
||||||
|
"Editor `{}` invocation failed: {}",
|
||||||
|
editor.to_string_lossy(),
|
||||||
|
error
|
||||||
|
);
|
||||||
|
Err(EXIT_FAILURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, justfile: Justfile) -> Result<(), i32> {
|
||||||
|
// Construct a target to alias map.
|
||||||
|
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
||||||
|
for alias in justfile.aliases.values() {
|
||||||
|
if alias.is_private() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recipe_aliases.contains_key(alias.target.lexeme()) {
|
||||||
|
recipe_aliases.insert(alias.target.lexeme(), vec![alias.name.lexeme()]);
|
||||||
|
} else {
|
||||||
|
let aliases = recipe_aliases.get_mut(alias.target.lexeme()).unwrap();
|
||||||
|
aliases.push(alias.name.lexeme());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new();
|
||||||
|
|
||||||
|
for (name, recipe) in &justfile.recipes {
|
||||||
|
if recipe.private {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
||||||
|
let mut line_width = UnicodeWidthStr::width(*name);
|
||||||
|
|
||||||
|
for parameter in &recipe.parameters {
|
||||||
|
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if line_width <= 30 {
|
||||||
|
line_widths.insert(name, line_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
||||||
|
|
||||||
|
let doc_color = self.color.stdout().doc();
|
||||||
|
println!("Available recipes:");
|
||||||
|
|
||||||
|
for (name, recipe) in &justfile.recipes {
|
||||||
|
if recipe.private {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alias_doc = format!("alias for `{}`", recipe.name);
|
||||||
|
|
||||||
|
for (i, name) in iter::once(name)
|
||||||
|
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
print!(" {}", name);
|
||||||
|
for parameter in &recipe.parameters {
|
||||||
|
if self.color.stdout().active() {
|
||||||
|
print!(" {:#}", parameter);
|
||||||
|
} else {
|
||||||
|
print!(" {}", parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declaring this outside of the nested loops will probably be more efficient, but
|
||||||
|
// it creates all sorts of lifetime issues with variables inside the loops.
|
||||||
|
// If this is inlined like the docs say, it shouldn't make any difference.
|
||||||
|
let print_doc = |doc| {
|
||||||
|
print!(
|
||||||
|
" {:padding$}{} {}",
|
||||||
|
"",
|
||||||
|
doc_color.paint("#"),
|
||||||
|
doc_color.paint(doc),
|
||||||
|
padding = max_line_width
|
||||||
|
.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
match (i, recipe.doc) {
|
||||||
|
(0, Some(doc)) => print_doc(doc),
|
||||||
|
(0, None) => (),
|
||||||
|
_ => print_doc(&alias_doc),
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&self, justfile: Justfile, working_directory: &Path) -> Result<(), i32> {
|
||||||
|
if let Err(error) = InterruptHandler::install() {
|
||||||
|
warn!("Failed to set CTRL-C handler: {}", error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = justfile.run(&self, working_directory);
|
||||||
|
|
||||||
|
if !self.quiet {
|
||||||
|
result.eprint(self.color)
|
||||||
|
} else {
|
||||||
|
result.map_err(|err| err.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show(&self, name: &str, justfile: Justfile) -> Result<(), i32> {
|
||||||
|
if let Some(alias) = justfile.get_alias(name) {
|
||||||
|
let recipe = justfile.get_recipe(alias.target.lexeme()).unwrap();
|
||||||
|
println!("{}", alias);
|
||||||
|
println!("{}", recipe);
|
||||||
|
Ok(())
|
||||||
|
} else if let Some(recipe) = justfile.get_recipe(name) {
|
||||||
|
println!("{}", recipe);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
eprintln!("Justfile does not contain recipe `{}`.", name);
|
||||||
|
if let Some(suggestion) = justfile.suggest(name) {
|
||||||
|
eprintln!("Did you mean `{}`?", suggestion);
|
||||||
|
}
|
||||||
|
Err(EXIT_FAILURE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary(&self, justfile: Justfile) -> Result<(), i32> {
|
||||||
|
if justfile.count() == 0 {
|
||||||
|
eprintln!("Justfile contains no recipes.");
|
||||||
|
} else {
|
||||||
|
let summary = justfile
|
||||||
|
.recipes
|
||||||
|
.iter()
|
||||||
|
.filter(|&(_, recipe)| !recipe.private)
|
||||||
|
.map(|(name, _)| name)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
println!("{}", summary);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,7 +567,8 @@ USAGE:
|
|||||||
FLAGS:
|
FLAGS:
|
||||||
--dry-run Print what just would do without doing it
|
--dry-run Print what just would do without doing it
|
||||||
--dump Print entire justfile
|
--dump Print entire justfile
|
||||||
-e, --edit Open justfile with $EDITOR
|
-e, --edit \
|
||||||
|
Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`
|
||||||
--evaluate Print evaluated variables
|
--evaluate Print evaluated variables
|
||||||
--highlight Highlight echoed recipe lines in bold
|
--highlight Highlight echoed recipe lines in bold
|
||||||
-l, --list List available recipes and their arguments
|
-l, --list List available recipes and their arguments
|
||||||
@ -384,4 +599,437 @@ ARGS:
|
|||||||
|
|
||||||
assert_eq!(help, EXPECTED_HELP);
|
assert_eq!(help, EXPECTED_HELP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
args: [$($arg:expr),*],
|
||||||
|
$(arguments: $arguments:expr,)?
|
||||||
|
$(color: $color:expr,)?
|
||||||
|
$(dry_run: $dry_run:expr,)?
|
||||||
|
$(highlight: $highlight:expr,)?
|
||||||
|
$(overrides: $overrides:expr,)?
|
||||||
|
$(quiet: $quiet:expr,)?
|
||||||
|
$(search_config: $search_config:expr,)?
|
||||||
|
$(shell: $shell:expr,)?
|
||||||
|
$(subcommand: $subcommand:expr,)?
|
||||||
|
$(verbosity: $verbosity:expr,)?
|
||||||
|
} => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let arguments = &[
|
||||||
|
"just",
|
||||||
|
$($arg,)*
|
||||||
|
];
|
||||||
|
|
||||||
|
let want = Config {
|
||||||
|
$(arguments: $arguments.iter().map(|argument| argument.to_string()).collect(),)?
|
||||||
|
$(color: $color,)?
|
||||||
|
$(dry_run: $dry_run,)?
|
||||||
|
$(highlight: $highlight,)?
|
||||||
|
$(
|
||||||
|
overrides: $overrides.iter().cloned()
|
||||||
|
.map(|(key, value): (&str, &str)| (key.to_owned(), value.to_owned())).collect(),
|
||||||
|
)?
|
||||||
|
$(quiet: $quiet,)?
|
||||||
|
$(search_config: $search_config,)?
|
||||||
|
$(shell: $shell.to_string(),)?
|
||||||
|
$(subcommand: $subcommand,)?
|
||||||
|
$(verbosity: $verbosity,)?
|
||||||
|
..testing::config(&[])
|
||||||
|
};
|
||||||
|
|
||||||
|
test(arguments, want);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test(arguments: &[&str], want: Config) {
|
||||||
|
let app = Config::app();
|
||||||
|
let matches = app
|
||||||
|
.get_matches_from_safe(arguments)
|
||||||
|
.expect("agument parsing failed");
|
||||||
|
let have = Config::from_matches(&matches).expect("config parsing failed");
|
||||||
|
assert_eq!(have, want);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! error {
|
||||||
|
{
|
||||||
|
name: $name:ident,
|
||||||
|
args: [$($arg:expr),*],
|
||||||
|
} => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let arguments = &[
|
||||||
|
"just",
|
||||||
|
$($arg,)*
|
||||||
|
];
|
||||||
|
|
||||||
|
error(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(arguments: &[&str]) {
|
||||||
|
let app = Config::app();
|
||||||
|
if let Ok(matches) = app.get_matches_from_safe(arguments) {
|
||||||
|
Config::from_matches(&matches).expect_err("config parsing unexpectedly succeeded");
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: default_config,
|
||||||
|
args: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: color_default,
|
||||||
|
args: [],
|
||||||
|
color: Color::auto(),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: color_never,
|
||||||
|
args: ["--color", "never"],
|
||||||
|
color: Color::never(),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: color_always,
|
||||||
|
args: ["--color", "always"],
|
||||||
|
color: Color::always(),
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: color_auto,
|
||||||
|
args: ["--color", "auto"],
|
||||||
|
color: Color::auto(),
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: color_bad_value,
|
||||||
|
args: ["--color", "foo"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: dry_run_default,
|
||||||
|
args: [],
|
||||||
|
dry_run: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: dry_run_true,
|
||||||
|
args: ["--dry-run"],
|
||||||
|
dry_run: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: dry_run_quiet,
|
||||||
|
args: ["--dry-run", "--quiet"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_default,
|
||||||
|
args: [],
|
||||||
|
highlight: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_yes,
|
||||||
|
args: ["--highlight"],
|
||||||
|
highlight: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_no,
|
||||||
|
args: ["--no-highlight"],
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_no_yes,
|
||||||
|
args: ["--no-highlight", "--highlight"],
|
||||||
|
highlight: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_no_yes_no,
|
||||||
|
args: ["--no-highlight", "--highlight", "--no-highlight"],
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: highlight_yes_no,
|
||||||
|
args: ["--highlight", "--no-highlight"],
|
||||||
|
highlight: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: quiet_default,
|
||||||
|
args: [],
|
||||||
|
quiet: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: quiet_long,
|
||||||
|
args: ["--quiet"],
|
||||||
|
quiet: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: quiet_short,
|
||||||
|
args: ["-q"],
|
||||||
|
quiet: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: set_default,
|
||||||
|
args: [],
|
||||||
|
overrides: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: set_one,
|
||||||
|
args: ["--set", "foo", "bar"],
|
||||||
|
overrides: [("foo", "bar")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: set_empty,
|
||||||
|
args: ["--set", "foo", ""],
|
||||||
|
overrides: [("foo", "")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: set_two,
|
||||||
|
args: ["--set", "foo", "bar", "--set", "bar", "baz"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "baz")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: set_override,
|
||||||
|
args: ["--set", "foo", "bar", "--set", "foo", "baz"],
|
||||||
|
overrides: [("foo", "baz")],
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: set_bad,
|
||||||
|
args: ["--set", "foo"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: shell_default,
|
||||||
|
args: [],
|
||||||
|
shell: "sh",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: shell_set,
|
||||||
|
args: ["--shell", "tclsh"],
|
||||||
|
shell: "tclsh",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: verbosity_default,
|
||||||
|
args: [],
|
||||||
|
verbosity: Verbosity::Taciturn,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: verbosity_long,
|
||||||
|
args: ["--verbose"],
|
||||||
|
verbosity: Verbosity::Loquacious,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: verbosity_loquacious,
|
||||||
|
args: ["-v"],
|
||||||
|
verbosity: Verbosity::Loquacious,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: verbosity_grandiloquent,
|
||||||
|
args: ["-v", "-v"],
|
||||||
|
verbosity: Verbosity::Grandiloquent,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: verbosity_great_grandiloquent,
|
||||||
|
args: ["-v", "-v", "-v"],
|
||||||
|
verbosity: Verbosity::Grandiloquent,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_default,
|
||||||
|
args: [],
|
||||||
|
subcommand: Subcommand::Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_dump,
|
||||||
|
args: ["--dump"],
|
||||||
|
subcommand: Subcommand::Dump,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_edit,
|
||||||
|
args: ["--edit"],
|
||||||
|
subcommand: Subcommand::Edit,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_evaluate,
|
||||||
|
args: ["--evaluate"],
|
||||||
|
subcommand: Subcommand::Evaluate,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_list_long,
|
||||||
|
args: ["--list"],
|
||||||
|
subcommand: Subcommand::List,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_list_short,
|
||||||
|
args: ["-l"],
|
||||||
|
subcommand: Subcommand::List,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_show_long,
|
||||||
|
args: ["--show", "build"],
|
||||||
|
subcommand: Subcommand::Show { name: String::from("build") },
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_show_short,
|
||||||
|
args: ["-s", "build"],
|
||||||
|
subcommand: Subcommand::Show { name: String::from("build") },
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: subcommand_show_no_arg,
|
||||||
|
args: ["--show"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: subcommand_summary,
|
||||||
|
args: ["--summary"],
|
||||||
|
subcommand: Subcommand::Summary,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: arguments,
|
||||||
|
args: ["foo", "bar"],
|
||||||
|
arguments: ["foo", "bar"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: arguments_leading_equals,
|
||||||
|
args: ["=foo"],
|
||||||
|
arguments: ["=foo"],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: overrides,
|
||||||
|
args: ["foo=bar", "bar=baz"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "baz")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: overrides_empty,
|
||||||
|
args: ["foo=", "bar="],
|
||||||
|
overrides: [("foo", ""), ("bar", "")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: overrides_override_sets,
|
||||||
|
args: ["--set", "foo", "0", "--set", "bar", "1", "foo=bar", "bar=baz"],
|
||||||
|
overrides: [("foo", "bar"), ("bar", "baz")],
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_default,
|
||||||
|
args: [],
|
||||||
|
search_config: SearchConfig::FromInvocationDirectory,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_from_working_directory_and_justfile,
|
||||||
|
args: ["--working-directory", "foo", "--justfile", "bar"],
|
||||||
|
search_config: SearchConfig::WithJustfileAndWorkingDirectory {
|
||||||
|
justfile: PathBuf::from("bar"),
|
||||||
|
working_directory: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_justfile_long,
|
||||||
|
args: ["--justfile", "foo"],
|
||||||
|
search_config: SearchConfig::WithJustfile {
|
||||||
|
justfile: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_config_justfile_short,
|
||||||
|
args: ["-f", "foo"],
|
||||||
|
search_config: SearchConfig::WithJustfile {
|
||||||
|
justfile: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_parent,
|
||||||
|
args: ["../"],
|
||||||
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
|
search_directory: PathBuf::from(".."),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_parent_with_recipe,
|
||||||
|
args: ["../build"],
|
||||||
|
arguments: ["build"],
|
||||||
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
|
search_directory: PathBuf::from(".."),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_child,
|
||||||
|
args: ["foo/"],
|
||||||
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
|
search_directory: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_deep,
|
||||||
|
args: ["foo/bar/"],
|
||||||
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
|
search_directory: PathBuf::from("foo/bar"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: search_directory_child_with_recipe,
|
||||||
|
args: ["foo/build"],
|
||||||
|
arguments: ["build"],
|
||||||
|
search_config: SearchConfig::FromSearchDirectory {
|
||||||
|
search_directory: PathBuf::from("foo"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: search_directory_conflict_justfile,
|
||||||
|
args: ["--justfile", "bar", "foo/build"],
|
||||||
|
}
|
||||||
|
|
||||||
|
error! {
|
||||||
|
name: search_directory_conflict_working_directory,
|
||||||
|
args: ["--justfile", "bar", "--working-directory", "baz", "foo/build"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,30 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub(crate) enum ConfigError {
|
pub(crate) enum ConfigError {
|
||||||
|
#[snafu(display(
|
||||||
|
"Internal config error, this may indicate a bug in just: {} \
|
||||||
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
|
message
|
||||||
|
))]
|
||||||
Internal { message: String },
|
Internal { message: String },
|
||||||
|
#[snafu(display("Could not canonicalize justfile path `{}`: {}", path.display(), source))]
|
||||||
|
JustfilePathCanonicalize { path: PathBuf, source: io::Error },
|
||||||
|
#[snafu(display("Failed to get current directory: {}", source))]
|
||||||
|
CurrentDir { source: io::Error },
|
||||||
|
#[snafu(display(
|
||||||
|
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
|
||||||
|
))]
|
||||||
|
SearchDirConflict,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ConfigError {
|
impl ConfigError {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
pub(crate) fn internal(message: impl Into<String>) -> ConfigError {
|
||||||
use ConfigError::*;
|
ConfigError::Internal {
|
||||||
|
message: message.into(),
|
||||||
match self {
|
|
||||||
Internal { message } => write!(
|
|
||||||
f,
|
|
||||||
"Internal config error, this may indicate a bug in just: {} \
|
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
|
||||||
message
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Error for ConfigError {}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
macro_rules! die {
|
|
||||||
($($arg:tt)*) => {{
|
|
||||||
eprintln!($($arg)*);
|
|
||||||
std::process::exit(EXIT_FAILURE)
|
|
||||||
}};
|
|
||||||
}
|
|
7
src/error.rs
Normal file
7
src/error.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) trait Error: Display {
|
||||||
|
fn code(&self) -> i32 {
|
||||||
|
EXIT_FAILURE
|
||||||
|
}
|
||||||
|
}
|
22
src/error_result_ext.rs
Normal file
22
src/error_result_ext.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) trait ErrorResultExt<T> {
|
||||||
|
fn eprint(self, color: Color) -> Result<T, i32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E: Error> ErrorResultExt<T> for Result<T, E> {
|
||||||
|
fn eprint(self, color: Color) -> Result<T, i32> {
|
||||||
|
match self {
|
||||||
|
Ok(ok) => Ok(ok),
|
||||||
|
Err(error) => {
|
||||||
|
if color.stderr().active() {
|
||||||
|
eprintln!("{} {:#}", color.error().paint("error:"), error);
|
||||||
|
} else {
|
||||||
|
eprintln!("error: {}", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(error.code())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -107,9 +107,8 @@ pub(crate) fn os_family(_context: &FunctionContext) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
|
pub(crate) fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
|
||||||
context.invocation_directory.clone().and_then(|s| {
|
Platform::to_shell_path(context.working_directory, context.invocation_directory)
|
||||||
Platform::to_shell_path(&s).map_err(|e| format!("Error getting shell path: {}", e))
|
.map_err(|e| format!("Error getting shell path: {}", e))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
pub(crate) fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
pub(crate) struct FunctionContext<'a> {
|
pub(crate) struct FunctionContext<'a> {
|
||||||
pub(crate) invocation_directory: &'a Result<PathBuf, String>,
|
pub(crate) invocation_directory: &'a Path,
|
||||||
|
pub(crate) working_directory: &'a Path,
|
||||||
pub(crate) dotenv: &'a BTreeMap<String, String>,
|
pub(crate) dotenv: &'a BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,15 @@ impl InterruptHandler {
|
|||||||
|
|
||||||
match INSTANCE.lock() {
|
match INSTANCE.lock() {
|
||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
Err(poison_error) => die!(
|
Err(poison_error) => {
|
||||||
"{}",
|
eprintln!(
|
||||||
RuntimeError::Internal {
|
"{}",
|
||||||
message: format!("interrupt handler mutex poisoned: {}", poison_error),
|
RuntimeError::Internal {
|
||||||
}
|
message: format!("interrupt handler mutex poisoned: {}", poison_error),
|
||||||
),
|
}
|
||||||
|
);
|
||||||
|
std::process::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,13 +56,14 @@ impl InterruptHandler {
|
|||||||
|
|
||||||
pub(crate) fn unblock(&mut self) {
|
pub(crate) fn unblock(&mut self) {
|
||||||
if self.blocks == 0 {
|
if self.blocks == 0 {
|
||||||
die!(
|
eprintln!(
|
||||||
"{}",
|
"{}",
|
||||||
RuntimeError::Internal {
|
RuntimeError::Internal {
|
||||||
message: "attempted to unblock interrupt handler, but handler was not blocked"
|
message: "attempted to unblock interrupt handler, but handler was not blocked"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
std::process::exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.blocks -= 1;
|
self.blocks -= 1;
|
||||||
|
168
src/justfile.rs
168
src/justfile.rs
@ -42,13 +42,38 @@ impl<'a> Justfile<'a> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(&'a self, arguments: &[&'a str], config: &'a Config<'a>) -> RunResult<'a, ()> {
|
pub(crate) fn run(
|
||||||
|
&'a self,
|
||||||
|
config: &'a Config,
|
||||||
|
working_directory: &'a Path,
|
||||||
|
) -> RunResult<'a, ()> {
|
||||||
|
let argvec: Vec<&str> = if !config.arguments.is_empty() {
|
||||||
|
config
|
||||||
|
.arguments
|
||||||
|
.iter()
|
||||||
|
.map(|argument| argument.as_str())
|
||||||
|
.collect()
|
||||||
|
} else if let Some(recipe) = self.first() {
|
||||||
|
let min_arguments = recipe.min_arguments();
|
||||||
|
if min_arguments > 0 {
|
||||||
|
return Err(RuntimeError::DefaultRecipeRequiresArguments {
|
||||||
|
recipe: recipe.name.lexeme(),
|
||||||
|
min_arguments,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
vec![recipe.name()]
|
||||||
|
} else {
|
||||||
|
return Err(RuntimeError::NoRecipes);
|
||||||
|
};
|
||||||
|
|
||||||
|
let arguments = argvec.as_slice();
|
||||||
|
|
||||||
let unknown_overrides = config
|
let unknown_overrides = config
|
||||||
.overrides
|
.overrides
|
||||||
.keys()
|
.keys()
|
||||||
.cloned()
|
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||||
.filter(|name| !self.assignments.contains_key(name))
|
.map(|name| name.as_str())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<&str>>();
|
||||||
|
|
||||||
if !unknown_overrides.is_empty() {
|
if !unknown_overrides.is_empty() {
|
||||||
return Err(RuntimeError::UnknownOverrides {
|
return Err(RuntimeError::UnknownOverrides {
|
||||||
@ -59,13 +84,10 @@ impl<'a> Justfile<'a> {
|
|||||||
let dotenv = load_dotenv()?;
|
let dotenv = load_dotenv()?;
|
||||||
|
|
||||||
let scope = AssignmentEvaluator::evaluate_assignments(
|
let scope = AssignmentEvaluator::evaluate_assignments(
|
||||||
&self.assignments,
|
config,
|
||||||
&config.invocation_directory,
|
working_directory,
|
||||||
&dotenv,
|
&dotenv,
|
||||||
&config.overrides,
|
&self.assignments,
|
||||||
config.quiet,
|
|
||||||
config.shell,
|
|
||||||
config.dry_run,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if config.subcommand == Subcommand::Evaluate {
|
if config.subcommand == Subcommand::Evaluate {
|
||||||
@ -121,7 +143,11 @@ impl<'a> Justfile<'a> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = RecipeContext { config, scope };
|
let context = RecipeContext {
|
||||||
|
config,
|
||||||
|
scope,
|
||||||
|
working_directory,
|
||||||
|
};
|
||||||
|
|
||||||
let mut ran = empty();
|
let mut ran = empty();
|
||||||
for (recipe, arguments) in grouped {
|
for (recipe, arguments) in grouped {
|
||||||
@ -201,14 +227,15 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use crate::runtime_error::RuntimeError::*;
|
use crate::runtime_error::RuntimeError::*;
|
||||||
use crate::testing::compile;
|
use crate::testing::{compile, config};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_recipes() {
|
fn unknown_recipes() {
|
||||||
match compile("a:\nb:\nc:")
|
let justfile = compile("a:\nb:\nc:");
|
||||||
.run(&["a", "x", "y", "z"], &Default::default())
|
let config = config(&["a", "x", "y", "z"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
UnknownRecipes {
|
UnknownRecipes {
|
||||||
recipes,
|
recipes,
|
||||||
suggestion,
|
suggestion,
|
||||||
@ -216,7 +243,7 @@ mod tests {
|
|||||||
assert_eq!(recipes, &["x", "y", "z"]);
|
assert_eq!(recipes, &["x", "y", "z"]);
|
||||||
assert_eq!(suggestion, None);
|
assert_eq!(suggestion, None);
|
||||||
}
|
}
|
||||||
other => panic!("expected an unknown recipe error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,8 +264,10 @@ a:
|
|||||||
x
|
x
|
||||||
x
|
x
|
||||||
";
|
";
|
||||||
|
let justfile = compile(text);
|
||||||
match compile(text).run(&["a"], &Default::default()).unwrap_err() {
|
let config = config(&["a"]);
|
||||||
|
let dir = env::current_dir().unwrap();
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
Code {
|
Code {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
@ -248,16 +277,16 @@ a:
|
|||||||
assert_eq!(code, 200);
|
assert_eq!(code, 200);
|
||||||
assert_eq!(line_number, None);
|
assert_eq!(line_number, None);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn code_error() {
|
fn code_error() {
|
||||||
match compile("fail:\n @exit 100")
|
let justfile = compile("fail:\n @exit 100");
|
||||||
.run(&["fail"], &Default::default())
|
let config = config(&["fail"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
Code {
|
Code {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
@ -267,7 +296,7 @@ a:
|
|||||||
assert_eq!(code, 100);
|
assert_eq!(code, 100);
|
||||||
assert_eq!(line_number, Some(2));
|
assert_eq!(line_number, Some(2));
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,11 +305,11 @@ a:
|
|||||||
let text = r#"
|
let text = r#"
|
||||||
a return code:
|
a return code:
|
||||||
@x() { {{return}} {{code + "0"}}; }; x"#;
|
@x() { {{return}} {{code + "0"}}; }; x"#;
|
||||||
|
let justfile = compile(text);
|
||||||
|
let config = config(&["a", "return", "15"]);
|
||||||
|
let dir = env::current_dir().unwrap();
|
||||||
|
|
||||||
match compile(text)
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
.run(&["a", "return", "15"], &Default::default())
|
|
||||||
.unwrap_err()
|
|
||||||
{
|
|
||||||
Code {
|
Code {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
@ -290,16 +319,16 @@ a return code:
|
|||||||
assert_eq!(code, 150);
|
assert_eq!(code, 150);
|
||||||
assert_eq!(line_number, Some(3));
|
assert_eq!(line_number, Some(3));
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_some_arguments() {
|
fn missing_some_arguments() {
|
||||||
match compile("a b c d:")
|
let justfile = compile("a b c d:");
|
||||||
.run(&["a", "b", "c"], &Default::default())
|
let config = config(&["a", "b", "c"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
@ -317,16 +346,16 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_some_arguments_variadic() {
|
fn missing_some_arguments_variadic() {
|
||||||
match compile("a b c +d:")
|
let justfile = compile("a b c +d:");
|
||||||
.run(&["a", "B", "C"], &Default::default())
|
let config = config(&["a", "B", "C"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
@ -344,16 +373,17 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, usize::MAX - 1);
|
assert_eq!(max, usize::MAX - 1);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_all_arguments() {
|
fn missing_all_arguments() {
|
||||||
match compile("a b c d:\n echo {{b}}{{c}}{{d}}")
|
let justfile = compile("a b c d:\n echo {{b}}{{c}}{{d}}");
|
||||||
.run(&["a"], &Default::default())
|
let config = config(&["a"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
@ -371,16 +401,17 @@ a return code:
|
|||||||
assert_eq!(min, 3);
|
assert_eq!(min, 3);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_some_defaults() {
|
fn missing_some_defaults() {
|
||||||
match compile("a b c d='hello':")
|
let justfile = compile("a b c d='hello':");
|
||||||
.run(&["a", "b"], &Default::default())
|
let config = config(&["a", "b"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
@ -398,16 +429,17 @@ a return code:
|
|||||||
assert_eq!(min, 2);
|
assert_eq!(min, 2);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_all_defaults() {
|
fn missing_all_defaults() {
|
||||||
match compile("a b c='r' d='h':")
|
let justfile = compile("a b c='r' d='h':");
|
||||||
.run(&["a"], &Default::default())
|
let config = &config(&["a"]);
|
||||||
.unwrap_err()
|
let dir = env::current_dir().unwrap();
|
||||||
{
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe,
|
recipe,
|
||||||
parameters,
|
parameters,
|
||||||
@ -425,23 +457,21 @@ a return code:
|
|||||||
assert_eq!(min, 1);
|
assert_eq!(min, 1);
|
||||||
assert_eq!(max, 3);
|
assert_eq!(max, 3);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_overrides() {
|
fn unknown_overrides() {
|
||||||
let mut config: Config = Default::default();
|
let config = config(&["foo=bar", "baz=bob", "a"]);
|
||||||
config.overrides.insert("foo", "bar");
|
let justfile = compile("a:\n echo {{`f() { return 100; }; f`}}");
|
||||||
config.overrides.insert("baz", "bob");
|
let dir = env::current_dir().unwrap();
|
||||||
match compile("a:\n echo {{`f() { return 100; }; f`}}")
|
|
||||||
.run(&["a"], &config)
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
.unwrap_err()
|
|
||||||
{
|
|
||||||
UnknownOverrides { overrides } => {
|
UnknownOverrides { overrides } => {
|
||||||
assert_eq!(overrides, &["baz", "foo"]);
|
assert_eq!(overrides, &["baz", "foo"]);
|
||||||
}
|
}
|
||||||
other => panic!("expected a code run error, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,12 +487,12 @@ wut:
|
|||||||
echo $foo $bar $baz
|
echo $foo $bar $baz
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let config = Config {
|
let config = config(&["--quiet", "wut"]);
|
||||||
quiet: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
match compile(text).run(&["wut"], &config).unwrap_err() {
|
let justfile = compile(text);
|
||||||
|
let dir = env::current_dir().unwrap();
|
||||||
|
|
||||||
|
match justfile.run(&config, &dir).unwrap_err() {
|
||||||
Code {
|
Code {
|
||||||
code: _,
|
code: _,
|
||||||
line_number,
|
line_number,
|
||||||
@ -471,7 +501,7 @@ wut:
|
|||||||
assert_eq!(recipe, "wut");
|
assert_eq!(recipe, "wut");
|
||||||
assert_eq!(line_number, Some(8));
|
assert_eq!(line_number, Some(8));
|
||||||
}
|
}
|
||||||
other => panic!("expected a recipe code errror, but got: {}", other),
|
other => panic!("unexpected error: {}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,9 +15,6 @@ pub mod node;
|
|||||||
#[cfg(fuzzing)]
|
#[cfg(fuzzing)]
|
||||||
pub(crate) mod fuzzing;
|
pub(crate) mod fuzzing;
|
||||||
|
|
||||||
#[macro_use]
|
|
||||||
mod die;
|
|
||||||
|
|
||||||
mod alias;
|
mod alias;
|
||||||
mod alias_resolver;
|
mod alias_resolver;
|
||||||
mod analyzer;
|
mod analyzer;
|
||||||
@ -37,6 +34,8 @@ mod count;
|
|||||||
mod default;
|
mod default;
|
||||||
mod empty;
|
mod empty;
|
||||||
mod enclosure;
|
mod enclosure;
|
||||||
|
mod error;
|
||||||
|
mod error_result_ext;
|
||||||
mod expression;
|
mod expression;
|
||||||
mod fragment;
|
mod fragment;
|
||||||
mod function;
|
mod function;
|
||||||
@ -52,6 +51,7 @@ mod lexer;
|
|||||||
mod line;
|
mod line;
|
||||||
mod list;
|
mod list;
|
||||||
mod load_dotenv;
|
mod load_dotenv;
|
||||||
|
mod load_error;
|
||||||
mod module;
|
mod module;
|
||||||
mod name;
|
mod name;
|
||||||
mod ordinal;
|
mod ordinal;
|
||||||
@ -69,6 +69,7 @@ mod recipe_resolver;
|
|||||||
mod run;
|
mod run;
|
||||||
mod runtime_error;
|
mod runtime_error;
|
||||||
mod search;
|
mod search;
|
||||||
|
mod search_config;
|
||||||
mod search_error;
|
mod search_error;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
mod show_whitespace;
|
mod show_whitespace;
|
||||||
|
19
src/load_error.rs
Normal file
19
src/load_error.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct LoadError<'path> {
|
||||||
|
pub(crate) path: &'path Path,
|
||||||
|
pub(crate) io_error: io::Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for LoadError<'_> {}
|
||||||
|
|
||||||
|
impl Display for LoadError<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to read justfile at `{}`: {}",
|
||||||
|
self.path.display(),
|
||||||
|
self.io_error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -48,7 +48,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
&self,
|
&self,
|
||||||
expected: &[TokenKind],
|
expected: &[TokenKind],
|
||||||
) -> CompilationResult<'src, CompilationError<'src>> {
|
) -> CompilationResult<'src, CompilationError<'src>> {
|
||||||
let mut expected = expected.iter().cloned().collect::<Vec<TokenKind>>();
|
let mut expected = expected.to_vec();
|
||||||
expected.sort();
|
expected.sort();
|
||||||
|
|
||||||
self.error(CompilationErrorKind::UnexpectedToken {
|
self.error(CompilationErrorKind::UnexpectedToken {
|
||||||
@ -69,7 +69,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
/// An iterator over the remaining significant tokens
|
/// An iterator over the remaining significant tokens
|
||||||
fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'tokens {
|
fn rest(&self) -> impl Iterator<Item = Token<'src>> + 'tokens {
|
||||||
self.tokens[self.next..]
|
self.tokens[self.next..]
|
||||||
.into_iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.filter(|token| token.kind != Whitespace)
|
.filter(|token| token.kind != Whitespace)
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Get the `n`th next significant token
|
/// Get the `n`th next significant token
|
||||||
fn get(&self, n: usize) -> CompilationResult<'src, Token<'src>> {
|
fn get(&self, n: usize) -> CompilationResult<'src, Token<'src>> {
|
||||||
match self.rest().skip(n).next() {
|
match self.rest().nth(n) {
|
||||||
Some(token) => Ok(token),
|
Some(token) => Ok(token),
|
||||||
None => Err(self.internal_error("`Parser::get()` advanced past end of token stream")?),
|
None => Err(self.internal_error("`Parser::get()` advanced past end of token stream")?),
|
||||||
}
|
}
|
||||||
@ -374,15 +374,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
self.expect(ParenR)?;
|
self.expect(ParenR)?;
|
||||||
Ok(Expression::Group { contents })
|
Ok(Expression::Group { contents })
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Err(self.unexpected_token(&[StringCooked, StringRaw, Backtick, Identifier, ParenL])?),
|
||||||
return Err(self.unexpected_token(&[
|
|
||||||
StringCooked,
|
|
||||||
StringRaw,
|
|
||||||
Backtick,
|
|
||||||
Identifier,
|
|
||||||
ParenL,
|
|
||||||
])?)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,9 +426,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Parse a name from an identifier token
|
/// Parse a name from an identifier token
|
||||||
fn parse_name(&mut self) -> CompilationResult<'src, Name<'src>> {
|
fn parse_name(&mut self) -> CompilationResult<'src, Name<'src>> {
|
||||||
self
|
self.expect(Identifier).map(Name::from_identifier)
|
||||||
.expect(Identifier)
|
|
||||||
.map(|token| Name::from_identifier(token))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse sequence of comma-separated expressions
|
/// Parse sequence of comma-separated expressions
|
||||||
@ -1415,7 +1405,10 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 10,
|
column: 10,
|
||||||
width: 1,
|
width: 1,
|
||||||
kind: UnexpectedToken{expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw], found: Eol},
|
kind: UnexpectedToken {
|
||||||
|
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
||||||
|
found: Eol
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
@ -1425,7 +1418,10 @@ mod tests {
|
|||||||
line: 0,
|
line: 0,
|
||||||
column: 10,
|
column: 10,
|
||||||
width: 0,
|
width: 0,
|
||||||
kind: UnexpectedToken{expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw], found: Eof},
|
kind: UnexpectedToken {
|
||||||
|
expected: vec![Backtick, Identifier, ParenL, StringCooked, StringRaw],
|
||||||
|
found: Eof,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
error! {
|
error! {
|
||||||
|
@ -6,11 +6,16 @@ pub(crate) struct Platform;
|
|||||||
impl PlatformInterface for Platform {
|
impl PlatformInterface for Platform {
|
||||||
fn make_shebang_command(
|
fn make_shebang_command(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
working_directory: &Path,
|
||||||
_command: &str,
|
_command: &str,
|
||||||
_argument: Option<&str>,
|
_argument: Option<&str>,
|
||||||
) -> Result<Command, OutputError> {
|
) -> Result<Command, OutputError> {
|
||||||
// shebang scripts can be executed directly on unix
|
// shebang scripts can be executed directly on unix
|
||||||
Ok(Command::new(path))
|
let mut cmd = Command::new(path);
|
||||||
|
|
||||||
|
cmd.current_dir(working_directory);
|
||||||
|
|
||||||
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_execute_permission(path: &Path) -> Result<(), io::Error> {
|
fn set_execute_permission(path: &Path) -> Result<(), io::Error> {
|
||||||
@ -32,7 +37,7 @@ impl PlatformInterface for Platform {
|
|||||||
exit_status.signal()
|
exit_status.signal()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_shell_path(path: &Path) -> Result<String, String> {
|
fn to_shell_path(_working_directory: &Path, path: &Path) -> Result<String, String> {
|
||||||
path
|
path
|
||||||
.to_str()
|
.to_str()
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
@ -44,15 +49,20 @@ impl PlatformInterface for Platform {
|
|||||||
impl PlatformInterface for Platform {
|
impl PlatformInterface for Platform {
|
||||||
fn make_shebang_command(
|
fn make_shebang_command(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
working_directory: &Path,
|
||||||
command: &str,
|
command: &str,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
) -> Result<Command, OutputError> {
|
) -> Result<Command, OutputError> {
|
||||||
// Translate path to the interpreter from unix style to windows style
|
// Translate path to the interpreter from unix style to windows style
|
||||||
let mut cygpath = Command::new("cygpath");
|
let mut cygpath = Command::new("cygpath");
|
||||||
|
cygpath.current_dir(working_directory);
|
||||||
cygpath.arg("--windows");
|
cygpath.arg("--windows");
|
||||||
cygpath.arg(command);
|
cygpath.arg(command);
|
||||||
|
|
||||||
let mut cmd = Command::new(output(cygpath)?);
|
let mut cmd = Command::new(output(cygpath)?);
|
||||||
|
|
||||||
|
cmd.current_dir(working_directory);
|
||||||
|
|
||||||
if let Some(argument) = argument {
|
if let Some(argument) = argument {
|
||||||
cmd.arg(argument);
|
cmd.arg(argument);
|
||||||
}
|
}
|
||||||
@ -72,9 +82,10 @@ impl PlatformInterface for Platform {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_shell_path(path: &Path) -> Result<String, String> {
|
fn to_shell_path(working_directory: &Path, path: &Path) -> Result<String, String> {
|
||||||
// Translate path from windows style to unix style
|
// Translate path from windows style to unix style
|
||||||
let mut cygpath = Command::new("cygpath");
|
let mut cygpath = Command::new("cygpath");
|
||||||
|
cygpath.current_dir(working_directory);
|
||||||
cygpath.arg("--unix");
|
cygpath.arg("--unix");
|
||||||
cygpath.arg(path);
|
cygpath.arg(path);
|
||||||
output(cygpath).map_err(|e| format!("Error converting shell path: {}", e))
|
output(cygpath).map_err(|e| format!("Error converting shell path: {}", e))
|
||||||
|
@ -5,6 +5,7 @@ pub(crate) trait PlatformInterface {
|
|||||||
/// shebang line `shebang`
|
/// shebang line `shebang`
|
||||||
fn make_shebang_command(
|
fn make_shebang_command(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
working_directory: &Path,
|
||||||
command: &str,
|
command: &str,
|
||||||
argument: Option<&str>,
|
argument: Option<&str>,
|
||||||
) -> Result<Command, OutputError>;
|
) -> Result<Command, OutputError>;
|
||||||
@ -16,5 +17,5 @@ pub(crate) trait PlatformInterface {
|
|||||||
fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option<i32>;
|
fn signal_from_exit_status(exit_status: process::ExitStatus) -> Option<i32>;
|
||||||
|
|
||||||
/// Translate a path from a "native" path to a path the interpreter expects
|
/// Translate a path from a "native" path to a path the interpreter expects
|
||||||
fn to_shell_path(path: &Path) -> Result<String, String>;
|
fn to_shell_path(working_directory: &Path, path: &Path) -> Result<String, String>;
|
||||||
}
|
}
|
||||||
|
@ -86,13 +86,10 @@ impl<'a> Recipe<'a> {
|
|||||||
|
|
||||||
let mut evaluator = AssignmentEvaluator {
|
let mut evaluator = AssignmentEvaluator {
|
||||||
assignments: &empty(),
|
assignments: &empty(),
|
||||||
dry_run: config.dry_run,
|
|
||||||
evaluated: empty(),
|
evaluated: empty(),
|
||||||
invocation_directory: &config.invocation_directory,
|
working_directory: context.working_directory,
|
||||||
overrides: &empty(),
|
|
||||||
quiet: config.quiet,
|
|
||||||
scope: &context.scope,
|
scope: &context.scope,
|
||||||
shell: config.shell,
|
config,
|
||||||
dotenv,
|
dotenv,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,12 +193,11 @@ impl<'a> Recipe<'a> {
|
|||||||
|
|
||||||
// create a command to run the script
|
// create a command to run the script
|
||||||
let mut command =
|
let mut command =
|
||||||
Platform::make_shebang_command(&path, interpreter, argument).map_err(|output_error| {
|
Platform::make_shebang_command(&path, context.working_directory, interpreter, argument)
|
||||||
RuntimeError::Cygpath {
|
.map_err(|output_error| RuntimeError::Cygpath {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
output_error,
|
output_error,
|
||||||
}
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
command.export_environment_variables(&context.scope, dotenv)?;
|
command.export_environment_variables(&context.scope, dotenv)?;
|
||||||
|
|
||||||
@ -276,7 +272,9 @@ impl<'a> Recipe<'a> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cmd = Command::new(config.shell);
|
let mut cmd = Command::new(&config.shell);
|
||||||
|
|
||||||
|
cmd.current_dir(context.working_directory);
|
||||||
|
|
||||||
cmd.arg("-cu").arg(command);
|
cmd.arg("-cu").arg(command);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
pub(crate) struct RecipeContext<'a> {
|
pub(crate) struct RecipeContext<'a> {
|
||||||
pub(crate) config: &'a Config<'a>,
|
pub(crate) config: &'a Config,
|
||||||
pub(crate) scope: BTreeMap<&'a str, (bool, String)>,
|
pub(crate) scope: BTreeMap<&'a str, (bool, String)>,
|
||||||
|
pub(crate) working_directory: &'a Path,
|
||||||
}
|
}
|
||||||
|
118
src/run.rs
118
src/run.rs
@ -15,121 +15,7 @@ pub fn run() -> Result<(), i32> {
|
|||||||
|
|
||||||
let matches = app.get_matches();
|
let matches = app.get_matches();
|
||||||
|
|
||||||
let config = match Config::from_matches(&matches) {
|
let config = Config::from_matches(&matches).eprint(Color::auto())?;
|
||||||
Ok(config) => config,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("error: {}", error);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let justfile = config.justfile;
|
config.run_subcommand()
|
||||||
|
|
||||||
if let Some(directory) = config.search_directory {
|
|
||||||
if let Err(error) = env::set_current_dir(&directory) {
|
|
||||||
die!(
|
|
||||||
"Error changing directory to {}: {}",
|
|
||||||
directory.display(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut working_directory = config.working_directory.map(PathBuf::from);
|
|
||||||
|
|
||||||
if let (Some(justfile), None) = (justfile, working_directory.as_ref()) {
|
|
||||||
let mut justfile = justfile.to_path_buf();
|
|
||||||
|
|
||||||
if !justfile.is_absolute() {
|
|
||||||
match justfile.canonicalize() {
|
|
||||||
Ok(canonical) => justfile = canonical,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!(
|
|
||||||
"Could not canonicalize justfile path `{}`: {}",
|
|
||||||
justfile.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
justfile.pop();
|
|
||||||
|
|
||||||
working_directory = Some(justfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text;
|
|
||||||
if let (Some(justfile), Some(directory)) = (justfile, working_directory) {
|
|
||||||
if config.subcommand == Subcommand::Edit {
|
|
||||||
return Subcommand::edit(justfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
text = fs::read_to_string(justfile)
|
|
||||||
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
|
||||||
|
|
||||||
if let Err(error) = env::set_current_dir(&directory) {
|
|
||||||
die!(
|
|
||||||
"Error changing directory to {}: {}",
|
|
||||||
directory.display(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let current_dir = match env::current_dir() {
|
|
||||||
Ok(current_dir) => current_dir,
|
|
||||||
Err(io_error) => die!("Error getting current dir: {}", io_error),
|
|
||||||
};
|
|
||||||
match search::justfile(¤t_dir) {
|
|
||||||
Ok(name) => {
|
|
||||||
if config.subcommand == Subcommand::Edit {
|
|
||||||
return Subcommand::edit(&name);
|
|
||||||
}
|
|
||||||
text = match fs::read_to_string(&name) {
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error reading justfile: {}", error);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
Ok(text) => text,
|
|
||||||
};
|
|
||||||
|
|
||||||
let parent = name.parent().unwrap();
|
|
||||||
|
|
||||||
if let Err(error) = env::set_current_dir(&parent) {
|
|
||||||
eprintln!(
|
|
||||||
"Error changing directory to {}: {}",
|
|
||||||
parent.display(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(search_error) => {
|
|
||||||
eprintln!("{}", search_error);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let justfile = match Compiler::compile(&text) {
|
|
||||||
Err(error) => {
|
|
||||||
if config.color.stderr().active() {
|
|
||||||
eprintln!("{:#}", error);
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", error);
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
Ok(justfile) => justfile,
|
|
||||||
};
|
|
||||||
|
|
||||||
for warning in &justfile.warnings {
|
|
||||||
if config.color.stderr().active() {
|
|
||||||
eprintln!("{:#}", warning);
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config.subcommand.run(&config, justfile)
|
|
||||||
}
|
}
|
||||||
|
@ -62,18 +62,22 @@ pub(crate) enum RuntimeError<'a> {
|
|||||||
recipe: &'a str,
|
recipe: &'a str,
|
||||||
line_number: Option<usize>,
|
line_number: Option<usize>,
|
||||||
},
|
},
|
||||||
|
NoRecipes,
|
||||||
|
DefaultRecipeRequiresArguments {
|
||||||
|
recipe: &'a str,
|
||||||
|
min_arguments: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> RuntimeError<'a> {
|
impl Error for RuntimeError<'_> {
|
||||||
pub(crate) fn code(&self) -> Option<i32> {
|
fn code(&self) -> i32 {
|
||||||
use RuntimeError::*;
|
|
||||||
match *self {
|
match *self {
|
||||||
Code { code, .. }
|
Self::Code { code, .. } => code,
|
||||||
| Backtick {
|
Self::Backtick {
|
||||||
output_error: OutputError::Code(code),
|
output_error: OutputError::Code(code),
|
||||||
..
|
..
|
||||||
} => Some(code),
|
} => code,
|
||||||
_ => None,
|
_ => EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,9 +91,8 @@ impl<'a> Display for RuntimeError<'a> {
|
|||||||
} else {
|
} else {
|
||||||
Color::never()
|
Color::never()
|
||||||
};
|
};
|
||||||
let error = color.error();
|
|
||||||
let message = color.message();
|
let message = color.message();
|
||||||
write!(f, "{} {}", error.paint("error:"), message.prefix())?;
|
write!(f, "{}", message.prefix())?;
|
||||||
|
|
||||||
let mut error_token: Option<Token> = None;
|
let mut error_token: Option<Token> = None;
|
||||||
|
|
||||||
@ -372,6 +375,21 @@ impl<'a> Display for RuntimeError<'a> {
|
|||||||
error_token = Some(*token);
|
error_token = Some(*token);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
NoRecipes => {
|
||||||
|
writeln!(f, "Justfile contains no recipes.",)?;
|
||||||
|
}
|
||||||
|
DefaultRecipeRequiresArguments {
|
||||||
|
recipe,
|
||||||
|
min_arguments,
|
||||||
|
} => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` cannot be used as default recipe since it requires at least {} {}.",
|
||||||
|
recipe,
|
||||||
|
min_arguments,
|
||||||
|
Count("argument", min_arguments),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
Internal { ref message } => {
|
Internal { ref message } => {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
|
120
src/search.rs
120
src/search.rs
@ -2,31 +2,101 @@ use crate::common::*;
|
|||||||
|
|
||||||
const FILENAME: &str = "justfile";
|
const FILENAME: &str = "justfile";
|
||||||
|
|
||||||
pub(crate) fn justfile(directory: &Path) -> Result<PathBuf, SearchError> {
|
pub(crate) struct Search {
|
||||||
let mut candidates = Vec::new();
|
pub(crate) justfile: PathBuf,
|
||||||
let dir = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
|
pub(crate) working_directory: PathBuf,
|
||||||
io_error,
|
}
|
||||||
directory: directory.to_owned(),
|
|
||||||
})?;
|
impl Search {
|
||||||
for entry in dir {
|
pub(crate) fn search(
|
||||||
let entry = entry.map_err(|io_error| SearchError::Io {
|
search_config: &SearchConfig,
|
||||||
|
invocation_directory: &Path,
|
||||||
|
) -> SearchResult<Search> {
|
||||||
|
match search_config {
|
||||||
|
SearchConfig::FromInvocationDirectory => {
|
||||||
|
let justfile = Self::justfile(&invocation_directory)?;
|
||||||
|
|
||||||
|
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||||
|
|
||||||
|
Ok(Search {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchConfig::FromSearchDirectory { search_directory } => {
|
||||||
|
let justfile = Self::justfile(search_directory)?;
|
||||||
|
|
||||||
|
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||||
|
|
||||||
|
Ok(Search {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchConfig::WithJustfile { justfile } => {
|
||||||
|
let justfile: PathBuf = justfile.to_path_buf();
|
||||||
|
|
||||||
|
let working_directory = Self::working_directory_from_justfile(&justfile)?;
|
||||||
|
|
||||||
|
Ok(Search {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchConfig::WithJustfileAndWorkingDirectory {
|
||||||
|
justfile,
|
||||||
|
working_directory,
|
||||||
|
} => Ok(Search {
|
||||||
|
justfile: justfile.to_path_buf(),
|
||||||
|
working_directory: working_directory.to_path_buf(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
|
||||||
io_error,
|
io_error,
|
||||||
directory: directory.to_owned(),
|
directory: directory.to_owned(),
|
||||||
})?;
|
})?;
|
||||||
if let Some(name) = entry.file_name().to_str() {
|
for entry in entries {
|
||||||
if name.eq_ignore_ascii_case(FILENAME) {
|
let entry = entry.map_err(|io_error| SearchError::Io {
|
||||||
candidates.push(entry.path());
|
io_error,
|
||||||
|
directory: directory.to_owned(),
|
||||||
|
})?;
|
||||||
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
|
if name.eq_ignore_ascii_case(FILENAME) {
|
||||||
|
candidates.push(entry.path());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if candidates.len() == 1 {
|
||||||
|
Ok(candidates.pop().unwrap())
|
||||||
|
} else if candidates.len() > 1 {
|
||||||
|
Err(SearchError::MultipleCandidates { candidates })
|
||||||
|
} else if let Some(parent) = directory.parent() {
|
||||||
|
Self::justfile(parent)
|
||||||
|
} else {
|
||||||
|
Err(SearchError::NotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if candidates.len() == 1 {
|
|
||||||
Ok(candidates.pop().unwrap())
|
fn working_directory_from_justfile(justfile: &Path) -> SearchResult<PathBuf> {
|
||||||
} else if candidates.len() > 1 {
|
let justfile_canonical = justfile
|
||||||
Err(SearchError::MultipleCandidates { candidates })
|
.canonicalize()
|
||||||
} else if let Some(parent_dir) = directory.parent() {
|
.context(search_error::Canonicalize { path: justfile })?;
|
||||||
justfile(parent_dir)
|
|
||||||
} else {
|
Ok(
|
||||||
Err(SearchError::NotFound)
|
justfile_canonical
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| SearchError::JustfileHadNoParent {
|
||||||
|
path: justfile_canonical.clone(),
|
||||||
|
})?
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +107,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn not_found() {
|
fn not_found() {
|
||||||
let tmp = testing::tempdir();
|
let tmp = testing::tempdir();
|
||||||
match search::justfile(tmp.path()) {
|
match Search::justfile(tmp.path()) {
|
||||||
Err(SearchError::NotFound) => {
|
Err(SearchError::NotFound) => {
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
@ -59,7 +129,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
fs::write(&path, "default:\n\techo ok").unwrap();
|
||||||
path.pop();
|
path.pop();
|
||||||
match search::justfile(path.as_path()) {
|
match Search::justfile(path.as_path()) {
|
||||||
Err(SearchError::MultipleCandidates { .. }) => {
|
Err(SearchError::MultipleCandidates { .. }) => {
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
@ -74,7 +144,7 @@ mod tests {
|
|||||||
path.push(FILENAME);
|
path.push(FILENAME);
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
fs::write(&path, "default:\n\techo ok").unwrap();
|
||||||
path.pop();
|
path.pop();
|
||||||
match search::justfile(path.as_path()) {
|
match Search::justfile(path.as_path()) {
|
||||||
Ok(_path) => {
|
Ok(_path) => {
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
@ -100,7 +170,7 @@ mod tests {
|
|||||||
path.push(spongebob_case);
|
path.push(spongebob_case);
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
fs::write(&path, "default:\n\techo ok").unwrap();
|
||||||
path.pop();
|
path.pop();
|
||||||
match search::justfile(path.as_path()) {
|
match Search::justfile(path.as_path()) {
|
||||||
Ok(_path) => {
|
Ok(_path) => {
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
@ -119,7 +189,7 @@ mod tests {
|
|||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
||||||
path.push("b");
|
path.push("b");
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
||||||
match search::justfile(path.as_path()) {
|
match Search::justfile(path.as_path()) {
|
||||||
Ok(_path) => {
|
Ok(_path) => {
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
@ -141,7 +211,7 @@ mod tests {
|
|||||||
path.pop();
|
path.pop();
|
||||||
path.push("b");
|
path.push("b");
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
||||||
match search::justfile(path.as_path()) {
|
match Search::justfile(path.as_path()) {
|
||||||
Ok(found_path) => {
|
Ok(found_path) => {
|
||||||
path.pop();
|
path.pop();
|
||||||
path.push(FILENAME);
|
path.push(FILENAME);
|
||||||
|
21
src/search_config.rs
Normal file
21
src/search_config.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
/// Controls how `just` will search for the justfile.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub(crate) enum SearchConfig {
|
||||||
|
/// Recursively search for the justfile upwards from the
|
||||||
|
/// invocation directory to the root, setting the working
|
||||||
|
/// directory to the directory in which the justfile is
|
||||||
|
/// found.
|
||||||
|
FromInvocationDirectory,
|
||||||
|
/// As in `Invocation`, but start from `search_directory`.
|
||||||
|
FromSearchDirectory { search_directory: PathBuf },
|
||||||
|
/// Use user-specified justfile, with the working directory
|
||||||
|
/// set to the directory that contains it.
|
||||||
|
WithJustfile { justfile: PathBuf },
|
||||||
|
/// Use user-specified justfile and working directory.
|
||||||
|
WithJustfileAndWorkingDirectory {
|
||||||
|
justfile: PathBuf,
|
||||||
|
working_directory: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
@ -1,42 +1,41 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Snafu)]
|
||||||
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub(crate) enum SearchError {
|
pub(crate) enum SearchError {
|
||||||
|
#[snafu(display(
|
||||||
|
"Multiple candidate justfiles found in `{}`: {}",
|
||||||
|
candidates[0].parent().unwrap().display(),
|
||||||
|
List::and_ticked(
|
||||||
|
candidates
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| candidate.file_name().unwrap().to_string_lossy())
|
||||||
|
),
|
||||||
|
))]
|
||||||
MultipleCandidates {
|
MultipleCandidates {
|
||||||
candidates: Vec<PathBuf>,
|
candidates: Vec<PathBuf>,
|
||||||
},
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"I/O error reading directory `{}`: {}",
|
||||||
|
directory.display(),
|
||||||
|
io_error
|
||||||
|
))]
|
||||||
Io {
|
Io {
|
||||||
directory: PathBuf,
|
directory: PathBuf,
|
||||||
io_error: io::Error,
|
io_error: io::Error,
|
||||||
},
|
},
|
||||||
|
#[snafu(display("No justfile found"))]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
Canonicalize {
|
||||||
|
path: PathBuf,
|
||||||
|
source: io::Error,
|
||||||
|
},
|
||||||
|
JustfileHadNoParent {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for SearchError {
|
impl Error for SearchError {}
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
SearchError::Io {
|
|
||||||
directory,
|
|
||||||
io_error,
|
|
||||||
} => write!(
|
|
||||||
f,
|
|
||||||
"I/O error reading directory `{}`: {}",
|
|
||||||
directory.display(),
|
|
||||||
io_error
|
|
||||||
),
|
|
||||||
SearchError::MultipleCandidates { candidates } => write!(
|
|
||||||
f,
|
|
||||||
"Multiple candidate justfiles found in `{}`: {}",
|
|
||||||
candidates[0].parent().unwrap().display(),
|
|
||||||
List::and_ticked(
|
|
||||||
candidates
|
|
||||||
.iter()
|
|
||||||
.map(|candidate| candidate.file_name().unwrap().to_string_lossy())
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SearchError::NotFound => write!(f, "No justfile found"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
@ -1,224 +1,10 @@
|
|||||||
use crate::common::*;
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
|
pub(crate) enum Subcommand {
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy)]
|
|
||||||
pub(crate) enum Subcommand<'a> {
|
|
||||||
Dump,
|
Dump,
|
||||||
Edit,
|
Edit,
|
||||||
Evaluate,
|
Evaluate,
|
||||||
Execute,
|
Run,
|
||||||
List,
|
List,
|
||||||
Show { name: &'a str },
|
Show { name: String },
|
||||||
Summary,
|
Summary,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Subcommand<'a> {
|
|
||||||
pub(crate) fn run(self, config: &Config, justfile: Justfile) -> Result<(), i32> {
|
|
||||||
use Subcommand::*;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Dump => Self::dump(justfile),
|
|
||||||
Edit => {
|
|
||||||
eprintln!("Internal error: Subcommand::run unexpectadly invoked on Edit variant!");
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
}
|
|
||||||
Execute | Evaluate => Self::execute(config, justfile),
|
|
||||||
List => Self::list(config, justfile),
|
|
||||||
Show { name } => Self::show(justfile, name),
|
|
||||||
Summary => Self::summary(justfile),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dump(justfile: Justfile) -> Result<(), i32> {
|
|
||||||
println!("{}", justfile);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn edit(path: &Path) -> Result<(), i32> {
|
|
||||||
let editor = match env::var_os("EDITOR") {
|
|
||||||
None => {
|
|
||||||
eprintln!("Error getting EDITOR environment variable");
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
Some(editor) => editor,
|
|
||||||
};
|
|
||||||
|
|
||||||
let error = Command::new(editor).arg(path).status();
|
|
||||||
|
|
||||||
match error {
|
|
||||||
Ok(status) => {
|
|
||||||
if status.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
eprintln!("Editor failed: {}", status);
|
|
||||||
Err(status.code().unwrap_or(EXIT_FAILURE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Failed to invoke editor: {}", error);
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(config: &Config, justfile: Justfile) -> Result<(), i32> {
|
|
||||||
// Construct a target to alias map.
|
|
||||||
let mut recipe_aliases: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
|
|
||||||
for alias in justfile.aliases.values() {
|
|
||||||
if alias.is_private() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !recipe_aliases.contains_key(alias.target.lexeme()) {
|
|
||||||
recipe_aliases.insert(alias.target.lexeme(), vec![alias.name.lexeme()]);
|
|
||||||
} else {
|
|
||||||
let aliases = recipe_aliases.get_mut(alias.target.lexeme()).unwrap();
|
|
||||||
aliases.push(alias.name.lexeme());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line_widths: BTreeMap<&str, usize> = BTreeMap::new();
|
|
||||||
|
|
||||||
for (name, recipe) in &justfile.recipes {
|
|
||||||
if recipe.private {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for name in iter::once(name).chain(recipe_aliases.get(name).unwrap_or(&Vec::new())) {
|
|
||||||
let mut line_width = UnicodeWidthStr::width(*name);
|
|
||||||
|
|
||||||
for parameter in &recipe.parameters {
|
|
||||||
line_width += UnicodeWidthStr::width(format!(" {}", parameter).as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
if line_width <= 30 {
|
|
||||||
line_widths.insert(name, line_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_line_width = cmp::min(line_widths.values().cloned().max().unwrap_or(0), 30);
|
|
||||||
|
|
||||||
let doc_color = config.color.stdout().doc();
|
|
||||||
println!("Available recipes:");
|
|
||||||
|
|
||||||
for (name, recipe) in &justfile.recipes {
|
|
||||||
if recipe.private {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let alias_doc = format!("alias for `{}`", recipe.name);
|
|
||||||
|
|
||||||
for (i, name) in iter::once(name)
|
|
||||||
.chain(recipe_aliases.get(name).unwrap_or(&Vec::new()))
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
print!(" {}", name);
|
|
||||||
for parameter in &recipe.parameters {
|
|
||||||
if config.color.stdout().active() {
|
|
||||||
print!(" {:#}", parameter);
|
|
||||||
} else {
|
|
||||||
print!(" {}", parameter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Declaring this outside of the nested loops will probably be more efficient, but
|
|
||||||
// it creates all sorts of lifetime issues with variables inside the loops.
|
|
||||||
// If this is inlined like the docs say, it shouldn't make any difference.
|
|
||||||
let print_doc = |doc| {
|
|
||||||
print!(
|
|
||||||
" {:padding$}{} {}",
|
|
||||||
"",
|
|
||||||
doc_color.paint("#"),
|
|
||||||
doc_color.paint(doc),
|
|
||||||
padding = max_line_width
|
|
||||||
.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
match (i, recipe.doc) {
|
|
||||||
(0, Some(doc)) => print_doc(doc),
|
|
||||||
(0, None) => (),
|
|
||||||
_ => print_doc(&alias_doc),
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute(config: &Config, justfile: Justfile) -> Result<(), i32> {
|
|
||||||
let arguments = if !config.arguments.is_empty() {
|
|
||||||
config.arguments.clone()
|
|
||||||
} else if let Some(recipe) = justfile.first() {
|
|
||||||
let min_arguments = recipe.min_arguments();
|
|
||||||
if min_arguments > 0 {
|
|
||||||
die!(
|
|
||||||
"Recipe `{}` cannot be used as default recipe since it requires at least {} {}.",
|
|
||||||
recipe.name,
|
|
||||||
min_arguments,
|
|
||||||
Count("argument", min_arguments),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
vec![recipe.name()]
|
|
||||||
} else {
|
|
||||||
die!("Justfile contains no recipes.");
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(error) = InterruptHandler::install() {
|
|
||||||
warn!("Failed to set CTRL-C handler: {}", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(run_error) = justfile.run(&arguments, &config) {
|
|
||||||
if !config.quiet {
|
|
||||||
if config.color.stderr().active() {
|
|
||||||
eprintln!("{:#}", run_error);
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", run_error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(run_error.code().unwrap_or(EXIT_FAILURE));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show(justfile: Justfile, name: &str) -> Result<(), i32> {
|
|
||||||
if let Some(alias) = justfile.get_alias(name) {
|
|
||||||
let recipe = justfile.get_recipe(alias.target.lexeme()).unwrap();
|
|
||||||
println!("{}", alias);
|
|
||||||
println!("{}", recipe);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if let Some(recipe) = justfile.get_recipe(name) {
|
|
||||||
println!("{}", recipe);
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
eprintln!("Justfile does not contain recipe `{}`.", name);
|
|
||||||
if let Some(suggestion) = justfile.suggest(name) {
|
|
||||||
eprintln!("Did you mean `{}`?", suggestion);
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn summary(justfile: Justfile) -> Result<(), i32> {
|
|
||||||
if justfile.count() == 0 {
|
|
||||||
eprintln!("Justfile contains no recipes.");
|
|
||||||
} else {
|
|
||||||
let summary = justfile
|
|
||||||
.recipes
|
|
||||||
.iter()
|
|
||||||
.filter(|&(_, recipe)| !recipe.private)
|
|
||||||
.map(|(name, _)| name)
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ");
|
|
||||||
println!("{}", summary);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -7,6 +7,17 @@ pub(crate) fn compile(text: &str) -> Justfile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn config(args: &[&str]) -> Config {
|
||||||
|
let mut args = Vec::from(args);
|
||||||
|
args.insert(0, "just");
|
||||||
|
|
||||||
|
let app = Config::app();
|
||||||
|
|
||||||
|
let matches = app.get_matches_from_safe(args).unwrap();
|
||||||
|
|
||||||
|
Config::from_matches(&matches).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) use test_utilities::{tempdir, unindent};
|
pub(crate) use test_utilities::{tempdir, unindent};
|
||||||
|
|
||||||
macro_rules! analysis_error {
|
macro_rules! analysis_error {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub(crate) enum UseColor {
|
pub(crate) enum UseColor {
|
||||||
Auto,
|
Auto,
|
||||||
Always,
|
Always,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use Verbosity::*;
|
use Verbosity::*;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub(crate) enum Verbosity {
|
pub(crate) enum Verbosity {
|
||||||
Taciturn,
|
Taciturn,
|
||||||
Loquacious,
|
Loquacious,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::{collections::HashMap, fs, path::Path, process::Output};
|
||||||
|
|
||||||
pub fn tempdir() -> tempfile::TempDir {
|
pub fn tempdir() -> tempfile::TempDir {
|
||||||
tempfile::Builder::new()
|
tempfile::Builder::new()
|
||||||
.prefix("just-test-tempdir")
|
.prefix("just-test-tempdir")
|
||||||
@ -5,6 +7,16 @@ pub fn tempdir() -> tempfile::TempDir {
|
|||||||
.expect("failed to create temporary directory")
|
.expect("failed to create temporary directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assert_stdout(output: &Output, stdout: &str) {
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
eprintln!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||||
|
panic!(output.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(String::from_utf8_lossy(&output.stdout), stdout);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unindent(text: &str) -> String {
|
pub fn unindent(text: &str) -> String {
|
||||||
// find line start and end indices
|
// find line start and end indices
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
@ -66,6 +78,90 @@ pub fn unindent(text: &str) -> String {
|
|||||||
text.to_owned()
|
text.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Entry {
|
||||||
|
File {
|
||||||
|
contents: &'static str,
|
||||||
|
},
|
||||||
|
Dir {
|
||||||
|
entries: HashMap<&'static str, Entry>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
fn instantiate(self, path: &Path) {
|
||||||
|
match self {
|
||||||
|
Entry::File { contents } => fs::write(path, contents).expect("Failed to write tempfile"),
|
||||||
|
Entry::Dir { entries } => {
|
||||||
|
fs::create_dir(path).expect("Failed to create tempdir");
|
||||||
|
|
||||||
|
for (name, entry) in entries {
|
||||||
|
entry.instantiate(&path.join(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn instantiate_base(base: &Path, entries: HashMap<&'static str, Entry>) {
|
||||||
|
for (name, entry) in entries {
|
||||||
|
entry.instantiate(&base.join(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! entry {
|
||||||
|
{
|
||||||
|
{
|
||||||
|
$($contents:tt)*
|
||||||
|
}
|
||||||
|
} => {
|
||||||
|
$crate::Entry::Dir{entries: $crate::entries!($($contents)*)}
|
||||||
|
};
|
||||||
|
{
|
||||||
|
$contents:expr
|
||||||
|
} => {
|
||||||
|
$crate::Entry::File{contents: $contents}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! entries {
|
||||||
|
{
|
||||||
|
} => {
|
||||||
|
std::collections::HashMap::new()
|
||||||
|
};
|
||||||
|
{
|
||||||
|
$($name:ident : $contents:tt,)*
|
||||||
|
} => {
|
||||||
|
{
|
||||||
|
let mut entries: std::collections::HashMap<&'static str, $crate::Entry> = std::collections::HashMap::new();
|
||||||
|
|
||||||
|
$(
|
||||||
|
entries.insert(stringify!($name), $crate::entry!($contents));
|
||||||
|
)*
|
||||||
|
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! tmptree {
|
||||||
|
{
|
||||||
|
$($contents:tt)*
|
||||||
|
} => {
|
||||||
|
{
|
||||||
|
let tempdir = $crate::tempdir();
|
||||||
|
|
||||||
|
let entries = $crate::entries!($($contents)*);
|
||||||
|
|
||||||
|
$crate::Entry::instantiate_base(&tempdir.path(), entries);
|
||||||
|
|
||||||
|
tempdir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn indentation(line: &str) -> &str {
|
fn indentation(line: &str) -> &str {
|
||||||
for (i, c) in line.char_indices() {
|
for (i, c) in line.char_indices() {
|
||||||
if c != ' ' && c != '\t' {
|
if c != ' ' && c != '\t' {
|
||||||
@ -138,4 +234,28 @@ mod tests {
|
|||||||
assert_eq!(common("", ""), "");
|
assert_eq!(common("", ""), "");
|
||||||
assert_eq!(common("", "bar"), "");
|
assert_eq!(common("", "bar"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmptree_file() {
|
||||||
|
let tmpdir = tmptree! {
|
||||||
|
foo: "bar",
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(tmpdir.path().join("foo")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contents, "bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tmptree_dir() {
|
||||||
|
let tmpdir = tmptree! {
|
||||||
|
foo: {
|
||||||
|
bar: "baz",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(tmpdir.path().join("foo/bar")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(contents, "baz");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
116
tests/edit.rs
Normal file
116
tests/edit.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use std::{env, iter, process::Command, str};
|
||||||
|
|
||||||
|
use executable_path::executable_path;
|
||||||
|
use which::which;
|
||||||
|
|
||||||
|
use test_utilities::{assert_stdout, tmptree};
|
||||||
|
|
||||||
|
const JUSTFILE: &str = "Yooooooo, hopefully this never becomes valid syntax.";
|
||||||
|
|
||||||
|
/// Test that --edit doesn't require a valid justfile
|
||||||
|
#[test]
|
||||||
|
fn invalid_justfile() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!output.status.success());
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--edit")
|
||||||
|
.env("VISUAL", "cat")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_stdout(&output, JUSTFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that editor is $VISUAL, $EDITOR, or "vim" in that order
|
||||||
|
#[test]
|
||||||
|
fn editor_precedence() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--edit")
|
||||||
|
.env("VISUAL", "cat")
|
||||||
|
.env("EDITOR", "this-command-doesnt-exist")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_stdout(&output, JUSTFILE);
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--edit")
|
||||||
|
.env_remove("VISUAL")
|
||||||
|
.env("EDITOR", "cat")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_stdout(&output, JUSTFILE);
|
||||||
|
|
||||||
|
let cat = which("cat").unwrap();
|
||||||
|
let vim = tmp.path().join(format!("vim{}", env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::os::unix::fs::symlink(cat, vim).unwrap();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
std::os::windows::fs::symlink_file(cat, vim).unwrap();
|
||||||
|
|
||||||
|
let path = env::join_paths(
|
||||||
|
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--edit")
|
||||||
|
.env("PATH", path)
|
||||||
|
.env_remove("VISUAL")
|
||||||
|
.env_remove("EDITOR")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_stdout(&output, JUSTFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that editor working directory is the same as edited justfile
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn editor_working_directory() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
child: {},
|
||||||
|
editor: "#!/usr/bin/env sh\ncat $1\npwd",
|
||||||
|
};
|
||||||
|
|
||||||
|
let editor = tmp.path().join("editor");
|
||||||
|
|
||||||
|
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);
|
||||||
|
std::fs::set_permissions(&editor, permissions).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path().join("child"))
|
||||||
|
.arg("--edit")
|
||||||
|
.env("VISUAL", &editor)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let want = format!(
|
||||||
|
"{}{}\n",
|
||||||
|
JUSTFILE,
|
||||||
|
tmp.path().canonicalize().unwrap().display()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_stdout(&output, &want);
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -74,7 +74,7 @@ default:
|
|||||||
fn interrupt_backtick() {
|
fn interrupt_backtick() {
|
||||||
interrupt_test(
|
interrupt_test(
|
||||||
"
|
"
|
||||||
foo = `sleep 1`
|
foo := `sleep 1`
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo {{foo}}
|
@echo {{foo}}
|
||||||
|
139
tests/search.rs
139
tests/search.rs
@ -1,12 +1,7 @@
|
|||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
use std::{fs, path, process, str};
|
use std::{path, process, str};
|
||||||
|
|
||||||
fn tempdir() -> tempfile::TempDir {
|
use test_utilities::tmptree;
|
||||||
tempfile::Builder::new()
|
|
||||||
.prefix("just-test-tempdir")
|
|
||||||
.tempdir()
|
|
||||||
.expect("failed to create temporary directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
||||||
let binary = executable_path("just");
|
let binary = executable_path("just");
|
||||||
@ -28,78 +23,59 @@ fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_justfile_search() {
|
fn test_justfile_search() {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
let mut path = tmp.path().to_path_buf();
|
justfile: "default:\n\techo ok",
|
||||||
path.push("justfile");
|
a: {
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
b: {
|
||||||
path.pop();
|
c: {
|
||||||
|
d: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
path.push("a");
|
search_test(tmp.path().join("a/b/c/d"), &[]);
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("b");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("c");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("d");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
|
|
||||||
search_test(path, &[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_capitalized_justfile_search() {
|
fn test_capitalized_justfile_search() {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
let mut path = tmp.path().to_path_buf();
|
Justfile: "default:\n\techo ok",
|
||||||
path.push("Justfile");
|
a: {
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
b: {
|
||||||
path.pop();
|
c: {
|
||||||
|
d: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
path.push("a");
|
search_test(tmp.path().join("a/b/c/d"), &[]);
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("b");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("c");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
path.push("d");
|
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
|
|
||||||
search_test(path, &[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_upwards_path_argument() {
|
fn test_upwards_path_argument() {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
let mut path = tmp.path().to_path_buf();
|
justfile: "default:\n\techo ok",
|
||||||
path.push("justfile");
|
a: {
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
justfile: "default:\n\techo bad",
|
||||||
path.pop();
|
},
|
||||||
|
};
|
||||||
|
|
||||||
path.push("a");
|
search_test(&tmp.path().join("a"), &["../"]);
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
search_test(&tmp.path().join("a"), &["../default"]);
|
||||||
|
|
||||||
path.push("justfile");
|
|
||||||
fs::write(&path, "default:\n\techo bad").unwrap();
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
search_test(&path, &["../"]);
|
|
||||||
search_test(&path, &["../default"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_downwards_path_argument() {
|
fn test_downwards_path_argument() {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
let mut path = tmp.path().to_path_buf();
|
justfile: "default:\n\techo bad",
|
||||||
path.push("justfile");
|
a: {
|
||||||
fs::write(&path, "default:\n\techo bad").unwrap();
|
justfile: "default:\n\techo ok",
|
||||||
path.pop();
|
},
|
||||||
|
};
|
||||||
|
|
||||||
path.push("a");
|
let path = tmp.path();
|
||||||
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
|
|
||||||
|
|
||||||
path.push("justfile");
|
|
||||||
fs::write(&path, "default:\n\techo ok").unwrap();
|
|
||||||
path.pop();
|
|
||||||
path.pop();
|
|
||||||
|
|
||||||
search_test(&path, &["a/"]);
|
search_test(&path, &["a/"]);
|
||||||
search_test(&path, &["a/default"]);
|
search_test(&path, &["a/default"]);
|
||||||
@ -108,3 +84,40 @@ fn test_downwards_path_argument() {
|
|||||||
search_test(&path, &["./a/"]);
|
search_test(&path, &["./a/"]);
|
||||||
search_test(&path, &["./a/default"]);
|
search_test(&path, &["./a/default"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_upwards_multiple_path_argument() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: "default:\n\techo ok",
|
||||||
|
a: {
|
||||||
|
b: {
|
||||||
|
justfile: "default:\n\techo bad",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = tmp.path().join("a").join("b");
|
||||||
|
search_test(&path, &["../../"]);
|
||||||
|
search_test(&path, &["../../default"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_downwards_multiple_path_argument() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: "default:\n\techo bad",
|
||||||
|
a: {
|
||||||
|
b: {
|
||||||
|
justfile: "default:\n\techo ok",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = tmp.path();
|
||||||
|
|
||||||
|
search_test(&path, &["a/b/"]);
|
||||||
|
search_test(&path, &["a/b/default"]);
|
||||||
|
search_test(&path, &["./a/b/"]);
|
||||||
|
search_test(&path, &["./a/b/default"]);
|
||||||
|
search_test(&path, &["./a/b/"]);
|
||||||
|
search_test(&path, &["./a/b/default"]);
|
||||||
|
}
|
||||||
|
40
tests/shell.rs
Normal file
40
tests/shell.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use std::{process::Command, str};
|
||||||
|
|
||||||
|
use executable_path::executable_path;
|
||||||
|
|
||||||
|
use test_utilities::{assert_stdout, tmptree};
|
||||||
|
|
||||||
|
const JUSTFILE: &str = "
|
||||||
|
expression := `EXPRESSION`
|
||||||
|
|
||||||
|
recipe default=`DEFAULT`:
|
||||||
|
{{expression}}
|
||||||
|
{{default}}
|
||||||
|
RECIPE
|
||||||
|
";
|
||||||
|
|
||||||
|
/// Test that --shell correctly sets the shell
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn shell() {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
shell: "#!/usr/bin/env bash\necho \"$@\"",
|
||||||
|
};
|
||||||
|
|
||||||
|
let shell = tmp.path().join("shell");
|
||||||
|
|
||||||
|
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);
|
||||||
|
std::fs::set_permissions(&shell, permissions).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(tmp.path())
|
||||||
|
.arg("--shell")
|
||||||
|
.arg(shell)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let stdout = "-cu -cu EXPRESSION\n-cu -cu DEFAULT\n-cu RECIPE\n";
|
||||||
|
|
||||||
|
assert_stdout(&output, stdout);
|
||||||
|
}
|
@ -1,65 +1,186 @@
|
|||||||
use std::{error::Error, fs, process::Command};
|
use std::{error::Error, process::Command};
|
||||||
|
|
||||||
use executable_path::executable_path;
|
use executable_path::executable_path;
|
||||||
use test_utilities::tempdir;
|
use test_utilities::tmptree;
|
||||||
|
|
||||||
|
const JUSTFILE: &str = r#"
|
||||||
|
foo := `cat data`
|
||||||
|
|
||||||
|
linewise bar=`cat data`: shebang
|
||||||
|
echo expression: {{foo}}
|
||||||
|
echo default: {{bar}}
|
||||||
|
echo linewise: `cat data`
|
||||||
|
|
||||||
|
shebang:
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
echo "shebang:" `cat data`
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const DATA: &str = "OK";
|
||||||
|
|
||||||
|
const WANT: &str = "shebang: OK\nexpression: OK\ndefault: OK\nlinewise: OK\n";
|
||||||
|
|
||||||
/// Test that just runs with the correct working directory when invoked with
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
/// `--justfile` but not `--working-directory`
|
/// `--justfile` but not `--working-directory`
|
||||||
#[test]
|
#[test]
|
||||||
fn justfile_without_working_directory() -> Result<(), Box<dyn Error>> {
|
fn justfile_without_working_directory() -> Result<(), Box<dyn Error>> {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
let justfile = tmp.path().join("justfile");
|
justfile: JUSTFILE,
|
||||||
let data = tmp.path().join("data");
|
data: DATA,
|
||||||
fs::write(
|
};
|
||||||
&justfile,
|
|
||||||
"foo = `cat data`\ndefault:\n echo {{foo}}\n cat data",
|
|
||||||
)?;
|
|
||||||
fs::write(&data, "found it")?;
|
|
||||||
|
|
||||||
let output = Command::new(executable_path("just"))
|
let output = Command::new(executable_path("just"))
|
||||||
.arg("--justfile")
|
.arg("--justfile")
|
||||||
.arg(&justfile)
|
.arg(&tmp.path().join("justfile"))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
panic!()
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
assert_eq!(stdout, "found it\nfound it");
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
|
/// `--justfile` but not `--working-directory`, and justfile path has no
|
||||||
|
/// parent
|
||||||
|
#[test]
|
||||||
|
fn justfile_without_working_directory_relative() -> Result<(), Box<dyn Error>> {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
data: DATA,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(&tmp.path())
|
||||||
|
.arg("--justfile")
|
||||||
|
.arg("justfile")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test that just invokes commands from the directory in which the justfile is found
|
/// Test that just invokes commands from the directory in which the justfile is found
|
||||||
#[test]
|
#[test]
|
||||||
fn change_working_directory_to_justfile_parent() -> Result<(), Box<dyn Error>> {
|
fn change_working_directory_to_search_justfile_parent() -> Result<(), Box<dyn Error>> {
|
||||||
let tmp = tempdir();
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
let justfile = tmp.path().join("justfile");
|
data: DATA,
|
||||||
fs::write(
|
subdir: {},
|
||||||
&justfile,
|
};
|
||||||
"foo = `cat data`\ndefault:\n echo {{foo}}\n cat data",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let data = tmp.path().join("data");
|
|
||||||
fs::write(&data, "found it")?;
|
|
||||||
|
|
||||||
let subdir = tmp.path().join("subdir");
|
|
||||||
fs::create_dir(&subdir)?;
|
|
||||||
|
|
||||||
let output = Command::new(executable_path("just"))
|
let output = Command::new(executable_path("just"))
|
||||||
.current_dir(subdir)
|
.current_dir(tmp.path().join("subdir"))
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
panic!("just invocation failed: {}", output.status)
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
|
/// `--justfile` but not `--working-directory`
|
||||||
|
#[test]
|
||||||
|
fn justfile_and_working_directory() -> Result<(), Box<dyn Error>> {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
sub: {
|
||||||
|
data: DATA,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.arg("--justfile")
|
||||||
|
.arg(&tmp.path().join("justfile"))
|
||||||
|
.arg("--working-directory")
|
||||||
|
.arg(&tmp.path().join("sub"))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
assert_eq!(stdout, "found it\nfound it");
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
|
/// `--justfile` but not `--working-directory`
|
||||||
|
#[test]
|
||||||
|
fn search_dir_child() -> Result<(), Box<dyn Error>> {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
child: {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
data: DATA,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(&tmp.path())
|
||||||
|
.arg("child/")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that just runs with the correct working directory when invoked with
|
||||||
|
/// `--justfile` but not `--working-directory`
|
||||||
|
#[test]
|
||||||
|
fn search_dir_parent() -> Result<(), Box<dyn Error>> {
|
||||||
|
let tmp = tmptree! {
|
||||||
|
child: {
|
||||||
|
},
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
data: DATA,
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = Command::new(executable_path("just"))
|
||||||
|
.current_dir(&tmp.path().join("child"))
|
||||||
|
.arg("../")
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
eprintln!("{:?}", String::from_utf8_lossy(&output.stderr));
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(stdout, WANT);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user