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