Gargantuan refactor (#522)

- Instead of changing the current directory with `env::set_current_dir`
  to be implicitly inherited by subprocesses, we now use
  `Command::current_dir` to set it explicitly. This feels much better,
  since we aren't dependent on the implicit state of the process's
  current directory.

- Subcommand execution is much improved.

- Added a ton of tests for config parsing, config execution, working
  dir, and search dir.

- Error messages are improved. Many more will be colored.

- The Config is now onwed, instead of borrowing from the arguments and
  the `clap::ArgMatches` object. This is a huge ergonomic improvement,
  especially in tests, and I don't think anyone will notice.

- `--edit` now uses `$VISUAL`, `$EDITOR`, or `vim`, in that order,
  matching git, which I think is what most people will expect.

- Added a cute `tmptree!{}` macro, for creating temporary directories
  populated with directories and files for tests.

- Admitted that grammer is LL(k) and I don't know what `k` is.
This commit is contained in:
Casey Rodarmor 2019-11-09 21:43:20 -08:00 committed by GitHub
parent 8279361b39
commit aefdcea7d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1926 additions and 897 deletions

76
Cargo.lock generated
View File

@ -38,6 +38,26 @@ dependencies = [
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

7
src/error.rs Normal file
View File

@ -0,0 +1,7 @@
use crate::common::*;
pub(crate) trait Error: Display {
fn code(&self) -> i32 {
EXIT_FAILURE
}
}

22
src/error_result_ext.rs Normal file
View File

@ -0,0 +1,22 @@
use crate::common::*;
pub(crate) trait ErrorResultExt<T> {
fn eprint(self, color: Color) -> Result<T, i32>;
}
impl<T, E: Error> ErrorResultExt<T> for Result<T, E> {
fn eprint(self, color: Color) -> Result<T, i32> {
match self {
Ok(ok) => Ok(ok),
Err(error) => {
if color.stderr().active() {
eprintln!("{} {:#}", color.error().paint("error:"), error);
} else {
eprintln!("error: {}", error);
}
Err(error.code())
}
}
}
}

View File

@ -107,9 +107,8 @@ pub(crate) fn os_family(_context: &FunctionContext) -> Result<String, String> {
}
pub(crate) fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
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> {

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,19 @@
use crate::common::*;
pub(crate) struct LoadError<'path> {
pub(crate) path: &'path Path,
pub(crate) io_error: io::Error,
}
impl Error for LoadError<'_> {}
impl Display for LoadError<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"Failed to read justfile at `{}`: {}",
self.path.display(),
self.io_error
)
}
}

View File

@ -48,7 +48,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
&self,
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! {

View File

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

View File

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

View File

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

View File

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

View File

@ -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(&current_dir) {
Ok(name) => {
if config.subcommand == Subcommand::Edit {
return Subcommand::edit(&name);
}
text = match fs::read_to_string(&name) {
Err(error) => {
eprintln!("Error reading justfile: {}", error);
return Err(EXIT_FAILURE);
}
Ok(text) => text,
};
let parent = name.parent().unwrap();
if let Err(error) = env::set_current_dir(&parent) {
eprintln!(
"Error changing directory to {}: {}",
parent.display(),
error
);
return Err(EXIT_FAILURE);
}
}
Err(search_error) => {
eprintln!("{}", search_error);
return Err(EXIT_FAILURE);
}
}
}
let justfile = match Compiler::compile(&text) {
Err(error) => {
if config.color.stderr().active() {
eprintln!("{:#}", error);
} else {
eprintln!("{}", error);
}
return Err(EXIT_FAILURE);
}
Ok(justfile) => justfile,
};
for warning in &justfile.warnings {
if config.color.stderr().active() {
eprintln!("{:#}", warning);
} else {
eprintln!("{}", warning);
}
}
config.subcommand.run(&config, justfile)
config.run_subcommand()
}

View File

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

View File

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

@ -0,0 +1,21 @@
use crate::common::*;
/// Controls how `just` will search for the justfile.
#[derive(Debug, PartialEq)]
pub(crate) enum SearchConfig {
/// Recursively search for the justfile upwards from the
/// invocation directory to the root, setting the working
/// directory to the directory in which the justfile is
/// found.
FromInvocationDirectory,
/// As in `Invocation`, but start from `search_directory`.
FromSearchDirectory { search_directory: PathBuf },
/// Use user-specified justfile, with the working directory
/// set to the directory that contains it.
WithJustfile { justfile: PathBuf },
/// Use user-specified justfile and working directory.
WithJustfileAndWorkingDirectory {
justfile: PathBuf,
working_directory: PathBuf,
},
}

View File

@ -1,42 +1,41 @@
use crate::common::*;
#[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 {

