1
1
mirror of https://github.com/casey/just.git synced 2024-11-23 11:04:09 +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:
Casey Rodarmor 2019-11-09 21:43:20 -08:00 committed by GitHub
parent 8279361b39
commit aefdcea7d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1926 additions and 897 deletions

76
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
macro_rules! die {
($($arg:tt)*) => {{
eprintln!($($arg)*);
std::process::exit(EXIT_FAILURE)
}};
}

7
src/error.rs Normal file
View 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
View 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())
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
#[derive(Copy, Clone)] #[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum UseColor { pub(crate) enum UseColor {
Auto, Auto,
Always, Always,

View File

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

View File

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

View File

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

View File

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

View File

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