View File

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

View File

@ -7,6 +7,17 @@ pub(crate) fn compile(text: &str) -> Justfile {
}
}
pub(crate) fn config(args: &[&str]) -> Config {
let mut args = Vec::from(args);
args.insert(0, "just");
let app = Config::app();
let matches = app.get_matches_from_safe(args).unwrap();
Config::from_matches(&matches).unwrap()
}
pub(crate) use test_utilities::{tempdir, unindent};
macro_rules! analysis_error {

View File

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

View File

@ -1,6 +1,6 @@
use Verbosity::*;
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Verbosity {
Taciturn,
Loquacious,

View File

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

@ -0,0 +1,116 @@
use std::{env, iter, process::Command, str};
use executable_path::executable_path;
use which::which;
use test_utilities::{assert_stdout, tmptree};
const JUSTFILE: &str = "Yooooooo, hopefully this never becomes valid syntax.";
/// Test that --edit doesn't require a valid justfile
#[test]
fn invalid_justfile() {
let tmp = tmptree! {
justfile: JUSTFILE,
};
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.output()
.unwrap();
assert!(!output.status.success());
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.arg("--edit")
.env("VISUAL", "cat")
.output()
.unwrap();
assert_stdout(&output, JUSTFILE);
}
/// Test that editor is $VISUAL, $EDITOR, or "vim" in that order
#[test]
fn editor_precedence() {
let tmp = tmptree! {
justfile: JUSTFILE,
};
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.arg("--edit")
.env("VISUAL", "cat")
.env("EDITOR", "this-command-doesnt-exist")
.output()
.unwrap();
assert_stdout(&output, JUSTFILE);
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.arg("--edit")
.env_remove("VISUAL")
.env("EDITOR", "cat")
.output()
.unwrap();
assert_stdout(&output, JUSTFILE);
let cat = which("cat").unwrap();
let vim = tmp.path().join(format!("vim{}", env::consts::EXE_SUFFIX));
#[cfg(unix)]
std::os::unix::fs::symlink(cat, vim).unwrap();
#[cfg(windows)]
std::os::windows::fs::symlink_file(cat, vim).unwrap();
let path = env::join_paths(
iter::once(tmp.path().to_owned()).chain(env::split_paths(&env::var_os("PATH").unwrap())),
)
.unwrap();
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.arg("--edit")
.env("PATH", path)
.env_remove("VISUAL")
.env_remove("EDITOR")
.output()
.unwrap();
assert_stdout(&output, JUSTFILE);
}
/// Test that editor working directory is the same as edited justfile
#[cfg(unix)]
#[test]
fn editor_working_directory() {
let tmp = tmptree! {
justfile: JUSTFILE,
child: {},
editor: "#!/usr/bin/env sh\ncat $1\npwd",
};
let editor = tmp.path().join("editor");
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);
std::fs::set_permissions(&editor, permissions).unwrap();
let output = Command::new(executable_path("just"))
.current_dir(tmp.path().join("child"))
.arg("--edit")
.env("VISUAL", &editor)
.output()
.unwrap();
let want = format!(
"{}{}\n",
JUSTFILE,
tmp.path().canonicalize().unwrap().display()
);
assert_stdout(&output, &want);
}

File diff suppressed because it is too large Load Diff

View File

@ -74,7 +74,7 @@ default:
fn interrupt_backtick() {
interrupt_test(
"
foo = `sleep 1`
foo := `sleep 1`
default:
@echo {{foo}}

View File

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

@ -0,0 +1,40 @@
use std::{process::Command, str};
use executable_path::executable_path;
use test_utilities::{assert_stdout, tmptree};
const JUSTFILE: &str = "
expression := `EXPRESSION`
recipe default=`DEFAULT`:
{{expression}}
{{default}}
RECIPE
";
/// Test that --shell correctly sets the shell
#[cfg(unix)]
#[test]
fn shell() {
let tmp = tmptree! {
justfile: JUSTFILE,
shell: "#!/usr/bin/env bash\necho \"$@\"",
};
let shell = tmp.path().join("shell");
let permissions = std::os::unix::fs::PermissionsExt::from_mode(0o700);
std::fs::set_permissions(&shell, permissions).unwrap();
let output = Command::new(executable_path("just"))
.current_dir(tmp.path())
.arg("--shell")
.arg(shell)
.output()
.unwrap();
let stdout = "-cu -cu EXPRESSION\n-cu -cu DEFAULT\n-cu RECIPE\n";
assert_stdout(&output, stdout);
}

View File

@ -1,65 +1,186 @@
use std::{error::Error, fs, process::Command};
use std::{error::Error, process::Command};
use executable_path::executable_path;
use 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(())
}