Add loader and refactor errors (#917)
This commit adds a `Loader` type, which can be used to load multiple source strings. This was done to support the work on modules, but coincidentally enabled consolidating errors, since now `Config::run` can take a `&Loader`, and in the event of an error, return and `Error` that borrows from loaded strings. Multiple error types have been consolidated, and a bunch of ad-hoc error printing was removed.
This commit is contained in:
parent
98457c05d7
commit
1b0fafea75
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -77,6 +77,15 @@ dependencies = [
|
|||||||
"vec_map",
|
"vec_map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cradle"
|
||||||
|
version = "0.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2352f0ca05779da0791a0ea204cc7bfddf83ee6e6277c919d8c0a5801d27f0e4"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@ -200,6 +209,7 @@ dependencies = [
|
|||||||
"atty",
|
"atty",
|
||||||
"camino",
|
"camino",
|
||||||
"clap",
|
"clap",
|
||||||
|
"cradle",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"derivative",
|
"derivative",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
@ -211,12 +221,14 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
"regex",
|
||||||
"snafu",
|
"snafu",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"target",
|
"target",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"temptree",
|
"temptree",
|
||||||
|
"typed-arena",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"which",
|
"which",
|
||||||
"yaml-rust",
|
"yaml-rust",
|
||||||
@ -426,6 +438,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "snafu"
|
name = "snafu"
|
||||||
version = "0.6.10"
|
version = "0.6.10"
|
||||||
@ -556,6 +574,12 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-arena"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -33,6 +33,7 @@ snafu = "0.6.0"
|
|||||||
strum_macros = "0.21.1"
|
strum_macros = "0.21.1"
|
||||||
target = "1.0.0"
|
target = "1.0.0"
|
||||||
tempfile = "3.0.0"
|
tempfile = "3.0.0"
|
||||||
|
typed-arena = "2.0.1"
|
||||||
unicode-width = "0.1.0"
|
unicode-width = "0.1.0"
|
||||||
|
|
||||||
[dependencies.ctrlc]
|
[dependencies.ctrlc]
|
||||||
@ -44,8 +45,10 @@ version = "0.21.0"
|
|||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
cradle = "0.0.13"
|
||||||
executable-path = "1.0.0"
|
executable-path = "1.0.0"
|
||||||
pretty_assertions = "0.7.0"
|
pretty_assertions = "0.7.0"
|
||||||
|
regex = "1.5.4"
|
||||||
temptree = "0.1.0"
|
temptree = "0.1.0"
|
||||||
which = "4.0.0"
|
which = "4.0.0"
|
||||||
yaml-rust = "0.4.5"
|
yaml-rust = "0.4.5"
|
||||||
|
4
justfile
4
justfile
@ -40,7 +40,7 @@ build:
|
|||||||
fmt:
|
fmt:
|
||||||
cargo +nightly fmt --all
|
cargo +nightly fmt --all
|
||||||
|
|
||||||
watch +COMMAND='test':
|
watch +COMMAND='ltest':
|
||||||
cargo watch --clear --exec "{{COMMAND}}"
|
cargo watch --clear --exec "{{COMMAND}}"
|
||||||
|
|
||||||
man:
|
man:
|
||||||
@ -61,7 +61,7 @@ version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.tom
|
|||||||
changes:
|
changes:
|
||||||
git log --pretty=format:%s >> CHANGELOG.md
|
git log --pretty=format:%s >> CHANGELOG.md
|
||||||
|
|
||||||
check: clippy test forbid
|
check: clippy fmt test forbid
|
||||||
git diff --no-ext-diff --quiet --exit-code
|
git diff --no-ext-diff --quiet --exit-code
|
||||||
grep '^\[{{ version }}\]' CHANGELOG.md
|
grep '^\[{{ version }}\]' CHANGELOG.md
|
||||||
cargo +nightly generate-lockfile -Z minimal-versions
|
cargo +nightly generate-lockfile -Z minimal-versions
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Analyzer<'src> {
|
pub(crate) struct Analyzer<'src> {
|
||||||
@ -11,11 +11,11 @@ pub(crate) struct Analyzer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Analyzer<'src> {
|
impl<'src> Analyzer<'src> {
|
||||||
pub(crate) fn analyze(ast: Ast<'src>) -> CompilationResult<'src, Justfile> {
|
pub(crate) fn analyze(ast: Ast<'src>) -> CompileResult<'src, Justfile> {
|
||||||
Analyzer::default().justfile(ast)
|
Analyzer::default().justfile(ast)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompilationResult<'src, Justfile<'src>> {
|
pub(crate) fn justfile(mut self, ast: Ast<'src>) -> CompileResult<'src, Justfile<'src>> {
|
||||||
for item in ast.items {
|
for item in ast.items {
|
||||||
match item {
|
match item {
|
||||||
Item::Alias(alias) => {
|
Item::Alias(alias) => {
|
||||||
@ -88,7 +88,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompilationResult<'src, ()> {
|
fn analyze_recipe(&self, recipe: &UnresolvedRecipe<'src>) -> CompileResult<'src, ()> {
|
||||||
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
|
if let Some(original) = self.recipes.get(recipe.name.lexeme()) {
|
||||||
return Err(recipe.name.token().error(DuplicateRecipe {
|
return Err(recipe.name.token().error(DuplicateRecipe {
|
||||||
recipe: original.name(),
|
recipe: original.name(),
|
||||||
@ -140,7 +140,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompilationResult<'src, ()> {
|
fn analyze_assignment(&self, assignment: &Assignment<'src>) -> CompileResult<'src, ()> {
|
||||||
if self.assignments.contains_key(assignment.name.lexeme()) {
|
if self.assignments.contains_key(assignment.name.lexeme()) {
|
||||||
return Err(assignment.name.token().error(DuplicateVariable {
|
return Err(assignment.name.token().error(DuplicateVariable {
|
||||||
variable: assignment.name.lexeme(),
|
variable: assignment.name.lexeme(),
|
||||||
@ -149,7 +149,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompilationResult<'src, ()> {
|
fn analyze_alias(&self, alias: &Alias<'src, Name<'src>>) -> CompileResult<'src, ()> {
|
||||||
let name = alias.name.lexeme();
|
let name = alias.name.lexeme();
|
||||||
|
|
||||||
if let Some(original) = self.aliases.get(name) {
|
if let Some(original) = self.aliases.get(name) {
|
||||||
@ -162,7 +162,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn analyze_set(&self, set: &Set<'src>) -> CompilationResult<'src, ()> {
|
fn analyze_set(&self, set: &Set<'src>) -> CompileResult<'src, ()> {
|
||||||
if let Some(original) = self.sets.get(set.name.lexeme()) {
|
if let Some(original) = self.sets.get(set.name.lexeme()) {
|
||||||
return Err(set.name.error(DuplicateSet {
|
return Err(set.name.error(DuplicateSet {
|
||||||
setting: original.name.lexeme(),
|
setting: original.name.lexeme(),
|
||||||
@ -176,7 +176,7 @@ impl<'src> Analyzer<'src> {
|
|||||||
fn resolve_alias(
|
fn resolve_alias(
|
||||||
recipes: &Table<'src, Rc<Recipe<'src>>>,
|
recipes: &Table<'src, Rc<Recipe<'src>>>,
|
||||||
alias: Alias<'src, Name<'src>>,
|
alias: Alias<'src, Name<'src>>,
|
||||||
) -> CompilationResult<'src, Alias<'src>> {
|
) -> CompileResult<'src, Alias<'src>> {
|
||||||
let token = alias.name.token();
|
let token = alias.name.token();
|
||||||
// Make sure the alias doesn't conflict with any recipe
|
// Make sure the alias doesn't conflict with any recipe
|
||||||
if let Some(recipe) = recipes.get(alias.name.lexeme()) {
|
if let Some(recipe) = recipes.get(alias.name.lexeme()) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
|
|
||||||
pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
|
pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
|
||||||
assignments: &'run Table<'src, Assignment<'src>>,
|
assignments: &'run Table<'src, Assignment<'src>>,
|
||||||
@ -11,7 +11,7 @@ pub(crate) struct AssignmentResolver<'src: 'run, 'run> {
|
|||||||
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
||||||
pub(crate) fn resolve_assignments(
|
pub(crate) fn resolve_assignments(
|
||||||
assignments: &Table<'src, Assignment<'src>>,
|
assignments: &Table<'src, Assignment<'src>>,
|
||||||
) -> CompilationResult<'src, ()> {
|
) -> CompileResult<'src, ()> {
|
||||||
let mut resolver = AssignmentResolver {
|
let mut resolver = AssignmentResolver {
|
||||||
stack: Vec::new(),
|
stack: Vec::new(),
|
||||||
evaluated: BTreeSet::new(),
|
evaluated: BTreeSet::new(),
|
||||||
@ -25,7 +25,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_assignment(&mut self, name: &'src str) -> CompilationResult<'src, ()> {
|
fn resolve_assignment(&mut self, name: &'src str) -> CompileResult<'src, ()> {
|
||||||
if self.evaluated.contains(name) {
|
if self.evaluated.contains(name) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -45,7 +45,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
length: 0,
|
length: 0,
|
||||||
kind: TokenKind::Unspecified,
|
kind: TokenKind::Unspecified,
|
||||||
};
|
};
|
||||||
return Err(CompilationError {
|
return Err(CompileError {
|
||||||
kind: Internal { message },
|
kind: Internal { message },
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
@ -56,7 +56,7 @@ impl<'src: 'run, 'run> AssignmentResolver<'src, 'run> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompilationResult<'src, ()> {
|
fn resolve_expression(&mut self, expression: &Expression<'src>) -> CompileResult<'src, ()> {
|
||||||
match expression {
|
match expression {
|
||||||
Expression::Variable { name } => {
|
Expression::Variable { name } => {
|
||||||
let variable = name.lexeme();
|
let variable = name.lexeme();
|
||||||
|
@ -2,8 +2,7 @@ use crate::common::*;
|
|||||||
|
|
||||||
/// The top-level type produced by the parser. Not all successful parses result
|
/// The top-level type produced by the parser. Not all successful parses result
|
||||||
/// in valid justfiles, so additional consistency checks and name resolution
|
/// in valid justfiles, so additional consistency checks and name resolution
|
||||||
/// are performed by the `Analyzer`, which produces a `Justfile` from an
|
/// are performed by the `Analyzer`, which produces a `Justfile` from an `Ast`.
|
||||||
/// `Ast`.
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct Ast<'src> {
|
pub(crate) struct Ast<'src> {
|
||||||
/// Items in the justfile
|
/// Items in the justfile
|
||||||
|
@ -11,7 +11,7 @@ pub(crate) use std::{
|
|||||||
mem,
|
mem,
|
||||||
ops::{Index, Range, RangeInclusive},
|
ops::{Index, Range, RangeInclusive},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{self, Command, Stdio},
|
process::{self, Command, ExitStatus, Stdio},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
str::{self, Chars},
|
str::{self, Chars},
|
||||||
sync::{Mutex, MutexGuard},
|
sync::{Mutex, MutexGuard},
|
||||||
@ -27,6 +27,7 @@ pub(crate) use libc::EXIT_FAILURE;
|
|||||||
pub(crate) use log::{info, warn};
|
pub(crate) use log::{info, warn};
|
||||||
pub(crate) use snafu::{ResultExt, Snafu};
|
pub(crate) use snafu::{ResultExt, Snafu};
|
||||||
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
pub(crate) use strum::{Display, EnumString, IntoStaticStr};
|
||||||
|
pub(crate) use typed_arena::Arena;
|
||||||
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
pub(crate) use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
// modules
|
// modules
|
||||||
@ -37,24 +38,23 @@ pub(crate) use crate::{load_dotenv::load_dotenv, output::output, unindent::unind
|
|||||||
|
|
||||||
// traits
|
// traits
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
command_ext::CommandExt, error::Error, error_result_ext::ErrorResultExt, keyed::Keyed,
|
command_ext::CommandExt, keyed::Keyed, ordinal::Ordinal, platform_interface::PlatformInterface,
|
||||||
ordinal::Ordinal, platform_interface::PlatformInterface, range_ext::RangeExt,
|
range_ext::RangeExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
alias::Alias, analyzer::Analyzer, assignment::Assignment,
|
||||||
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
|
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
|
||||||
compilation_error::CompilationError, compilation_error_kind::CompilationErrorKind,
|
compile_error::CompileError, compile_error_kind::CompileErrorKind, config::Config,
|
||||||
config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter,
|
config_error::ConfigError, count::Count, delimiter::Delimiter, dependency::Dependency,
|
||||||
dependency::Dependency, enclosure::Enclosure, evaluator::Evaluator, expression::Expression,
|
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
|
||||||
fragment::Fragment, function::Function, function_context::FunctionContext,
|
fragment::Fragment, function::Function, function_context::FunctionContext,
|
||||||
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
|
||||||
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List,
|
justfile::Justfile, keyword::Keyword, lexer::Lexer, line::Line, list::List, loader::Loader,
|
||||||
load_error::LoadError, name::Name, output_error::OutputError, parameter::Parameter,
|
name::Name, output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind,
|
||||||
parameter_kind::ParameterKind, parser::Parser, platform::Platform, position::Position,
|
parser::Parser, platform::Platform, position::Position, positional::Positional, recipe::Recipe,
|
||||||
positional::Positional, recipe::Recipe, recipe_context::RecipeContext,
|
recipe_context::RecipeContext, recipe_resolver::RecipeResolver, scope::Scope, search::Search,
|
||||||
recipe_resolver::RecipeResolver, runtime_error::RuntimeError, scope::Scope, search::Search,
|
|
||||||
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
search_config::SearchConfig, search_error::SearchError, set::Set, setting::Setting,
|
||||||
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind,
|
settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, string_kind::StringKind,
|
||||||
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
string_literal::StringLiteral, subcommand::Subcommand, suggestion::Suggestion, table::Table,
|
||||||
@ -64,9 +64,9 @@ pub(crate) use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
pub(crate) type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
|
pub(crate) type CompileResult<'a, T> = Result<T, CompileError<'a>>;
|
||||||
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
pub(crate) type ConfigResult<T> = Result<T, ConfigError>;
|
||||||
pub(crate) type RunResult<'a, T> = Result<T, RuntimeError<'a>>;
|
pub(crate) type RunResult<'a, T> = Result<T, Error<'a>>;
|
||||||
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
pub(crate) type SearchResult<T> = Result<T, SearchError>;
|
||||||
|
|
||||||
// modules used in tests
|
// modules used in tests
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) struct CompilationError<'src> {
|
pub(crate) struct CompileError<'src> {
|
||||||
pub(crate) token: Token<'src>,
|
pub(crate) token: Token<'src>,
|
||||||
pub(crate) kind: CompilationErrorKind<'src>,
|
pub(crate) kind: CompileErrorKind<'src>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for CompilationError<'_> {}
|
impl<'src> CompileError<'src> {
|
||||||
|
pub(crate) fn context(&self) -> Token<'src> {
|
||||||
|
self.token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for CompilationError<'_> {
|
impl Display for CompileError<'_> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
let message = Color::fmt(f).message();
|
|
||||||
|
|
||||||
write!(f, "{}", message.prefix())?;
|
|
||||||
|
|
||||||
match &self.kind {
|
match &self.kind {
|
||||||
AliasShadowsRecipe { alias, recipe_line } => {
|
AliasShadowsRecipe { alias, recipe_line } => {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Alias `{}` defined on line {} shadows recipe `{}` defined on line {}",
|
"Alias `{}` defined on line {} shadows recipe `{}` defined on line {}",
|
||||||
alias,
|
alias,
|
||||||
@ -27,13 +28,13 @@ impl Display for CompilationError<'_> {
|
|||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
BacktickShebang => {
|
BacktickShebang => {
|
||||||
writeln!(f, "Backticks may not start with `#!`")?;
|
write!(f, "Backticks may not start with `#!`")?;
|
||||||
},
|
},
|
||||||
CircularRecipeDependency { recipe, ref circle } =>
|
CircularRecipeDependency { recipe, ref circle } =>
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Recipe `{}` depends on itself", recipe)?;
|
write!(f, "Recipe `{}` depends on itself", recipe)?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Recipe `{}` has circular dependency `{}`",
|
"Recipe `{}` has circular dependency `{}`",
|
||||||
recipe,
|
recipe,
|
||||||
@ -45,79 +46,15 @@ impl Display for CompilationError<'_> {
|
|||||||
ref circle,
|
ref circle,
|
||||||
} =>
|
} =>
|
||||||
if circle.len() == 2 {
|
if circle.len() == 2 {
|
||||||
writeln!(f, "Variable `{}` is defined in terms of itself", variable)?;
|
write!(f, "Variable `{}` is defined in terms of itself", variable)?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Variable `{}` depends on its own value: `{}`",
|
"Variable `{}` depends on its own value: `{}`",
|
||||||
variable,
|
variable,
|
||||||
circle.join(" -> ")
|
circle.join(" -> ")
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
|
|
||||||
InvalidEscapeSequence { character } => {
|
|
||||||
let representation = match character {
|
|
||||||
'`' => r"\`".to_owned(),
|
|
||||||
'\\' => r"\".to_owned(),
|
|
||||||
'\'' => r"'".to_owned(),
|
|
||||||
'"' => r#"""#.to_owned(),
|
|
||||||
_ => character.escape_default().collect(),
|
|
||||||
};
|
|
||||||
writeln!(f, "`\\{}` is not a valid escape sequence", representation)?;
|
|
||||||
},
|
|
||||||
DeprecatedEquals => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"`=` in assignments, exports, and aliases has been phased out on favor of `:=`"
|
|
||||||
)?;
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Please see this issue for more details: https://github.com/casey/just/issues/379"
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
DuplicateParameter { recipe, parameter } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` has duplicate parameter `{}`",
|
|
||||||
recipe, parameter
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
DuplicateVariable { variable } => {
|
|
||||||
writeln!(f, "Variable `{}` has multiple definitions", variable)?;
|
|
||||||
},
|
|
||||||
UnexpectedToken {
|
|
||||||
ref expected,
|
|
||||||
found,
|
|
||||||
} => {
|
|
||||||
writeln!(f, "Expected {}, but found {}", List::or(expected), found)?;
|
|
||||||
},
|
|
||||||
DuplicateAlias { alias, first } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Alias `{}` first defined on line {} is redefined on line {}",
|
|
||||||
alias,
|
|
||||||
first.ordinal(),
|
|
||||||
self.token.line.ordinal(),
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
DuplicateRecipe { recipe, first } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` first defined on line {} is redefined on line {}",
|
|
||||||
recipe,
|
|
||||||
first.ordinal(),
|
|
||||||
self.token.line.ordinal()
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
DuplicateSet { setting, first } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Setting `{}` first set on line {} is redefined on line {}",
|
|
||||||
setting,
|
|
||||||
first.ordinal(),
|
|
||||||
self.token.line.ordinal(),
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
DependencyArgumentCountMismatch {
|
DependencyArgumentCountMismatch {
|
||||||
dependency,
|
dependency,
|
||||||
found,
|
found,
|
||||||
@ -134,53 +71,75 @@ impl Display for CompilationError<'_> {
|
|||||||
|
|
||||||
if min == max {
|
if min == max {
|
||||||
let expected = min;
|
let expected = min;
|
||||||
writeln!(f, "{} {}", expected, Count("argument", *expected))?;
|
write!(f, "{} {}", expected, Count("argument", *expected))?;
|
||||||
} else if found < min {
|
} else if found < min {
|
||||||
writeln!(f, "at least {} {}", min, Count("argument", *min))?;
|
write!(f, "at least {} {}", min, Count("argument", *min))?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(f, "at most {} {}", max, Count("argument", *max))?;
|
write!(f, "at most {} {}", max, Count("argument", *max))?;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ExpectedKeyword { expected, found } => writeln!(
|
DeprecatedEquals => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"`=` in assignments, exports, and aliases has been phased out on favor of `:=`"
|
||||||
|
)?;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Please see this issue for more details: https://github.com/casey/just/issues/379"
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
DuplicateAlias { alias, first } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Alias `{}` first defined on line {} is redefined on line {}",
|
||||||
|
alias,
|
||||||
|
first.ordinal(),
|
||||||
|
self.token.line.ordinal(),
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
DuplicateParameter { recipe, parameter } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` has duplicate parameter `{}`",
|
||||||
|
recipe, parameter
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
DuplicateRecipe { recipe, first } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` first defined on line {} is redefined on line {}",
|
||||||
|
recipe,
|
||||||
|
first.ordinal(),
|
||||||
|
self.token.line.ordinal()
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
DuplicateSet { setting, first } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Setting `{}` first set on line {} is redefined on line {}",
|
||||||
|
setting,
|
||||||
|
first.ordinal(),
|
||||||
|
self.token.line.ordinal(),
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
DuplicateVariable { variable } => {
|
||||||
|
write!(f, "Variable `{}` has multiple definitions", variable)?;
|
||||||
|
},
|
||||||
|
ExpectedKeyword { expected, found } => write!(
|
||||||
f,
|
f,
|
||||||
"Expected keyword {} but found identifier `{}`",
|
"Expected keyword {} but found identifier `{}`",
|
||||||
List::or_ticked(expected),
|
List::or_ticked(expected),
|
||||||
found
|
found
|
||||||
)?,
|
)?,
|
||||||
ParameterShadowsVariable { parameter } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Parameter `{}` shadows variable of the same name",
|
|
||||||
parameter
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
RequiredParameterFollowsDefaultParameter { parameter } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Non-default parameter `{}` follows default parameter",
|
|
||||||
parameter
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
ParameterFollowsVariadicParameter { parameter } => {
|
|
||||||
writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?;
|
|
||||||
},
|
|
||||||
MixedLeadingWhitespace { whitespace } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \
|
|
||||||
consist of tabs or spaces, but not both",
|
|
||||||
ShowWhitespace(whitespace)
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
ExtraLeadingWhitespace => {
|
ExtraLeadingWhitespace => {
|
||||||
writeln!(f, "Recipe line has extra leading whitespace")?;
|
write!(f, "Recipe line has extra leading whitespace")?;
|
||||||
},
|
},
|
||||||
FunctionArgumentCountMismatch {
|
FunctionArgumentCountMismatch {
|
||||||
function,
|
function,
|
||||||
found,
|
found,
|
||||||
expected,
|
expected,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Function `{}` called with {} {} but takes {}",
|
"Function `{}` called with {} {} but takes {}",
|
||||||
function,
|
function,
|
||||||
@ -190,7 +149,7 @@ impl Display for CompilationError<'_> {
|
|||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
InconsistentLeadingWhitespace { expected, found } => {
|
InconsistentLeadingWhitespace { expected, found } => {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
|
"Recipe line has inconsistent leading whitespace. Recipe started with `{}` but found \
|
||||||
line with `{}`",
|
line with `{}`",
|
||||||
@ -198,40 +157,30 @@ impl Display for CompilationError<'_> {
|
|||||||
ShowWhitespace(found)
|
ShowWhitespace(found)
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
UnknownAliasTarget { alias, target } => {
|
Internal { ref message } => {
|
||||||
writeln!(f, "Alias `{}` has an unknown target `{}`", alias, target)?;
|
write!(
|
||||||
},
|
|
||||||
UnknownDependency { recipe, unknown } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
f,
|
||||||
"Recipe `{}` has unknown dependency `{}`",
|
"Internal error, this may indicate a bug in just: {}\n\
|
||||||
recipe, unknown
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
|
message
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
UndefinedVariable { variable } => {
|
InvalidEscapeSequence { character } => {
|
||||||
writeln!(f, "Variable `{}` not defined", variable)?;
|
let representation = match character {
|
||||||
},
|
'`' => r"\`".to_owned(),
|
||||||
UnknownFunction { function } => {
|
'\\' => r"\".to_owned(),
|
||||||
writeln!(f, "Call to unknown function `{}`", function)?;
|
'\'' => r"'".to_owned(),
|
||||||
},
|
'"' => r#"""#.to_owned(),
|
||||||
UnknownSetting { setting } => {
|
_ => character.escape_default().collect(),
|
||||||
writeln!(f, "Unknown setting `{}`", setting)?;
|
};
|
||||||
},
|
write!(f, "`\\{}` is not a valid escape sequence", representation)?;
|
||||||
UnexpectedCharacter { expected } => {
|
|
||||||
writeln!(f, "Expected character `{}`", expected)?;
|
|
||||||
},
|
|
||||||
UnknownStartOfToken => {
|
|
||||||
writeln!(f, "Unknown start of token:")?;
|
|
||||||
},
|
|
||||||
UnexpectedEndOfToken { expected } => {
|
|
||||||
writeln!(f, "Expected character `{}` but found end-of-file", expected)?;
|
|
||||||
},
|
},
|
||||||
MismatchedClosingDelimiter {
|
MismatchedClosingDelimiter {
|
||||||
open,
|
open,
|
||||||
open_line,
|
open_line,
|
||||||
close,
|
close,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)",
|
"Mismatched closing delimiter `{}`. (Did you mean to close the `{}` on line {}?)",
|
||||||
close.close(),
|
close.close(),
|
||||||
@ -239,33 +188,82 @@ impl Display for CompilationError<'_> {
|
|||||||
open_line.ordinal(),
|
open_line.ordinal(),
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
UnexpectedClosingDelimiter { close } => {
|
MixedLeadingWhitespace { whitespace } => {
|
||||||
writeln!(f, "Unexpected closing delimiter `{}`", close.close())?;
|
write!(
|
||||||
},
|
|
||||||
UnpairedCarriageReturn => {
|
|
||||||
writeln!(f, "Unpaired carriage return")?;
|
|
||||||
},
|
|
||||||
UnterminatedInterpolation => {
|
|
||||||
writeln!(f, "Unterminated interpolation")?;
|
|
||||||
},
|
|
||||||
UnterminatedString => {
|
|
||||||
writeln!(f, "Unterminated string")?;
|
|
||||||
},
|
|
||||||
UnterminatedBacktick => {
|
|
||||||
writeln!(f, "Unterminated backtick")?;
|
|
||||||
},
|
|
||||||
Internal { ref message } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
f,
|
||||||
"Internal error, this may indicate a bug in just: {}\n\
|
"Found a mix of tabs and spaces in leading whitespace: `{}`\nLeading whitespace may \
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
consist of tabs or spaces, but not both",
|
||||||
message
|
ShowWhitespace(whitespace)
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
|
ParameterFollowsVariadicParameter { parameter } => {
|
||||||
|
write!(f, "Parameter `{}` follows variadic parameter", parameter)?;
|
||||||
|
},
|
||||||
|
ParameterShadowsVariable { parameter } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Parameter `{}` shadows variable of the same name",
|
||||||
|
parameter
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
RequiredParameterFollowsDefaultParameter { parameter } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Non-default parameter `{}` follows default parameter",
|
||||||
|
parameter
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
UndefinedVariable { variable } => {
|
||||||
|
write!(f, "Variable `{}` not defined", variable)?;
|
||||||
|
},
|
||||||
|
UnexpectedCharacter { expected } => {
|
||||||
|
write!(f, "Expected character `{}`", expected)?;
|
||||||
|
},
|
||||||
|
UnexpectedClosingDelimiter { close } => {
|
||||||
|
write!(f, "Unexpected closing delimiter `{}`", close.close())?;
|
||||||
|
},
|
||||||
|
UnexpectedEndOfToken { expected } => {
|
||||||
|
write!(f, "Expected character `{}` but found end-of-file", expected)?;
|
||||||
|
},
|
||||||
|
UnexpectedToken {
|
||||||
|
ref expected,
|
||||||
|
found,
|
||||||
|
} => {
|
||||||
|
write!(f, "Expected {}, but found {}", List::or(expected), found)?;
|
||||||
|
},
|
||||||
|
UnknownAliasTarget { alias, target } => {
|
||||||
|
write!(f, "Alias `{}` has an unknown target `{}`", alias, target)?;
|
||||||
|
},
|
||||||
|
UnknownDependency { recipe, unknown } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` has unknown dependency `{}`",
|
||||||
|
recipe, unknown
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
UnknownFunction { function } => {
|
||||||
|
write!(f, "Call to unknown function `{}`", function)?;
|
||||||
|
},
|
||||||
|
UnknownSetting { setting } => {
|
||||||
|
write!(f, "Unknown setting `{}`", setting)?;
|
||||||
|
},
|
||||||
|
UnknownStartOfToken => {
|
||||||
|
write!(f, "Unknown start of token:")?;
|
||||||
|
},
|
||||||
|
UnpairedCarriageReturn => {
|
||||||
|
write!(f, "Unpaired carriage return")?;
|
||||||
|
},
|
||||||
|
UnterminatedBacktick => {
|
||||||
|
write!(f, "Unterminated backtick")?;
|
||||||
|
},
|
||||||
|
UnterminatedInterpolation => {
|
||||||
|
write!(f, "Unterminated interpolation")?;
|
||||||
|
},
|
||||||
|
UnterminatedString => {
|
||||||
|
write!(f, "Unterminated string")?;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "{}", message.suffix())?;
|
Ok(())
|
||||||
|
|
||||||
self.token.write_context(f, Color::fmt(f).error())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub(crate) enum CompilationErrorKind<'src> {
|
pub(crate) enum CompileErrorKind<'src> {
|
||||||
AliasShadowsRecipe {
|
AliasShadowsRecipe {
|
||||||
alias: &'src str,
|
alias: &'src str,
|
||||||
recipe_line: usize,
|
recipe_line: usize,
|
||||||
@ -34,13 +34,13 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
first: usize,
|
first: usize,
|
||||||
},
|
},
|
||||||
DuplicateVariable {
|
|
||||||
variable: &'src str,
|
|
||||||
},
|
|
||||||
DuplicateSet {
|
DuplicateSet {
|
||||||
setting: &'src str,
|
setting: &'src str,
|
||||||
first: usize,
|
first: usize,
|
||||||
},
|
},
|
||||||
|
DuplicateVariable {
|
||||||
|
variable: &'src str,
|
||||||
|
},
|
||||||
ExpectedKeyword {
|
ExpectedKeyword {
|
||||||
expected: Vec<Keyword>,
|
expected: Vec<Keyword>,
|
||||||
found: &'src str,
|
found: &'src str,
|
||||||
@ -61,6 +61,11 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
InvalidEscapeSequence {
|
InvalidEscapeSequence {
|
||||||
character: char,
|
character: char,
|
||||||
},
|
},
|
||||||
|
MismatchedClosingDelimiter {
|
||||||
|
close: Delimiter,
|
||||||
|
open: Delimiter,
|
||||||
|
open_line: usize,
|
||||||
|
},
|
||||||
MixedLeadingWhitespace {
|
MixedLeadingWhitespace {
|
||||||
whitespace: &'src str,
|
whitespace: &'src str,
|
||||||
},
|
},
|
||||||
@ -76,6 +81,15 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
UndefinedVariable {
|
UndefinedVariable {
|
||||||
variable: &'src str,
|
variable: &'src str,
|
||||||
},
|
},
|
||||||
|
UnexpectedCharacter {
|
||||||
|
expected: char,
|
||||||
|
},
|
||||||
|
UnexpectedClosingDelimiter {
|
||||||
|
close: Delimiter,
|
||||||
|
},
|
||||||
|
UnexpectedEndOfToken {
|
||||||
|
expected: char,
|
||||||
|
},
|
||||||
UnexpectedToken {
|
UnexpectedToken {
|
||||||
expected: Vec<TokenKind>,
|
expected: Vec<TokenKind>,
|
||||||
found: TokenKind,
|
found: TokenKind,
|
||||||
@ -91,26 +105,12 @@ pub(crate) enum CompilationErrorKind<'src> {
|
|||||||
UnknownFunction {
|
UnknownFunction {
|
||||||
function: &'src str,
|
function: &'src str,
|
||||||
},
|
},
|
||||||
UnknownStartOfToken,
|
|
||||||
UnexpectedCharacter {
|
|
||||||
expected: char,
|
|
||||||
},
|
|
||||||
UnexpectedEndOfToken {
|
|
||||||
expected: char,
|
|
||||||
},
|
|
||||||
UnknownSetting {
|
UnknownSetting {
|
||||||
setting: &'src str,
|
setting: &'src str,
|
||||||
},
|
},
|
||||||
|
UnknownStartOfToken,
|
||||||
UnpairedCarriageReturn,
|
UnpairedCarriageReturn,
|
||||||
UnexpectedClosingDelimiter {
|
UnterminatedBacktick,
|
||||||
close: Delimiter,
|
|
||||||
},
|
|
||||||
MismatchedClosingDelimiter {
|
|
||||||
close: Delimiter,
|
|
||||||
open: Delimiter,
|
|
||||||
open_line: usize,
|
|
||||||
},
|
|
||||||
UnterminatedInterpolation,
|
UnterminatedInterpolation,
|
||||||
UnterminatedString,
|
UnterminatedString,
|
||||||
UnterminatedBacktick,
|
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ use crate::common::*;
|
|||||||
pub(crate) struct Compiler;
|
pub(crate) struct Compiler;
|
||||||
|
|
||||||
impl Compiler {
|
impl Compiler {
|
||||||
pub(crate) fn compile(src: &str) -> CompilationResult<Justfile> {
|
pub(crate) fn compile(src: &str) -> CompileResult<Justfile> {
|
||||||
let tokens = Lexer::lex(src)?;
|
let tokens = Lexer::lex(src)?;
|
||||||
|
|
||||||
let ast = Parser::parse(&tokens)?;
|
let ast = Parser::parse(&tokens)?;
|
||||||
|
214
src/config.rs
214
src/config.rs
@ -532,7 +532,7 @@ impl Config {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run_subcommand(self) -> Result<(), i32> {
|
pub(crate) fn run_subcommand<'src>(self, loader: &'src Loader) -> Result<(), Error<'src>> {
|
||||||
use Subcommand::*;
|
use Subcommand::*;
|
||||||
|
|
||||||
if self.subcommand == Init {
|
if self.subcommand == Init {
|
||||||
@ -540,34 +540,24 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Completions { shell } = self.subcommand {
|
if let Completions { shell } = self.subcommand {
|
||||||
return Subcommand::completions(self.verbosity, &shell);
|
return Subcommand::completions(&shell);
|
||||||
}
|
}
|
||||||
|
|
||||||
let search =
|
let search = Search::find(&self.search_config, &self.invocation_directory)?;
|
||||||
Search::find(&self.search_config, &self.invocation_directory).eprint(self.color)?;
|
|
||||||
|
|
||||||
if self.subcommand == Edit {
|
if self.subcommand == Edit {
|
||||||
return self.edit(&search);
|
return Self::edit(&search);
|
||||||
}
|
}
|
||||||
|
|
||||||
let src = fs::read_to_string(&search.justfile)
|
let src = loader.load(&search.justfile)?;
|
||||||
.map_err(|io_error| LoadError {
|
|
||||||
io_error,
|
|
||||||
path: &search.justfile,
|
|
||||||
})
|
|
||||||
.eprint(self.color)?;
|
|
||||||
|
|
||||||
let tokens = Lexer::lex(&src).eprint(self.color)?;
|
let tokens = Lexer::lex(&src)?;
|
||||||
let ast = Parser::parse(&tokens).eprint(self.color)?;
|
let ast = Parser::parse(&tokens)?;
|
||||||
let justfile = Analyzer::analyze(ast.clone()).eprint(self.color)?;
|
let justfile = Analyzer::analyze(ast.clone())?;
|
||||||
|
|
||||||
if self.verbosity.loud() {
|
if self.verbosity.loud() {
|
||||||
for warning in &justfile.warnings {
|
for warning in &justfile.warnings {
|
||||||
if self.color.stderr().active() {
|
warning.write(&mut io::stderr(), self.color.stderr()).ok();
|
||||||
eprintln!("{:#}", warning);
|
|
||||||
} else {
|
|
||||||
eprintln!("{}", warning);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,7 +565,7 @@ impl Config {
|
|||||||
Choose { overrides, chooser } =>
|
Choose { overrides, chooser } =>
|
||||||
self.choose(justfile, &search, overrides, chooser.as_deref())?,
|
self.choose(justfile, &search, overrides, chooser.as_deref())?,
|
||||||
Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
Command { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
||||||
Dump => Self::dump(ast)?,
|
Dump => Self::dump(ast),
|
||||||
Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
Evaluate { overrides, .. } => self.run(justfile, &search, overrides, &[])?,
|
||||||
Format => self.format(ast, &search)?,
|
Format => self.format(ast, &search)?,
|
||||||
List => self.list(justfile),
|
List => self.list(justfile),
|
||||||
@ -583,7 +573,7 @@ impl Config {
|
|||||||
arguments,
|
arguments,
|
||||||
overrides,
|
overrides,
|
||||||
} => self.run(justfile, &search, overrides, arguments)?,
|
} => self.run(justfile, &search, overrides, arguments)?,
|
||||||
Show { ref name } => self.show(&name, justfile)?,
|
Show { ref name } => Self::show(&name, justfile)?,
|
||||||
Summary => self.summary(justfile),
|
Summary => self.summary(justfile),
|
||||||
Variables => Self::variables(justfile),
|
Variables => Self::variables(justfile),
|
||||||
Completions { .. } | Edit | Init => unreachable!(),
|
Completions { .. } | Edit | Init => unreachable!(),
|
||||||
@ -592,13 +582,13 @@ impl Config {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn choose(
|
fn choose<'src>(
|
||||||
&self,
|
&self,
|
||||||
justfile: Justfile,
|
justfile: Justfile<'src>,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
chooser: Option<&str>,
|
chooser: Option<&str>,
|
||||||
) -> Result<(), i32> {
|
) -> Result<(), Error<'src>> {
|
||||||
let recipes = justfile
|
let recipes = justfile
|
||||||
.public_recipes(self.unsorted)
|
.public_recipes(self.unsorted)
|
||||||
.iter()
|
.iter()
|
||||||
@ -607,10 +597,7 @@ impl Config {
|
|||||||
.collect::<Vec<&Recipe<Dependency>>>();
|
.collect::<Vec<&Recipe<Dependency>>>();
|
||||||
|
|
||||||
if recipes.is_empty() {
|
if recipes.is_empty() {
|
||||||
if self.verbosity.loud() {
|
return Err(Error::NoChoosableRecipes);
|
||||||
eprintln!("Justfile contains no choosable recipes.");
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let chooser = chooser
|
let chooser = chooser
|
||||||
@ -629,61 +616,39 @@ impl Config {
|
|||||||
|
|
||||||
let mut child = match result {
|
let mut child = match result {
|
||||||
Ok(child) => child,
|
Ok(child) => child,
|
||||||
Err(error) => {
|
Err(io_error) => {
|
||||||
if self.verbosity.loud() {
|
return Err(Error::ChooserInvoke {
|
||||||
eprintln!(
|
shell_binary: justfile.settings.shell_binary(self).to_owned(),
|
||||||
"Chooser `{} {} {}` invocation failed: {}",
|
shell_arguments: justfile.settings.shell_arguments(self).join(" "),
|
||||||
justfile.settings.shell_binary(self),
|
chooser,
|
||||||
justfile.settings.shell_arguments(self).join(" "),
|
io_error,
|
||||||
chooser.to_string_lossy(),
|
});
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for recipe in recipes {
|
for recipe in recipes {
|
||||||
if let Err(error) = child
|
if let Err(io_error) = child
|
||||||
.stdin
|
.stdin
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.expect("Child was created with piped stdio")
|
.expect("Child was created with piped stdio")
|
||||||
.write_all(format!("{}\n", recipe.name).as_bytes())
|
.write_all(format!("{}\n", recipe.name).as_bytes())
|
||||||
{
|
{
|
||||||
if self.verbosity.loud() {
|
return Err(Error::ChooserWrite { io_error, chooser });
|
||||||
eprintln!(
|
|
||||||
"Failed to write to chooser `{}`: {}",
|
|
||||||
chooser.to_string_lossy(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = match child.wait_with_output() {
|
let output = match child.wait_with_output() {
|
||||||
Ok(output) => output,
|
Ok(output) => output,
|
||||||
Err(error) => {
|
Err(io_error) => {
|
||||||
if self.verbosity.loud() {
|
return Err(Error::ChooserRead { io_error, chooser });
|
||||||
eprintln!(
|
|
||||||
"Failed to read output from chooser `{}`: {}",
|
|
||||||
chooser.to_string_lossy(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
if self.verbosity.loud() {
|
return Err(Error::ChooserStatus {
|
||||||
eprintln!(
|
status: output.status,
|
||||||
"Chooser `{}` returned error: {}",
|
chooser,
|
||||||
chooser.to_string_lossy(),
|
});
|
||||||
output.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(output.status.code().unwrap_or(EXIT_FAILURE));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
@ -697,12 +662,11 @@ impl Config {
|
|||||||
self.run(justfile, search, overrides, &recipes)
|
self.run(justfile, search, overrides, &recipes)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dump(ast: Ast) -> Result<(), i32> {
|
fn dump(ast: Ast) {
|
||||||
print!("{}", ast);
|
print!("{}", ast);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> {
|
pub(crate) fn edit(search: &Search) -> Result<(), Error<'static>> {
|
||||||
let editor = env::var_os("VISUAL")
|
let editor = env::var_os("VISUAL")
|
||||||
.or_else(|| env::var_os("EDITOR"))
|
.or_else(|| env::var_os("EDITOR"))
|
||||||
.unwrap_or_else(|| "vim".into());
|
.unwrap_or_else(|| "vim".into());
|
||||||
@ -712,47 +676,38 @@ impl Config {
|
|||||||
.arg(&search.justfile)
|
.arg(&search.justfile)
|
||||||
.status();
|
.status();
|
||||||
|
|
||||||
match error {
|
let status = match error {
|
||||||
Ok(status) =>
|
Err(io_error) => return Err(Error::EditorInvoke { editor, io_error }),
|
||||||
if status.success() {
|
Ok(status) => status,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(Error::EditorStatus { editor, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_unstable(&self, message: &str) -> Result<(), Error<'static>> {
|
||||||
|
if self.unstable {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
if self.verbosity.loud() {
|
Err(Error::Unstable {
|
||||||
eprintln!("Editor `{}` failed: {}", editor.to_string_lossy(), status);
|
message: message.to_owned(),
|
||||||
}
|
})
|
||||||
Err(status.code().unwrap_or(EXIT_FAILURE))
|
|
||||||
},
|
|
||||||
Err(error) => {
|
|
||||||
if self.verbosity.loud() {
|
|
||||||
eprintln!(
|
|
||||||
"Editor `{}` invocation failed: {}",
|
|
||||||
editor.to_string_lossy(),
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format(&self, ast: Ast, search: &Search) -> Result<(), i32> {
|
fn format(&self, ast: Ast, search: &Search) -> Result<(), Error<'static>> {
|
||||||
if !self.unstable {
|
self.require_unstable("The `--fmt` command is currently unstable.")?;
|
||||||
eprintln!(
|
|
||||||
"The `--fmt` command is currently unstable. Pass the `--unstable` flag to enable it."
|
|
||||||
);
|
|
||||||
return Err(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(error) = File::create(&search.justfile).and_then(|mut file| write!(file, "{}", ast))
|
if let Err(io_error) =
|
||||||
|
File::create(&search.justfile).and_then(|mut file| write!(file, "{}", ast))
|
||||||
{
|
{
|
||||||
if self.verbosity.loud() {
|
Err(Error::WriteJustfile {
|
||||||
eprintln!(
|
justfile: search.justfile.clone(),
|
||||||
"Failed to write justfile to `{}`: {}",
|
io_error,
|
||||||
search.justfile.display(),
|
})
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
} else {
|
} else {
|
||||||
if self.verbosity.loud() {
|
if self.verbosity.loud() {
|
||||||
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
||||||
@ -761,24 +716,18 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn init(&self) -> Result<(), i32> {
|
pub(crate) fn init(&self) -> Result<(), Error<'static>> {
|
||||||
let search =
|
let search = Search::init(&self.search_config, &self.invocation_directory)?;
|
||||||
Search::init(&self.search_config, &self.invocation_directory).eprint(self.color)?;
|
|
||||||
|
|
||||||
if search.justfile.exists() {
|
if search.justfile.is_file() {
|
||||||
if self.verbosity.loud() {
|
Err(Error::InitExists {
|
||||||
eprintln!("Justfile `{}` already exists", search.justfile.display());
|
justfile: search.justfile,
|
||||||
}
|
})
|
||||||
Err(EXIT_FAILURE)
|
} else if let Err(io_error) = fs::write(&search.justfile, INIT_JUSTFILE) {
|
||||||
} else if let Err(err) = fs::write(&search.justfile, INIT_JUSTFILE) {
|
Err(Error::WriteJustfile {
|
||||||
if self.verbosity.loud() {
|
justfile: search.justfile,
|
||||||
eprintln!(
|
io_error,
|
||||||
"Failed to write justfile to `{}`: {}",
|
})
|
||||||
search.justfile.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
} else {
|
} else {
|
||||||
if self.verbosity.loud() {
|
if self.verbosity.loud() {
|
||||||
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
eprintln!("Wrote justfile to `{}`", search.justfile.display());
|
||||||
@ -871,27 +820,21 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(
|
fn run<'src>(
|
||||||
&self,
|
&self,
|
||||||
justfile: Justfile,
|
justfile: Justfile<'src>,
|
||||||
search: &Search,
|
search: &Search,
|
||||||
overrides: &BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
) -> Result<(), i32> {
|
) -> Result<(), Error<'src>> {
|
||||||
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
if let Err(error) = InterruptHandler::install(self.verbosity) {
|
||||||
warn!("Failed to set CTRL-C handler: {}", error);
|
warn!("Failed to set CTRL-C handler: {}", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = justfile.run(&self, search, overrides, arguments);
|
justfile.run(&self, search, overrides, arguments)
|
||||||
|
|
||||||
if !self.verbosity.quiet() {
|
|
||||||
result.eprint(self.color)
|
|
||||||
} else {
|
|
||||||
result.map_err(|err| err.code())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&self, name: &str, justfile: Justfile) -> Result<(), i32> {
|
fn show<'src>(name: &str, justfile: Justfile<'src>) -> Result<(), Error<'src>> {
|
||||||
if let Some(alias) = justfile.get_alias(name) {
|
if let Some(alias) = justfile.get_alias(name) {
|
||||||
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap();
|
||||||
println!("{}", alias);
|
println!("{}", alias);
|
||||||
@ -901,13 +844,10 @@ impl Config {
|
|||||||
println!("{}", recipe);
|
println!("{}", recipe);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
if self.verbosity.loud() {
|
Err(Error::UnknownRecipes {
|
||||||
eprintln!("Justfile does not contain recipe `{}`.", name);
|
recipes: vec![name.to_owned()],
|
||||||
if let Some(suggestion) = justfile.suggest_recipe(name) {
|
suggestion: justfile.suggest_recipe(name),
|
||||||
eprintln!("{}", suggestion);
|
})
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@ use crate::common::*;
|
|||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub(crate)))]
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub(crate) enum ConfigError {
|
pub(crate) enum ConfigError {
|
||||||
|
#[snafu(display("Failed to get current directory: {}", source))]
|
||||||
|
CurrentDir { source: io::Error },
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"Internal config error, this may indicate a bug in just: {} \
|
"Internal config error, this may indicate a bug in just: {} \
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
message
|
message
|
||||||
))]
|
))]
|
||||||
Internal { message: String },
|
Internal { message: String },
|
||||||
#[snafu(display("Failed to get current directory: {}", source))]
|
|
||||||
CurrentDir { source: io::Error },
|
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
|
"Path-prefixed recipes may not be used with `--working-directory` or `--justfile`."
|
||||||
))]
|
))]
|
||||||
@ -25,6 +25,15 @@ pub(crate) enum ConfigError {
|
|||||||
subcommand: &'static str,
|
subcommand: &'static str,
|
||||||
arguments: Vec<String>,
|
arguments: Vec<String>,
|
||||||
},
|
},
|
||||||
|
#[snafu(display(
|
||||||
|
"`--{}` used with unexpected overrides: {}",
|
||||||
|
subcommand.to_lowercase(),
|
||||||
|
List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))),
|
||||||
|
))]
|
||||||
|
SubcommandOverrides {
|
||||||
|
subcommand: &'static str,
|
||||||
|
overrides: BTreeMap<String, String>,
|
||||||
|
},
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"`--{}` used with unexpected overrides: {}; and arguments: {}",
|
"`--{}` used with unexpected overrides: {}; and arguments: {}",
|
||||||
subcommand.to_lowercase(),
|
subcommand.to_lowercase(),
|
||||||
@ -36,15 +45,6 @@ pub(crate) enum ConfigError {
|
|||||||
overrides: BTreeMap<String, String>,
|
overrides: BTreeMap<String, String>,
|
||||||
arguments: Vec<String>,
|
arguments: Vec<String>,
|
||||||
},
|
},
|
||||||
#[snafu(display(
|
|
||||||
"`--{}` used with unexpected overrides: {}",
|
|
||||||
subcommand.to_lowercase(),
|
|
||||||
List::and_ticked(overrides.iter().map(|(key, value)| format!("{}={}", key, value))),
|
|
||||||
))]
|
|
||||||
SubcommandOverrides {
|
|
||||||
subcommand: &'static str,
|
|
||||||
overrides: BTreeMap<String, String>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigError {
|
impl ConfigError {
|
||||||
@ -54,5 +54,3 @@ impl ConfigError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for ConfigError {}
|
|
||||||
|
623
src/error.rs
623
src/error.rs
@ -1,7 +1,624 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
pub(crate) trait Error: Display {
|
#[derive(Debug)]
|
||||||
fn code(&self) -> i32 {
|
pub(crate) enum Error<'src> {
|
||||||
EXIT_FAILURE
|
ArgumentCountMismatch {
|
||||||
|
recipe: &'src str,
|
||||||
|
parameters: Vec<Parameter<'src>>,
|
||||||
|
found: usize,
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
},
|
||||||
|
Backtick {
|
||||||
|
token: Token<'src>,
|
||||||
|
output_error: OutputError,
|
||||||
|
},
|
||||||
|
ChooserInvoke {
|
||||||
|
shell_binary: String,
|
||||||
|
shell_arguments: String,
|
||||||
|
chooser: OsString,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
ChooserRead {
|
||||||
|
chooser: OsString,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
ChooserStatus {
|
||||||
|
chooser: OsString,
|
||||||
|
status: ExitStatus,
|
||||||
|
},
|
||||||
|
ChooserWrite {
|
||||||
|
chooser: OsString,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Code {
|
||||||
|
recipe: &'src str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
code: i32,
|
||||||
|
},
|
||||||
|
CommandInvoke {
|
||||||
|
binary: OsString,
|
||||||
|
arguments: Vec<OsString>,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
CommandStatus {
|
||||||
|
binary: OsString,
|
||||||
|
arguments: Vec<OsString>,
|
||||||
|
status: ExitStatus,
|
||||||
|
},
|
||||||
|
Compile {
|
||||||
|
compile_error: CompileError<'src>,
|
||||||
|
},
|
||||||
|
Config {
|
||||||
|
config_error: ConfigError,
|
||||||
|
},
|
||||||
|
Cygpath {
|
||||||
|
recipe: &'src str,
|
||||||
|
output_error: OutputError,
|
||||||
|
},
|
||||||
|
DefaultRecipeRequiresArguments {
|
||||||
|
recipe: &'src str,
|
||||||
|
min_arguments: usize,
|
||||||
|
},
|
||||||
|
Dotenv {
|
||||||
|
dotenv_error: dotenv::Error,
|
||||||
|
},
|
||||||
|
EditorInvoke {
|
||||||
|
editor: OsString,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
EditorStatus {
|
||||||
|
editor: OsString,
|
||||||
|
status: ExitStatus,
|
||||||
|
},
|
||||||
|
EvalUnknownVariable {
|
||||||
|
variable: String,
|
||||||
|
suggestion: Option<Suggestion<'src>>,
|
||||||
|
},
|
||||||
|
FunctionCall {
|
||||||
|
function: Name<'src>,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
InitExists {
|
||||||
|
justfile: PathBuf,
|
||||||
|
},
|
||||||
|
Internal {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Io {
|
||||||
|
recipe: &'src str,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Load {
|
||||||
|
path: PathBuf,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
NoChoosableRecipes,
|
||||||
|
NoRecipes,
|
||||||
|
Search {
|
||||||
|
search_error: SearchError,
|
||||||
|
},
|
||||||
|
Shebang {
|
||||||
|
recipe: &'src str,
|
||||||
|
command: String,
|
||||||
|
argument: Option<String>,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Signal {
|
||||||
|
recipe: &'src str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
signal: i32,
|
||||||
|
},
|
||||||
|
TmpdirIo {
|
||||||
|
recipe: &'src str,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
Unknown {
|
||||||
|
recipe: &'src str,
|
||||||
|
line_number: Option<usize>,
|
||||||
|
},
|
||||||
|
UnknownOverrides {
|
||||||
|
overrides: Vec<String>,
|
||||||
|
},
|
||||||
|
UnknownRecipes {
|
||||||
|
recipes: Vec<String>,
|
||||||
|
suggestion: Option<Suggestion<'src>>,
|
||||||
|
},
|
||||||
|
Unstable {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
WriteJustfile {
|
||||||
|
justfile: PathBuf,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Error<'src> {
|
||||||
|
pub(crate) fn code(&self) -> Option<i32> {
|
||||||
|
match self {
|
||||||
|
Self::Code { code, .. }
|
||||||
|
| Self::Backtick {
|
||||||
|
output_error: OutputError::Code(code),
|
||||||
|
..
|
||||||
|
} => Some(*code),
|
||||||
|
Self::ChooserStatus { status, .. } | Self::EditorStatus { status, .. } => status.code(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context(&self) -> Option<Token<'src>> {
|
||||||
|
match self {
|
||||||
|
Self::Backtick { token, .. } => Some(*token),
|
||||||
|
Self::Compile { compile_error } => Some(compile_error.context()),
|
||||||
|
Self::FunctionCall { function, .. } => Some(function.token()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn internal(message: impl Into<String>) -> Self {
|
||||||
|
Self::Internal {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write(&self, w: &mut dyn Write, color: Color) -> io::Result<()> {
|
||||||
|
let color = color.stderr();
|
||||||
|
|
||||||
|
if color.active() {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"{}: {}{:#}{}",
|
||||||
|
color.error().paint("error"),
|
||||||
|
color.message().prefix(),
|
||||||
|
self,
|
||||||
|
color.message().suffix()
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(w, "error: {}", self)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(token) = self.context() {
|
||||||
|
token.write_context(w, color.error())?;
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> From<CompileError<'src>> for Error<'src> {
|
||||||
|
fn from(compile_error: CompileError<'src>) -> Self {
|
||||||
|
Self::Compile { compile_error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> From<ConfigError> for Error<'src> {
|
||||||
|
fn from(config_error: ConfigError) -> Self {
|
||||||
|
Self::Config { config_error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> From<dotenv::Error> for Error<'src> {
|
||||||
|
fn from(dotenv_error: dotenv::Error) -> Error<'src> {
|
||||||
|
Self::Dotenv { dotenv_error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> From<SearchError> for Error<'src> {
|
||||||
|
fn from(search_error: SearchError) -> Self {
|
||||||
|
Self::Search { search_error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'src> Display for Error<'src> {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
use Error::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
ArgumentCountMismatch {
|
||||||
|
recipe,
|
||||||
|
parameters,
|
||||||
|
found,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
} => {
|
||||||
|
if min == max {
|
||||||
|
let expected = min;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` got {} {} but {}takes {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
Count("argument", *found),
|
||||||
|
if expected < found { "only " } else { "" },
|
||||||
|
expected
|
||||||
|
)?;
|
||||||
|
} else if found < min {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` got {} {} but takes at least {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
Count("argument", *found),
|
||||||
|
min
|
||||||
|
)?;
|
||||||
|
} else if found > max {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` got {} {} but takes at most {}",
|
||||||
|
recipe,
|
||||||
|
found,
|
||||||
|
Count("argument", *found),
|
||||||
|
max
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
write!(f, "\nusage:\n just {}", recipe)?;
|
||||||
|
for param in parameters {
|
||||||
|
write!(f, " {}", param)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Backtick { output_error, .. } => match output_error {
|
||||||
|
OutputError::Code(code) => {
|
||||||
|
write!(f, "Backtick failed with exit code {}", code)?;
|
||||||
|
},
|
||||||
|
OutputError::Signal(signal) => {
|
||||||
|
write!(f, "Backtick was terminated by signal {}", signal)?;
|
||||||
|
},
|
||||||
|
OutputError::Unknown => {
|
||||||
|
write!(f, "Backtick failed for an unknown reason")?;
|
||||||
|
},
|
||||||
|
OutputError::Io(io_error) => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(
|
||||||
|
f,
|
||||||
|
"Backtick could not be run because just could not find `sh`:\n{}",
|
||||||
|
io_error
|
||||||
|
),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f,
|
||||||
|
"Backtick could not be run because just could not run `sh`:\n{}",
|
||||||
|
io_error
|
||||||
|
),
|
||||||
|
_ => write!(
|
||||||
|
f,
|
||||||
|
"Backtick could not be run because of an IO error while launching `sh`:\n{}",
|
||||||
|
io_error
|
||||||
|
),
|
||||||
|
}?;
|
||||||
|
},
|
||||||
|
OutputError::Utf8(utf8_error) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Backtick succeeded but stdout was not utf8: {}",
|
||||||
|
utf8_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ChooserInvoke {
|
||||||
|
shell_binary,
|
||||||
|
shell_arguments,
|
||||||
|
chooser,
|
||||||
|
io_error,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Chooser `{} {} {}` invocation failed: {}",
|
||||||
|
shell_binary,
|
||||||
|
shell_arguments,
|
||||||
|
chooser.to_string_lossy(),
|
||||||
|
io_error,
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
ChooserRead { chooser, io_error } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to read output from chooser `{}`: {}",
|
||||||
|
chooser.to_string_lossy(),
|
||||||
|
io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
ChooserStatus { chooser, status } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Chooser `{}` failed: {}",
|
||||||
|
chooser.to_string_lossy(),
|
||||||
|
status
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
ChooserWrite { chooser, io_error } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to write to chooser `{}`: {}",
|
||||||
|
chooser.to_string_lossy(),
|
||||||
|
io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
Code {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
code,
|
||||||
|
} =>
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` failed on line {} with exit code {}",
|
||||||
|
recipe, n, code
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
||||||
|
},
|
||||||
|
CommandInvoke {
|
||||||
|
binary,
|
||||||
|
arguments,
|
||||||
|
io_error,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to invoke {}: {}",
|
||||||
|
iter::once(binary)
|
||||||
|
.chain(arguments)
|
||||||
|
.map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" "),
|
||||||
|
io_error,
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
CommandStatus {
|
||||||
|
binary,
|
||||||
|
arguments,
|
||||||
|
status,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Command {} failed: {}",
|
||||||
|
iter::once(binary)
|
||||||
|
.chain(arguments)
|
||||||
|
.map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(" "),
|
||||||
|
status,
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
Compile { compile_error } => Display::fmt(compile_error, f)?,
|
||||||
|
Config { config_error } => Display::fmt(config_error, f)?,
|
||||||
|
Cygpath {
|
||||||
|
recipe,
|
||||||
|
output_error,
|
||||||
|
} => match output_error {
|
||||||
|
OutputError::Code(code) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Cygpath failed with exit code {} while translating recipe `{}` shebang interpreter \
|
||||||
|
path",
|
||||||
|
code, recipe
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
OutputError::Signal(signal) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Cygpath terminated by signal {} while translating recipe `{}` shebang interpreter \
|
||||||
|
path",
|
||||||
|
signal, recipe
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
OutputError::Unknown => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Cygpath experienced an unknown failure while translating recipe `{}` shebang \
|
||||||
|
interpreter path",
|
||||||
|
recipe
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
OutputError::Io(io_error) => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(
|
||||||
|
f,
|
||||||
|
"Could not find `cygpath` executable to translate recipe `{}` shebang interpreter \
|
||||||
|
path:\n{}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f,
|
||||||
|
"Could not run `cygpath` executable to translate recipe `{}` shebang interpreter \
|
||||||
|
path:\n{}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
|
_ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
|
||||||
|
}?;
|
||||||
|
},
|
||||||
|
OutputError::Utf8(utf8_error) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Cygpath successfully translated recipe `{}` shebang interpreter path, but output was \
|
||||||
|
not utf8: {}",
|
||||||
|
recipe, utf8_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DefaultRecipeRequiresArguments {
|
||||||
|
recipe,
|
||||||
|
min_arguments,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` cannot be used as default recipe since it requires at least {} {}.",
|
||||||
|
recipe,
|
||||||
|
min_arguments,
|
||||||
|
Count("argument", *min_arguments),
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
Dotenv { dotenv_error } => {
|
||||||
|
write!(f, "Failed to load .env: {}", dotenv_error)?;
|
||||||
|
},
|
||||||
|
EditorInvoke { editor, io_error } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Editor `{}` invocation failed: {}",
|
||||||
|
editor.to_string_lossy(),
|
||||||
|
io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
EditorStatus { editor, status } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Editor `{}` failed: {}",
|
||||||
|
editor.to_string_lossy(),
|
||||||
|
status
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
EvalUnknownVariable {
|
||||||
|
variable,
|
||||||
|
suggestion,
|
||||||
|
} => {
|
||||||
|
write!(f, "Justfile does not contain variable `{}`.", variable,)?;
|
||||||
|
if let Some(suggestion) = *suggestion {
|
||||||
|
write!(f, "\n{}", suggestion)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FunctionCall { function, message } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Call to function `{}` failed: {}",
|
||||||
|
function.lexeme(),
|
||||||
|
message
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
InitExists { justfile } => {
|
||||||
|
write!(f, "Justfile `{}` already exists", justfile.display())?;
|
||||||
|
},
|
||||||
|
Internal { message } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Internal runtime error, this may indicate a bug in just: {} \
|
||||||
|
consider filing an issue: https://github.com/casey/just/issues/new",
|
||||||
|
message
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
Io { recipe, io_error } => {
|
||||||
|
match io_error.kind() {
|
||||||
|
io::ErrorKind::NotFound => write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because just could not find `sh`: {}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
|
io::ErrorKind::PermissionDenied => write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because just could not run `sh`: {}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
|
_ => write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because of an IO error while launching `sh`: {}",
|
||||||
|
recipe, io_error
|
||||||
|
),
|
||||||
|
}?;
|
||||||
|
},
|
||||||
|
Load { io_error, path } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to read justfile at `{}`: {}",
|
||||||
|
path.display(),
|
||||||
|
io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
NoChoosableRecipes => {
|
||||||
|
write!(f, "Justfile contains no choosable recipes.")?;
|
||||||
|
},
|
||||||
|
NoRecipes => {
|
||||||
|
write!(f, "Justfile contains no recipes.")?;
|
||||||
|
},
|
||||||
|
Search { search_error } => Display::fmt(search_error, f)?,
|
||||||
|
Shebang {
|
||||||
|
recipe,
|
||||||
|
command,
|
||||||
|
argument,
|
||||||
|
io_error,
|
||||||
|
} =>
|
||||||
|
if let Some(argument) = argument {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` with shebang `#!{} {}` execution error: {}",
|
||||||
|
recipe, command, argument, io_error
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` with shebang `#!{}` execution error: {}",
|
||||||
|
recipe, command, io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
Signal {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
signal,
|
||||||
|
} =>
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` was terminated on line {} by signal {}",
|
||||||
|
recipe, n, signal
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
|
||||||
|
},
|
||||||
|
TmpdirIo { recipe, io_error } => write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` could not be run because of an IO error while trying to create a temporary \
|
||||||
|
directory or write a file to that directory`:{}",
|
||||||
|
recipe, io_error
|
||||||
|
)?,
|
||||||
|
Unknown {
|
||||||
|
recipe,
|
||||||
|
line_number,
|
||||||
|
} =>
|
||||||
|
if let Some(n) = line_number {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Recipe `{}` failed on line {} for an unknown reason",
|
||||||
|
recipe, n
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "Recipe `{}` failed for an unknown reason", recipe)?;
|
||||||
|
},
|
||||||
|
UnknownOverrides { overrides } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} {} overridden on the command line but not present in justfile",
|
||||||
|
Count("Variable", overrides.len()),
|
||||||
|
List::and_ticked(overrides),
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
UnknownRecipes {
|
||||||
|
recipes,
|
||||||
|
suggestion,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Justfile does not contain {} {}.",
|
||||||
|
Count("recipe", recipes.len()),
|
||||||
|
List::or_ticked(recipes),
|
||||||
|
)?;
|
||||||
|
if let Some(suggestion) = *suggestion {
|
||||||
|
write!(f, "\n{}", suggestion)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Unstable { message } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} Invoke `just` with the `--unstable` flag to enable unstable features.",
|
||||||
|
message
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
WriteJustfile { justfile, io_error } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"Failed to write justfile to `{}`: {}",
|
||||||
|
justfile.display(),
|
||||||
|
io_error
|
||||||
|
)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
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.stderr().error().paint("error"), error);
|
|
||||||
} else {
|
|
||||||
eprintln!("error: {}", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(error.code())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -60,7 +60,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
{
|
{
|
||||||
Ok(self.evaluate_assignment(assignment)?.to_owned())
|
Ok(self.evaluate_assignment(assignment)?.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(RuntimeError::Internal {
|
Err(Error::Internal {
|
||||||
message: format!("attempted to evaluate undefined variable `{}`", variable),
|
message: format!("attempted to evaluate undefined variable `{}`", variable),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
|
|
||||||
match thunk {
|
match thunk {
|
||||||
Nullary { name, function, .. } =>
|
Nullary { name, function, .. } =>
|
||||||
function(&context).map_err(|message| RuntimeError::FunctionCall {
|
function(&context).map_err(|message| Error::FunctionCall {
|
||||||
function: *name,
|
function: *name,
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
@ -86,7 +86,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
arg,
|
arg,
|
||||||
..
|
..
|
||||||
} => function(&context, &self.evaluate_expression(arg)?).map_err(|message| {
|
} => function(&context, &self.evaluate_expression(arg)?).map_err(|message| {
|
||||||
RuntimeError::FunctionCall {
|
Error::FunctionCall {
|
||||||
function: *name,
|
function: *name,
|
||||||
message,
|
message,
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
&self.evaluate_expression(a)?,
|
&self.evaluate_expression(a)?,
|
||||||
&self.evaluate_expression(b)?,
|
&self.evaluate_expression(b)?,
|
||||||
)
|
)
|
||||||
.map_err(|message| RuntimeError::FunctionCall {
|
.map_err(|message| Error::FunctionCall {
|
||||||
function: *name,
|
function: *name,
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
@ -116,7 +116,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
&self.evaluate_expression(b)?,
|
&self.evaluate_expression(b)?,
|
||||||
&self.evaluate_expression(c)?,
|
&self.evaluate_expression(c)?,
|
||||||
)
|
)
|
||||||
.map_err(|message| RuntimeError::FunctionCall {
|
.map_err(|message| Error::FunctionCall {
|
||||||
function: *name,
|
function: *name,
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
@ -169,7 +169,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
InterruptHandler::guard(|| {
|
InterruptHandler::guard(|| {
|
||||||
output(cmd).map_err(|output_error| RuntimeError::Backtick {
|
output(cmd).map_err(|output_error| Error::Backtick {
|
||||||
token: *token,
|
token: *token,
|
||||||
output_error,
|
output_error,
|
||||||
})
|
})
|
||||||
@ -233,7 +233,7 @@ impl<'src, 'run> Evaluator<'src, 'run> {
|
|||||||
} else if parameter.kind == ParameterKind::Star {
|
} else if parameter.kind == ParameterKind::Star {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
return Err(RuntimeError::Internal {
|
return Err(Error::Internal {
|
||||||
message: "missing parameter without default".to_owned(),
|
message: "missing parameter without default".to_owned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -285,7 +285,7 @@ mod tests {
|
|||||||
echo {{`f() { return 100; }; f`}}
|
echo {{`f() { return 100; }; f`}}
|
||||||
",
|
",
|
||||||
args: ["a"],
|
args: ["a"],
|
||||||
error: RuntimeError::Backtick {
|
error: Error::Backtick {
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(code),
|
output_error: OutputError::Code(code),
|
||||||
},
|
},
|
||||||
@ -305,7 +305,7 @@ mod tests {
|
|||||||
echo {{b}}
|
echo {{b}}
|
||||||
"#,
|
"#,
|
||||||
args: ["--quiet", "recipe"],
|
args: ["--quiet", "recipe"],
|
||||||
error: RuntimeError::Backtick {
|
error: Error::Backtick {
|
||||||
token,
|
token,
|
||||||
output_error: OutputError::Code(_),
|
output_error: OutputError::Code(_),
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ use crate::common::*;
|
|||||||
|
|
||||||
pub(crate) fn compile(text: &str) {
|
pub(crate) fn compile(text: &str) {
|
||||||
if let Err(error) = Parser::parse(text) {
|
if let Err(error) = Parser::parse(text) {
|
||||||
if let CompilationErrorKind::Internal { .. } = error.kind {
|
if let CompileErrorKind::Internal { .. } = error.kind {
|
||||||
panic!("{}", error)
|
panic!("{}", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ impl InterruptHandler {
|
|||||||
match INSTANCE.lock() {
|
match INSTANCE.lock() {
|
||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
Err(poison_error) => {
|
Err(poison_error) => {
|
||||||
eprintln!("{}", RuntimeError::Internal {
|
eprintln!("{}", Error::Internal {
|
||||||
message: format!("interrupt handler mutex poisoned: {}", poison_error),
|
message: format!("interrupt handler mutex poisoned: {}", poison_error),
|
||||||
});
|
});
|
||||||
std::process::exit(EXIT_FAILURE);
|
std::process::exit(EXIT_FAILURE);
|
||||||
@ -58,7 +58,7 @@ impl InterruptHandler {
|
|||||||
pub(crate) fn unblock(&mut self) {
|
pub(crate) fn unblock(&mut self) {
|
||||||
if self.blocks == 0 {
|
if self.blocks == 0 {
|
||||||
if self.verbosity.loud() {
|
if self.verbosity.loud() {
|
||||||
eprintln!("{}", RuntimeError::Internal {
|
eprintln!("{}", Error::Internal {
|
||||||
message: "attempted to unblock interrupt handler, but handler was not blocked".to_owned(),
|
message: "attempted to unblock interrupt handler, but handler was not blocked".to_owned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ pub(crate) struct Justfile<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'src> Justfile<'src> {
|
impl<'src> Justfile<'src> {
|
||||||
pub(crate) fn first(&self) -> Option<&Recipe> {
|
pub(crate) fn first(&self) -> Option<&Recipe<'src>> {
|
||||||
let mut first: Option<&Recipe<Dependency>> = None;
|
let mut first: Option<&Recipe<Dependency>> = None;
|
||||||
for recipe in self.recipes.values() {
|
for recipe in self.recipes.values() {
|
||||||
if let Some(first_recipe) = first {
|
if let Some(first_recipe) = first {
|
||||||
@ -28,7 +28,7 @@ impl<'src> Justfile<'src> {
|
|||||||
self.recipes.len()
|
self.recipes.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion> {
|
pub(crate) fn suggest_recipe(&self, input: &str) -> Option<Suggestion<'src>> {
|
||||||
let mut suggestions = self
|
let mut suggestions = self
|
||||||
.recipes
|
.recipes
|
||||||
.keys()
|
.keys()
|
||||||
@ -54,7 +54,7 @@ impl<'src> Justfile<'src> {
|
|||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion> {
|
pub(crate) fn suggest_variable(&self, input: &str) -> Option<Suggestion<'src>> {
|
||||||
let mut suggestions = self
|
let mut suggestions = self
|
||||||
.assignments
|
.assignments
|
||||||
.keys()
|
.keys()
|
||||||
@ -74,21 +74,21 @@ impl<'src> Justfile<'src> {
|
|||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run<'run>(
|
pub(crate) fn run(
|
||||||
&'run self,
|
&self,
|
||||||
config: &'run Config,
|
config: &Config,
|
||||||
search: &'run Search,
|
search: &Search,
|
||||||
overrides: &'run BTreeMap<String, String>,
|
overrides: &BTreeMap<String, String>,
|
||||||
arguments: &'run [String],
|
arguments: &[String],
|
||||||
) -> RunResult<'run, ()> {
|
) -> RunResult<'src, ()> {
|
||||||
let unknown_overrides = overrides
|
let unknown_overrides = overrides
|
||||||
.keys()
|
.keys()
|
||||||
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
.filter(|name| !self.assignments.contains_key(name.as_str()))
|
||||||
.map(String::as_str)
|
.cloned()
|
||||||
.collect::<Vec<&str>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
if !unknown_overrides.is_empty() {
|
if !unknown_overrides.is_empty() {
|
||||||
return Err(RuntimeError::UnknownOverrides {
|
return Err(Error::UnknownOverrides {
|
||||||
overrides: unknown_overrides,
|
overrides: unknown_overrides,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -107,12 +107,12 @@ impl<'src> Justfile<'src> {
|
|||||||
if let Some(assignment) = self.assignments.get(name) {
|
if let Some(assignment) = self.assignments.get(name) {
|
||||||
scope.bind(assignment.export, assignment.name, value.clone());
|
scope.bind(assignment.export, assignment.name, value.clone());
|
||||||
} else {
|
} else {
|
||||||
unknown_overrides.push(name.as_ref());
|
unknown_overrides.push(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !unknown_overrides.is_empty() {
|
if !unknown_overrides.is_empty() {
|
||||||
return Err(RuntimeError::UnknownOverrides {
|
return Err(Error::UnknownOverrides {
|
||||||
overrides: unknown_overrides,
|
overrides: unknown_overrides,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -148,7 +148,7 @@ impl<'src> Justfile<'src> {
|
|||||||
command.export(&self.settings, &dotenv, &scope);
|
command.export(&self.settings, &dotenv, &scope);
|
||||||
|
|
||||||
let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
|
let status = InterruptHandler::guard(|| command.status()).map_err(|io_error| {
|
||||||
RuntimeError::CommandInvocation {
|
Error::CommandInvoke {
|
||||||
binary: binary.clone(),
|
binary: binary.clone(),
|
||||||
arguments: arguments.clone(),
|
arguments: arguments.clone(),
|
||||||
io_error,
|
io_error,
|
||||||
@ -156,7 +156,11 @@ impl<'src> Justfile<'src> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
process::exit(status.code().unwrap_or(EXIT_FAILURE));
|
return Err(Error::CommandStatus {
|
||||||
|
binary: binary.clone(),
|
||||||
|
arguments: arguments.clone(),
|
||||||
|
status,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -166,7 +170,7 @@ impl<'src> Justfile<'src> {
|
|||||||
if let Some(value) = scope.value(variable) {
|
if let Some(value) = scope.value(variable) {
|
||||||
print!("{}", value);
|
print!("{}", value);
|
||||||
} else {
|
} else {
|
||||||
return Err(RuntimeError::EvalUnknownVariable {
|
return Err(Error::EvalUnknownVariable {
|
||||||
suggestion: self.suggest_variable(&variable),
|
suggestion: self.suggest_variable(&variable),
|
||||||
variable: variable.clone(),
|
variable: variable.clone(),
|
||||||
});
|
});
|
||||||
@ -198,14 +202,14 @@ impl<'src> Justfile<'src> {
|
|||||||
} else if let Some(recipe) = self.first() {
|
} else if let Some(recipe) = self.first() {
|
||||||
let min_arguments = recipe.min_arguments();
|
let min_arguments = recipe.min_arguments();
|
||||||
if min_arguments > 0 {
|
if min_arguments > 0 {
|
||||||
return Err(RuntimeError::DefaultRecipeRequiresArguments {
|
return Err(Error::DefaultRecipeRequiresArguments {
|
||||||
recipe: recipe.name.lexeme(),
|
recipe: recipe.name.lexeme(),
|
||||||
min_arguments,
|
min_arguments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
vec![recipe.name()]
|
vec![recipe.name()]
|
||||||
} else {
|
} else {
|
||||||
return Err(RuntimeError::NoRecipes);
|
return Err(Error::NoRecipes);
|
||||||
};
|
};
|
||||||
|
|
||||||
let arguments = argvec.as_slice();
|
let arguments = argvec.as_slice();
|
||||||
@ -222,9 +226,9 @@ impl<'src> Justfile<'src> {
|
|||||||
let argument_range = recipe.argument_range();
|
let argument_range = recipe.argument_range();
|
||||||
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
|
||||||
if !argument_range.range_contains(&argument_count) {
|
if !argument_range.range_contains(&argument_count) {
|
||||||
return Err(RuntimeError::ArgumentCountMismatch {
|
return Err(Error::ArgumentCountMismatch {
|
||||||
recipe: recipe.name(),
|
recipe: recipe.name(),
|
||||||
parameters: recipe.parameters.iter().collect(),
|
parameters: recipe.parameters.clone(),
|
||||||
found: tail.len(),
|
found: tail.len(),
|
||||||
min: recipe.min_arguments(),
|
min: recipe.min_arguments(),
|
||||||
max: recipe.max_arguments(),
|
max: recipe.max_arguments(),
|
||||||
@ -234,7 +238,7 @@ impl<'src> Justfile<'src> {
|
|||||||
tail = &tail[argument_count..];
|
tail = &tail[argument_count..];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
missing.push(*argument);
|
missing.push((*argument).to_owned());
|
||||||
}
|
}
|
||||||
rest = tail;
|
rest = tail;
|
||||||
}
|
}
|
||||||
@ -245,7 +249,7 @@ impl<'src> Justfile<'src> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
return Err(RuntimeError::UnknownRecipes {
|
return Err(Error::UnknownRecipes {
|
||||||
recipes: missing,
|
recipes: missing,
|
||||||
suggestion,
|
suggestion,
|
||||||
});
|
});
|
||||||
@ -266,7 +270,7 @@ impl<'src> Justfile<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias> {
|
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias<'src>> {
|
||||||
self.aliases.get(name)
|
self.aliases.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,13 +282,13 @@ impl<'src> Justfile<'src> {
|
|||||||
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
|
.or_else(|| self.aliases.get(name).map(|alias| alias.target.as_ref()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_recipe<'run>(
|
fn run_recipe(
|
||||||
&self,
|
&self,
|
||||||
context: &'run RecipeContext<'src, 'run>,
|
context: &RecipeContext<'src, '_>,
|
||||||
recipe: &Recipe<'src>,
|
recipe: &Recipe<'src>,
|
||||||
arguments: &[&'run str],
|
arguments: &[&str],
|
||||||
dotenv: &BTreeMap<String, String>,
|
dotenv: &BTreeMap<String, String>,
|
||||||
search: &'run Search,
|
search: &Search,
|
||||||
ran: &mut BTreeSet<Vec<String>>,
|
ran: &mut BTreeSet<Vec<String>>,
|
||||||
) -> RunResult<'src, ()> {
|
) -> RunResult<'src, ()> {
|
||||||
let (outer, positional) = Evaluator::evaluate_parameters(
|
let (outer, positional) = Evaluator::evaluate_parameters(
|
||||||
@ -351,7 +355,7 @@ impl<'src> Justfile<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<Dependency>> {
|
pub(crate) fn public_recipes(&self, source_order: bool) -> Vec<&Recipe<'src, Dependency>> {
|
||||||
let mut recipes = self
|
let mut recipes = self
|
||||||
.recipes
|
.recipes
|
||||||
.values()
|
.values()
|
||||||
@ -403,7 +407,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use testing::compile;
|
use testing::compile;
|
||||||
use RuntimeError::*;
|
use Error::*;
|
||||||
|
|
||||||
run_error! {
|
run_error! {
|
||||||
name: unknown_recipes,
|
name: unknown_recipes,
|
||||||
|
98
src/lexer.rs
98
src/lexer.rs
@ -1,6 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
use TokenKind::*;
|
use TokenKind::*;
|
||||||
|
|
||||||
/// Just language lexer
|
/// Just language lexer
|
||||||
@ -38,7 +38,7 @@ pub(crate) struct Lexer<'src> {
|
|||||||
|
|
||||||
impl<'src> Lexer<'src> {
|
impl<'src> Lexer<'src> {
|
||||||
/// Lex `text`
|
/// Lex `text`
|
||||||
pub(crate) fn lex(src: &str) -> CompilationResult<Vec<Token>> {
|
pub(crate) fn lex(src: &'src str) -> CompileResult<Vec<Token<'src>>> {
|
||||||
Lexer::new(src).tokenize()
|
Lexer::new(src).tokenize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ impl<'src> Lexer<'src> {
|
|||||||
|
|
||||||
/// Advance over the character in `self.next`, updating `self.token_end`
|
/// Advance over the character in `self.next`, updating `self.token_end`
|
||||||
/// accordingly.
|
/// accordingly.
|
||||||
fn advance(&mut self) -> CompilationResult<'src, ()> {
|
fn advance(&mut self) -> CompileResult<'src, ()> {
|
||||||
match self.next {
|
match self.next {
|
||||||
Some(c) => {
|
Some(c) => {
|
||||||
let len_utf8 = c.len_utf8();
|
let len_utf8 = c.len_utf8();
|
||||||
@ -92,7 +92,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advance over N characters.
|
/// Advance over N characters.
|
||||||
fn skip(&mut self, n: usize) -> CompilationResult<'src, ()> {
|
fn skip(&mut self, n: usize) -> CompileResult<'src, ()> {
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ impl<'src> Lexer<'src> {
|
|||||||
self.token_end.offset - self.token_start.offset
|
self.token_end.offset - self.token_start.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accepted(&mut self, c: char) -> CompilationResult<'src, bool> {
|
fn accepted(&mut self, c: char) -> CompileResult<'src, bool> {
|
||||||
if self.next_is(c) {
|
if self.next_is(c) {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@ -119,7 +119,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn presume(&mut self, c: char) -> CompilationResult<'src, ()> {
|
fn presume(&mut self, c: char) -> CompileResult<'src, ()> {
|
||||||
if !self.next_is(c) {
|
if !self.next_is(c) {
|
||||||
return Err(self.internal_error(format!("Lexer presumed character `{}`", c)));
|
return Err(self.internal_error(format!("Lexer presumed character `{}`", c)));
|
||||||
}
|
}
|
||||||
@ -129,7 +129,7 @@ impl<'src> Lexer<'src> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn presume_str(&mut self, s: &str) -> CompilationResult<'src, ()> {
|
fn presume_str(&mut self, s: &str) -> CompileResult<'src, ()> {
|
||||||
for c in s.chars() {
|
for c in s.chars() {
|
||||||
self.presume(c)?;
|
self.presume(c)?;
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create an internal error with `message`
|
/// Create an internal error with `message`
|
||||||
fn internal_error(&self, message: impl Into<String>) -> CompilationError<'src> {
|
fn internal_error(&self, message: impl Into<String>) -> CompileError<'src> {
|
||||||
// Use `self.token_end` as the location of the error
|
// Use `self.token_end` as the location of the error
|
||||||
let token = Token {
|
let token = Token {
|
||||||
src: self.src,
|
src: self.src,
|
||||||
@ -209,8 +209,8 @@ impl<'src> Lexer<'src> {
|
|||||||
length: 0,
|
length: 0,
|
||||||
kind: Unspecified,
|
kind: Unspecified,
|
||||||
};
|
};
|
||||||
CompilationError {
|
CompileError {
|
||||||
kind: CompilationErrorKind::Internal {
|
kind: CompileErrorKind::Internal {
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
@ -218,7 +218,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a compilation error with `kind`
|
/// Create a compilation error with `kind`
|
||||||
fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
|
fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {
|
||||||
// Use the in-progress token span as the location of the error.
|
// Use the in-progress token span as the location of the error.
|
||||||
|
|
||||||
// The width of the error site to highlight depends on the kind of error:
|
// The width of the error site to highlight depends on the kind of error:
|
||||||
@ -244,11 +244,11 @@ impl<'src> Lexer<'src> {
|
|||||||
length,
|
length,
|
||||||
};
|
};
|
||||||
|
|
||||||
CompilationError { token, kind }
|
CompileError { token, kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompilationError<'src> {
|
fn unterminated_interpolation_error(interpolation_start: Token<'src>) -> CompileError<'src> {
|
||||||
CompilationError {
|
CompileError {
|
||||||
token: interpolation_start,
|
token: interpolation_start,
|
||||||
kind: UnterminatedInterpolation,
|
kind: UnterminatedInterpolation,
|
||||||
}
|
}
|
||||||
@ -289,7 +289,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Consume the text and produce a series of tokens
|
/// Consume the text and produce a series of tokens
|
||||||
fn tokenize(mut self) -> CompilationResult<'src, Vec<Token<'src>>> {
|
fn tokenize(mut self) -> CompileResult<'src, Vec<Token<'src>>> {
|
||||||
loop {
|
loop {
|
||||||
if self.token_start.column == 0 {
|
if self.token_start.column == 0 {
|
||||||
self.lex_line_start()?;
|
self.lex_line_start()?;
|
||||||
@ -327,7 +327,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle blank lines and indentation
|
/// Handle blank lines and indentation
|
||||||
fn lex_line_start(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_line_start(&mut self) -> CompileResult<'src, ()> {
|
||||||
enum Indentation<'src> {
|
enum Indentation<'src> {
|
||||||
// Line only contains whitespace
|
// Line only contains whitespace
|
||||||
Blank,
|
Blank,
|
||||||
@ -477,7 +477,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex token beginning with `start` outside of a recipe body
|
/// Lex token beginning with `start` outside of a recipe body
|
||||||
fn lex_normal(&mut self, start: char) -> CompilationResult<'src, ()> {
|
fn lex_normal(&mut self, start: char) -> CompileResult<'src, ()> {
|
||||||
match start {
|
match start {
|
||||||
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
'&' => self.lex_digraph('&', '&', AmpersandAmpersand),
|
||||||
'!' => self.lex_digraph('!', '=', BangEquals),
|
'!' => self.lex_digraph('!', '=', BangEquals),
|
||||||
@ -513,7 +513,7 @@ impl<'src> Lexer<'src> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
interpolation_start: Token<'src>,
|
interpolation_start: Token<'src>,
|
||||||
start: char,
|
start: char,
|
||||||
) -> CompilationResult<'src, ()> {
|
) -> CompileResult<'src, ()> {
|
||||||
if self.rest_starts_with("}}") {
|
if self.rest_starts_with("}}") {
|
||||||
// end current interpolation
|
// end current interpolation
|
||||||
if self.interpolation_stack.pop().is_none() {
|
if self.interpolation_stack.pop().is_none() {
|
||||||
@ -536,7 +536,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex token while in recipe body
|
/// Lex token while in recipe body
|
||||||
fn lex_body(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_body(&mut self) -> CompileResult<'src, ()> {
|
||||||
enum Terminator {
|
enum Terminator {
|
||||||
Newline,
|
Newline,
|
||||||
NewlineCarriageReturn,
|
NewlineCarriageReturn,
|
||||||
@ -599,14 +599,14 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a single-character token
|
/// Lex a single-character token
|
||||||
fn lex_single(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
fn lex_single(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
self.token(kind);
|
self.token(kind);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a double-character token
|
/// Lex a double-character token
|
||||||
fn lex_double(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
fn lex_double(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
self.token(kind);
|
self.token(kind);
|
||||||
@ -621,7 +621,7 @@ impl<'src> Lexer<'src> {
|
|||||||
second: char,
|
second: char,
|
||||||
then: TokenKind,
|
then: TokenKind,
|
||||||
otherwise: TokenKind,
|
otherwise: TokenKind,
|
||||||
) -> CompilationResult<'src, ()> {
|
) -> CompileResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
|
|
||||||
if self.accepted(second)? {
|
if self.accepted(second)? {
|
||||||
@ -634,7 +634,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex an opening or closing delimiter
|
/// Lex an opening or closing delimiter
|
||||||
fn lex_delimiter(&mut self, kind: TokenKind) -> CompilationResult<'src, ()> {
|
fn lex_delimiter(&mut self, kind: TokenKind) -> CompileResult<'src, ()> {
|
||||||
use Delimiter::*;
|
use Delimiter::*;
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
@ -663,7 +663,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pop a delimiter from the open delimiter stack and error if incorrect type
|
/// Pop a delimiter from the open delimiter stack and error if incorrect type
|
||||||
fn close_delimiter(&mut self, close: Delimiter) -> CompilationResult<'src, ()> {
|
fn close_delimiter(&mut self, close: Delimiter) -> CompileResult<'src, ()> {
|
||||||
match self.open_delimiters.pop() {
|
match self.open_delimiters.pop() {
|
||||||
Some((open, _)) if open == close => Ok(()),
|
Some((open, _)) if open == close => Ok(()),
|
||||||
Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {
|
Some((open, open_line)) => Err(self.error(MismatchedClosingDelimiter {
|
||||||
@ -681,12 +681,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a two-character digraph
|
/// Lex a two-character digraph
|
||||||
fn lex_digraph(
|
fn lex_digraph(&mut self, left: char, right: char, token: TokenKind) -> CompileResult<'src, ()> {
|
||||||
&mut self,
|
|
||||||
left: char,
|
|
||||||
right: char,
|
|
||||||
token: TokenKind,
|
|
||||||
) -> CompilationResult<'src, ()> {
|
|
||||||
self.presume(left)?;
|
self.presume(left)?;
|
||||||
|
|
||||||
if self.accepted(right)? {
|
if self.accepted(right)? {
|
||||||
@ -708,7 +703,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a token starting with ':'
|
/// Lex a token starting with ':'
|
||||||
fn lex_colon(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_colon(&mut self) -> CompileResult<'src, ()> {
|
||||||
self.presume(':')?;
|
self.presume(':')?;
|
||||||
|
|
||||||
if self.accepted('=')? {
|
if self.accepted('=')? {
|
||||||
@ -722,7 +717,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex a carriage return and line feed
|
/// Lex a carriage return and line feed
|
||||||
fn lex_eol(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_eol(&mut self) -> CompileResult<'src, ()> {
|
||||||
if self.accepted('\r')? {
|
if self.accepted('\r')? {
|
||||||
if !self.accepted('\n')? {
|
if !self.accepted('\n')? {
|
||||||
return Err(self.error(UnpairedCarriageReturn));
|
return Err(self.error(UnpairedCarriageReturn));
|
||||||
@ -743,7 +738,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
/// Lex name: [a-zA-Z_][a-zA-Z0-9_]*
|
||||||
fn lex_identifier(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_identifier(&mut self) -> CompileResult<'src, ()> {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
|
|
||||||
while let Some(c) = self.next {
|
while let Some(c) = self.next {
|
||||||
@ -760,7 +755,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex comment: #[^\r\n]
|
/// Lex comment: #[^\r\n]
|
||||||
fn lex_comment(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_comment(&mut self) -> CompileResult<'src, ()> {
|
||||||
self.presume('#')?;
|
self.presume('#')?;
|
||||||
|
|
||||||
while !self.at_eol_or_eof() {
|
while !self.at_eol_or_eof() {
|
||||||
@ -773,7 +768,7 @@ impl<'src> Lexer<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Lex whitespace: [ \t]+
|
/// Lex whitespace: [ \t]+
|
||||||
fn lex_whitespace(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_whitespace(&mut self) -> CompileResult<'src, ()> {
|
||||||
while self.next_is_whitespace() {
|
while self.next_is_whitespace() {
|
||||||
self.advance()?;
|
self.advance()?;
|
||||||
}
|
}
|
||||||
@ -788,7 +783,7 @@ impl<'src> Lexer<'src> {
|
|||||||
/// Backtick: `[^`]*`
|
/// Backtick: `[^`]*`
|
||||||
/// Cooked string: "[^"]*" # also processes escape sequences
|
/// Cooked string: "[^"]*" # also processes escape sequences
|
||||||
/// Raw string: '[^']*'
|
/// Raw string: '[^']*'
|
||||||
fn lex_string(&mut self) -> CompilationResult<'src, ()> {
|
fn lex_string(&mut self) -> CompileResult<'src, ()> {
|
||||||
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
|
let kind = if let Some(kind) = StringKind::from_token_start(self.rest()) {
|
||||||
kind
|
kind
|
||||||
} else {
|
} else {
|
||||||
@ -980,12 +975,12 @@ mod tests {
|
|||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
length: usize,
|
length: usize,
|
||||||
kind: CompilationErrorKind,
|
kind: CompileErrorKind,
|
||||||
) {
|
) {
|
||||||
match Lexer::lex(src) {
|
match Lexer::lex(src) {
|
||||||
Ok(_) => panic!("Lexing succeeded but expected"),
|
Ok(_) => panic!("Lexing succeeded but expected"),
|
||||||
Err(have) => {
|
Err(have) => {
|
||||||
let want = CompilationError {
|
let want = CompileError {
|
||||||
token: Token {
|
token: Token {
|
||||||
kind: have.token.kind,
|
kind: have.token.kind,
|
||||||
src,
|
src,
|
||||||
@ -2285,9 +2280,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn presume_error() {
|
fn presume_error() {
|
||||||
|
let compile_error = Lexer::new("!").presume('-').unwrap_err();
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
Lexer::new("!").presume('-').unwrap_err(),
|
compile_error,
|
||||||
CompilationError {
|
CompileError {
|
||||||
token: Token {
|
token: Token {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
@ -2297,22 +2293,22 @@ mod tests {
|
|||||||
kind: Unspecified,
|
kind: Unspecified,
|
||||||
},
|
},
|
||||||
kind: Internal {
|
kind: Internal {
|
||||||
message,
|
ref message,
|
||||||
},
|
},
|
||||||
} if message == "Lexer presumed character `-`"
|
} if message == "Lexer presumed character `-`"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(Vec::new());
|
||||||
|
|
||||||
|
Error::Compile { compile_error }
|
||||||
|
.write(&mut cursor, Color::never())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Lexer::new("!").presume('-').unwrap_err().to_string(),
|
str::from_utf8(&cursor.into_inner()).unwrap(),
|
||||||
unindent(
|
"error: Internal error, this may indicate a bug in just: \
|
||||||
"
|
Lexer presumed character `-`\nconsider filing an issue: \
|
||||||
Internal error, this may indicate a bug in just: Lexer presumed character `-`
|
https://github.com/casey/just/issues/new\n |\n1 | !\n | ^\n"
|
||||||
\
|
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new
|
|
||||||
|
|
|
||||||
1 | !
|
|
||||||
| ^"
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
clippy::missing_docs_in_private_items,
|
clippy::missing_docs_in_private_items,
|
||||||
clippy::missing_errors_doc,
|
clippy::missing_errors_doc,
|
||||||
clippy::missing_inline_in_public_items,
|
clippy::missing_inline_in_public_items,
|
||||||
|
clippy::needless_lifetimes,
|
||||||
clippy::needless_pass_by_value,
|
clippy::needless_pass_by_value,
|
||||||
clippy::non_ascii_literal,
|
clippy::non_ascii_literal,
|
||||||
clippy::option_if_let_else,
|
clippy::option_if_let_else,
|
||||||
@ -66,8 +67,8 @@ mod binding;
|
|||||||
mod color;
|
mod color;
|
||||||
mod command_ext;
|
mod command_ext;
|
||||||
mod common;
|
mod common;
|
||||||
mod compilation_error;
|
mod compile_error;
|
||||||
mod compilation_error_kind;
|
mod compile_error_kind;
|
||||||
mod compiler;
|
mod compiler;
|
||||||
mod config;
|
mod config;
|
||||||
mod config_error;
|
mod config_error;
|
||||||
@ -76,7 +77,6 @@ mod delimiter;
|
|||||||
mod dependency;
|
mod dependency;
|
||||||
mod enclosure;
|
mod enclosure;
|
||||||
mod error;
|
mod error;
|
||||||
mod error_result_ext;
|
|
||||||
mod evaluator;
|
mod evaluator;
|
||||||
mod expression;
|
mod expression;
|
||||||
mod fragment;
|
mod fragment;
|
||||||
@ -92,7 +92,7 @@ mod lexer;
|
|||||||
mod line;
|
mod line;
|
||||||
mod list;
|
mod list;
|
||||||
mod load_dotenv;
|
mod load_dotenv;
|
||||||
mod load_error;
|
mod loader;
|
||||||
mod name;
|
mod name;
|
||||||
mod ordinal;
|
mod ordinal;
|
||||||
mod output;
|
mod output;
|
||||||
@ -109,7 +109,6 @@ mod recipe;
|
|||||||
mod recipe_context;
|
mod recipe_context;
|
||||||
mod recipe_resolver;
|
mod recipe_resolver;
|
||||||
mod run;
|
mod run;
|
||||||
mod runtime_error;
|
|
||||||
mod scope;
|
mod scope;
|
||||||
mod search;
|
mod search;
|
||||||
mod search_config;
|
mod search_config;
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
21
src/loader.rs
Normal file
21
src/loader.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct Loader {
|
||||||
|
arena: Arena<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Loader {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Loader {
|
||||||
|
arena: Arena::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load<'src>(&'src self, path: &Path) -> RunResult<&'src str> {
|
||||||
|
let src = fs::read_to_string(path).map_err(|io_error| Error::Load {
|
||||||
|
path: path.to_owned(),
|
||||||
|
io_error,
|
||||||
|
})?;
|
||||||
|
Ok(self.arena.alloc(src))
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@ impl<'src> Name<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
|
pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {
|
||||||
self.token().error(kind)
|
self.token().error(kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
src/parser.rs
102
src/parser.rs
@ -36,7 +36,7 @@ pub(crate) struct Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
impl<'tokens, 'src> Parser<'tokens, 'src> {
|
impl<'tokens, 'src> Parser<'tokens, 'src> {
|
||||||
/// Parse `tokens` into an `Ast`
|
/// Parse `tokens` into an `Ast`
|
||||||
pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompilationResult<'src, Ast<'src>> {
|
pub(crate) fn parse(tokens: &'tokens [Token<'src>]) -> CompileResult<'src, Ast<'src>> {
|
||||||
Self::new(tokens).parse_ast()
|
Self::new(tokens).parse_ast()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,27 +49,21 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn error(
|
fn error(&self, kind: CompileErrorKind<'src>) -> CompileResult<'src, CompileError<'src>> {
|
||||||
&self,
|
|
||||||
kind: CompilationErrorKind<'src>,
|
|
||||||
) -> CompilationResult<'src, CompilationError<'src>> {
|
|
||||||
Ok(self.next()?.error(kind))
|
Ok(self.next()?.error(kind))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct an unexpected token error with the token returned by
|
/// Construct an unexpected token error with the token returned by
|
||||||
/// `Parser::next`
|
/// `Parser::next`
|
||||||
fn unexpected_token(&self) -> CompilationResult<'src, CompilationError<'src>> {
|
fn unexpected_token(&self) -> CompileResult<'src, CompileError<'src>> {
|
||||||
self.error(CompilationErrorKind::UnexpectedToken {
|
self.error(CompileErrorKind::UnexpectedToken {
|
||||||
expected: self.expected.iter().cloned().collect::<Vec<TokenKind>>(),
|
expected: self.expected.iter().cloned().collect::<Vec<TokenKind>>(),
|
||||||
found: self.next()?.kind,
|
found: self.next()?.kind,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn internal_error(
|
fn internal_error(&self, message: impl Into<String>) -> CompileResult<'src, CompileError<'src>> {
|
||||||
&self,
|
self.error(CompileErrorKind::Internal {
|
||||||
message: impl Into<String>,
|
|
||||||
) -> CompilationResult<'src, CompilationError<'src>> {
|
|
||||||
self.error(CompilationErrorKind::Internal {
|
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -83,7 +77,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The next significant token
|
/// The next significant token
|
||||||
fn next(&self) -> CompilationResult<'src, Token<'src>> {
|
fn next(&self) -> CompileResult<'src, Token<'src>> {
|
||||||
if let Some(token) = self.rest().next() {
|
if let Some(token) = self.rest().next() {
|
||||||
Ok(token)
|
Ok(token)
|
||||||
} else {
|
} else {
|
||||||
@ -118,7 +112,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the `n`th next significant token
|
/// Get the `n`th next significant token
|
||||||
fn get(&self, n: usize) -> CompilationResult<'src, Token<'src>> {
|
fn get(&self, n: usize) -> CompileResult<'src, Token<'src>> {
|
||||||
match self.rest().nth(n) {
|
match self.rest().nth(n) {
|
||||||
Some(token) => Ok(token),
|
Some(token) => Ok(token),
|
||||||
None => Err(self.internal_error("`Parser::get()` advanced past end of token stream")?),
|
None => Err(self.internal_error("`Parser::get()` advanced past end of token stream")?),
|
||||||
@ -126,7 +120,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advance past one significant token, clearing the expected token set.
|
/// Advance past one significant token, clearing the expected token set.
|
||||||
fn advance(&mut self) -> CompilationResult<'src, Token<'src>> {
|
fn advance(&mut self) -> CompileResult<'src, Token<'src>> {
|
||||||
self.expected.clear();
|
self.expected.clear();
|
||||||
|
|
||||||
for skipped in &self.tokens[self.next..] {
|
for skipped in &self.tokens[self.next..] {
|
||||||
@ -142,7 +136,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Return the next token if it is of kind `expected`, otherwise, return an
|
/// Return the next token if it is of kind `expected`, otherwise, return an
|
||||||
/// unexpected token error
|
/// unexpected token error
|
||||||
fn expect(&mut self, expected: TokenKind) -> CompilationResult<'src, Token<'src>> {
|
fn expect(&mut self, expected: TokenKind) -> CompileResult<'src, Token<'src>> {
|
||||||
if let Some(token) = self.accept(expected)? {
|
if let Some(token) = self.accept(expected)? {
|
||||||
Ok(token)
|
Ok(token)
|
||||||
} else {
|
} else {
|
||||||
@ -151,7 +145,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an unexpected token error if the next token is not an EOL
|
/// Return an unexpected token error if the next token is not an EOL
|
||||||
fn expect_eol(&mut self) -> CompilationResult<'src, ()> {
|
fn expect_eol(&mut self) -> CompileResult<'src, ()> {
|
||||||
self.accept(Comment)?;
|
self.accept(Comment)?;
|
||||||
|
|
||||||
if self.next_is(Eof) {
|
if self.next_is(Eof) {
|
||||||
@ -161,14 +155,14 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
self.expect(Eol).map(|_| ())
|
self.expect(Eol).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expect_keyword(&mut self, expected: Keyword) -> CompilationResult<'src, ()> {
|
fn expect_keyword(&mut self, expected: Keyword) -> CompileResult<'src, ()> {
|
||||||
let identifier = self.expect(Identifier)?;
|
let identifier = self.expect(Identifier)?;
|
||||||
let found = identifier.lexeme();
|
let found = identifier.lexeme();
|
||||||
|
|
||||||
if expected == found {
|
if expected == found {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(identifier.error(CompilationErrorKind::ExpectedKeyword {
|
Err(identifier.error(CompileErrorKind::ExpectedKeyword {
|
||||||
expected: vec![expected],
|
expected: vec![expected],
|
||||||
found,
|
found,
|
||||||
}))
|
}))
|
||||||
@ -177,7 +171,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
|
|
||||||
/// Return an internal error if the next token is not of kind `Identifier`
|
/// Return an internal error if the next token is not of kind `Identifier`
|
||||||
/// with lexeme `lexeme`.
|
/// with lexeme `lexeme`.
|
||||||
fn presume_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, ()> {
|
fn presume_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, ()> {
|
||||||
let next = self.advance()?;
|
let next = self.advance()?;
|
||||||
|
|
||||||
if next.kind != Identifier {
|
if next.kind != Identifier {
|
||||||
@ -197,7 +191,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an internal error if the next token is not of kind `kind`.
|
/// Return an internal error if the next token is not of kind `kind`.
|
||||||
fn presume(&mut self, kind: TokenKind) -> CompilationResult<'src, Token<'src>> {
|
fn presume(&mut self, kind: TokenKind) -> CompileResult<'src, Token<'src>> {
|
||||||
let next = self.advance()?;
|
let next = self.advance()?;
|
||||||
|
|
||||||
if next.kind != kind {
|
if next.kind != kind {
|
||||||
@ -211,7 +205,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an internal error if the next token is not one of kinds `kinds`.
|
/// Return an internal error if the next token is not one of kinds `kinds`.
|
||||||
fn presume_any(&mut self, kinds: &[TokenKind]) -> CompilationResult<'src, Token<'src>> {
|
fn presume_any(&mut self, kinds: &[TokenKind]) -> CompileResult<'src, Token<'src>> {
|
||||||
let next = self.advance()?;
|
let next = self.advance()?;
|
||||||
if !kinds.contains(&next.kind) {
|
if !kinds.contains(&next.kind) {
|
||||||
Err(self.internal_error(format!(
|
Err(self.internal_error(format!(
|
||||||
@ -225,7 +219,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Accept and return a token of kind `kind`
|
/// Accept and return a token of kind `kind`
|
||||||
fn accept(&mut self, kind: TokenKind) -> CompilationResult<'src, Option<Token<'src>>> {
|
fn accept(&mut self, kind: TokenKind) -> CompileResult<'src, Option<Token<'src>>> {
|
||||||
if self.next_is(kind) {
|
if self.next_is(kind) {
|
||||||
Ok(Some(self.advance()?))
|
Ok(Some(self.advance()?))
|
||||||
} else {
|
} else {
|
||||||
@ -234,9 +228,9 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an error if the next token is of kind `forbidden`
|
/// Return an error if the next token is of kind `forbidden`
|
||||||
fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompilationResult<'src, ()>
|
fn forbid<F>(&self, forbidden: TokenKind, error: F) -> CompileResult<'src, ()>
|
||||||
where
|
where
|
||||||
F: FnOnce(Token) -> CompilationError,
|
F: FnOnce(Token) -> CompileError,
|
||||||
{
|
{
|
||||||
let next = self.next()?;
|
let next = self.next()?;
|
||||||
|
|
||||||
@ -248,7 +242,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Accept a token of kind `Identifier` and parse into a `Name`
|
/// Accept a token of kind `Identifier` and parse into a `Name`
|
||||||
fn accept_name(&mut self) -> CompilationResult<'src, Option<Name<'src>>> {
|
fn accept_name(&mut self) -> CompileResult<'src, Option<Name<'src>>> {
|
||||||
if self.next_is(Identifier) {
|
if self.next_is(Identifier) {
|
||||||
Ok(Some(self.parse_name()?))
|
Ok(Some(self.parse_name()?))
|
||||||
} else {
|
} else {
|
||||||
@ -256,7 +250,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accepted_keyword(&mut self, keyword: Keyword) -> CompilationResult<'src, bool> {
|
fn accepted_keyword(&mut self, keyword: Keyword) -> CompileResult<'src, bool> {
|
||||||
let next = self.next()?;
|
let next = self.next()?;
|
||||||
|
|
||||||
if next.kind == Identifier && next.lexeme() == keyword.lexeme() {
|
if next.kind == Identifier && next.lexeme() == keyword.lexeme() {
|
||||||
@ -268,7 +262,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Accept a dependency
|
/// Accept a dependency
|
||||||
fn accept_dependency(&mut self) -> CompilationResult<'src, Option<UnresolvedDependency<'src>>> {
|
fn accept_dependency(&mut self) -> CompileResult<'src, Option<UnresolvedDependency<'src>>> {
|
||||||
if let Some(recipe) = self.accept_name()? {
|
if let Some(recipe) = self.accept_name()? {
|
||||||
Ok(Some(UnresolvedDependency {
|
Ok(Some(UnresolvedDependency {
|
||||||
arguments: Vec::new(),
|
arguments: Vec::new(),
|
||||||
@ -290,12 +284,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Accept and return `true` if next token is of kind `kind`
|
/// Accept and return `true` if next token is of kind `kind`
|
||||||
fn accepted(&mut self, kind: TokenKind) -> CompilationResult<'src, bool> {
|
fn accepted(&mut self, kind: TokenKind) -> CompileResult<'src, bool> {
|
||||||
Ok(self.accept(kind)?.is_some())
|
Ok(self.accept(kind)?.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a justfile, consumes self
|
/// Parse a justfile, consumes self
|
||||||
fn parse_ast(mut self) -> CompilationResult<'src, Ast<'src>> {
|
fn parse_ast(mut self) -> CompileResult<'src, Ast<'src>> {
|
||||||
fn pop_doc_comment<'src>(
|
fn pop_doc_comment<'src>(
|
||||||
items: &mut Vec<Item<'src>>,
|
items: &mut Vec<Item<'src>>,
|
||||||
eol_since_last_comment: bool,
|
eol_since_last_comment: bool,
|
||||||
@ -330,7 +324,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
match Keyword::from_lexeme(next.lexeme()) {
|
match Keyword::from_lexeme(next.lexeme()) {
|
||||||
Some(Keyword::Alias) =>
|
Some(Keyword::Alias) =>
|
||||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||||
return Err(self.get(2)?.error(CompilationErrorKind::DeprecatedEquals));
|
return Err(self.get(2)?.error(CompileErrorKind::DeprecatedEquals));
|
||||||
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||||
items.push(Item::Alias(self.parse_alias()?));
|
items.push(Item::Alias(self.parse_alias()?));
|
||||||
} else {
|
} else {
|
||||||
@ -339,7 +333,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
},
|
},
|
||||||
Some(Keyword::Export) =>
|
Some(Keyword::Export) =>
|
||||||
if self.next_are(&[Identifier, Identifier, Equals]) {
|
if self.next_are(&[Identifier, Identifier, Equals]) {
|
||||||
return Err(self.get(2)?.error(CompilationErrorKind::DeprecatedEquals));
|
return Err(self.get(2)?.error(CompileErrorKind::DeprecatedEquals));
|
||||||
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
} else if self.next_are(&[Identifier, Identifier, ColonEquals]) {
|
||||||
self.presume_keyword(Keyword::Export)?;
|
self.presume_keyword(Keyword::Export)?;
|
||||||
items.push(Item::Assignment(self.parse_assignment(true)?));
|
items.push(Item::Assignment(self.parse_assignment(true)?));
|
||||||
@ -359,7 +353,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
},
|
},
|
||||||
_ =>
|
_ =>
|
||||||
if self.next_are(&[Identifier, Equals]) {
|
if self.next_are(&[Identifier, Equals]) {
|
||||||
return Err(self.get(1)?.error(CompilationErrorKind::DeprecatedEquals));
|
return Err(self.get(1)?.error(CompileErrorKind::DeprecatedEquals));
|
||||||
} else if self.next_are(&[Identifier, ColonEquals]) {
|
} else if self.next_are(&[Identifier, ColonEquals]) {
|
||||||
items.push(Item::Assignment(self.parse_assignment(false)?));
|
items.push(Item::Assignment(self.parse_assignment(false)?));
|
||||||
} else {
|
} else {
|
||||||
@ -389,7 +383,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an alias, e.g `alias name := target`
|
/// Parse an alias, e.g `alias name := target`
|
||||||
fn parse_alias(&mut self) -> CompilationResult<'src, Alias<'src, Name<'src>>> {
|
fn parse_alias(&mut self) -> CompileResult<'src, Alias<'src, Name<'src>>> {
|
||||||
self.presume_keyword(Keyword::Alias)?;
|
self.presume_keyword(Keyword::Alias)?;
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
self.presume_any(&[Equals, ColonEquals])?;
|
self.presume_any(&[Equals, ColonEquals])?;
|
||||||
@ -399,7 +393,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an assignment, e.g. `foo := bar`
|
/// Parse an assignment, e.g. `foo := bar`
|
||||||
fn parse_assignment(&mut self, export: bool) -> CompilationResult<'src, Assignment<'src>> {
|
fn parse_assignment(&mut self, export: bool) -> CompileResult<'src, Assignment<'src>> {
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
self.presume_any(&[Equals, ColonEquals])?;
|
self.presume_any(&[Equals, ColonEquals])?;
|
||||||
let value = self.parse_expression()?;
|
let value = self.parse_expression()?;
|
||||||
@ -412,7 +406,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an expression, e.g. `1 + 2`
|
/// Parse an expression, e.g. `1 + 2`
|
||||||
fn parse_expression(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_expression(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
if self.accepted_keyword(Keyword::If)? {
|
if self.accepted_keyword(Keyword::If)? {
|
||||||
self.parse_conditional()
|
self.parse_conditional()
|
||||||
} else {
|
} else {
|
||||||
@ -429,7 +423,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
/// Parse a conditional, e.g. `if a == b { "foo" } else { "bar" }`
|
||||||
fn parse_conditional(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_conditional(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
let lhs = self.parse_expression()?;
|
let lhs = self.parse_expression()?;
|
||||||
|
|
||||||
let inverted = self.accepted(BangEquals)?;
|
let inverted = self.accepted(BangEquals)?;
|
||||||
@ -467,7 +461,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a value, e.g. `(bar)`
|
/// Parse a value, e.g. `(bar)`
|
||||||
fn parse_value(&mut self) -> CompilationResult<'src, Expression<'src>> {
|
fn parse_value(&mut self) -> CompileResult<'src, Expression<'src>> {
|
||||||
if self.next_is(StringToken) {
|
if self.next_is(StringToken) {
|
||||||
Ok(Expression::StringLiteral {
|
Ok(Expression::StringLiteral {
|
||||||
string_literal: self.parse_string_literal()?,
|
string_literal: self.parse_string_literal()?,
|
||||||
@ -485,7 +479,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if contents.starts_with("#!") {
|
if contents.starts_with("#!") {
|
||||||
return Err(next.error(CompilationErrorKind::BacktickShebang));
|
return Err(next.error(CompileErrorKind::BacktickShebang));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Expression::Backtick { contents, token })
|
Ok(Expression::Backtick { contents, token })
|
||||||
@ -511,7 +505,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a string literal, e.g. `"FOO"`
|
/// Parse a string literal, e.g. `"FOO"`
|
||||||
fn parse_string_literal(&mut self) -> CompilationResult<'src, StringLiteral<'src>> {
|
fn parse_string_literal(&mut self) -> CompileResult<'src, StringLiteral<'src>> {
|
||||||
let token = self.expect(StringToken)?;
|
let token = self.expect(StringToken)?;
|
||||||
|
|
||||||
let kind = StringKind::from_string_or_backtick(token)?;
|
let kind = StringKind::from_string_or_backtick(token)?;
|
||||||
@ -540,7 +534,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
'"' => cooked.push('"'),
|
'"' => cooked.push('"'),
|
||||||
other => {
|
other => {
|
||||||
return Err(
|
return Err(
|
||||||
token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
|
token.error(CompileErrorKind::InvalidEscapeSequence { character: other }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -560,12 +554,12 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a name from an identifier token
|
/// Parse a name from an identifier token
|
||||||
fn parse_name(&mut self) -> CompilationResult<'src, Name<'src>> {
|
fn parse_name(&mut self) -> CompileResult<'src, Name<'src>> {
|
||||||
self.expect(Identifier).map(Name::from_identifier)
|
self.expect(Identifier).map(Name::from_identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse sequence of comma-separated expressions
|
/// Parse sequence of comma-separated expressions
|
||||||
fn parse_sequence(&mut self) -> CompilationResult<'src, Vec<Expression<'src>>> {
|
fn parse_sequence(&mut self) -> CompileResult<'src, Vec<Expression<'src>>> {
|
||||||
self.presume(ParenL)?;
|
self.presume(ParenL)?;
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
@ -588,7 +582,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
doc: Option<&'src str>,
|
doc: Option<&'src str>,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
) -> CompilationResult<'src, UnresolvedRecipe<'src>> {
|
) -> CompileResult<'src, UnresolvedRecipe<'src>> {
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
|
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
@ -609,7 +603,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
let variadic = self.parse_parameter(kind)?;
|
let variadic = self.parse_parameter(kind)?;
|
||||||
|
|
||||||
self.forbid(Identifier, |token| {
|
self.forbid(Identifier, |token| {
|
||||||
token.error(CompilationErrorKind::ParameterFollowsVariadicParameter {
|
token.error(CompileErrorKind::ParameterFollowsVariadicParameter {
|
||||||
parameter: token.lexeme(),
|
parameter: token.lexeme(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
@ -661,7 +655,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a recipe parameter
|
/// Parse a recipe parameter
|
||||||
fn parse_parameter(&mut self, kind: ParameterKind) -> CompilationResult<'src, Parameter<'src>> {
|
fn parse_parameter(&mut self, kind: ParameterKind) -> CompileResult<'src, Parameter<'src>> {
|
||||||
let export = self.accepted(Dollar)?;
|
let export = self.accepted(Dollar)?;
|
||||||
|
|
||||||
let name = self.parse_name()?;
|
let name = self.parse_name()?;
|
||||||
@ -681,7 +675,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the body of a recipe
|
/// Parse the body of a recipe
|
||||||
fn parse_body(&mut self) -> CompilationResult<'src, Vec<Line<'src>>> {
|
fn parse_body(&mut self) -> CompileResult<'src, Vec<Line<'src>>> {
|
||||||
let mut lines = Vec::new();
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
if self.accepted(Indent)? {
|
if self.accepted(Indent)? {
|
||||||
@ -721,7 +715,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a boolean setting value
|
/// Parse a boolean setting value
|
||||||
fn parse_set_bool(&mut self) -> CompilationResult<'src, bool> {
|
fn parse_set_bool(&mut self) -> CompileResult<'src, bool> {
|
||||||
if !self.accepted(ColonEquals)? {
|
if !self.accepted(ColonEquals)? {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@ -733,7 +727,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
} else if Keyword::False == identifier.lexeme() {
|
} else if Keyword::False == identifier.lexeme() {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
return Err(identifier.error(CompilationErrorKind::ExpectedKeyword {
|
return Err(identifier.error(CompileErrorKind::ExpectedKeyword {
|
||||||
expected: vec![Keyword::True, Keyword::False],
|
expected: vec![Keyword::True, Keyword::False],
|
||||||
found: identifier.lexeme(),
|
found: identifier.lexeme(),
|
||||||
}));
|
}));
|
||||||
@ -743,7 +737,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a setting
|
/// Parse a setting
|
||||||
fn parse_set(&mut self) -> CompilationResult<'src, Set<'src>> {
|
fn parse_set(&mut self) -> CompileResult<'src, Set<'src>> {
|
||||||
self.presume_keyword(Keyword::Set)?;
|
self.presume_keyword(Keyword::Set)?;
|
||||||
let name = Name::from_identifier(self.presume(Identifier)?);
|
let name = Name::from_identifier(self.presume(Identifier)?);
|
||||||
let lexeme = name.lexeme();
|
let lexeme = name.lexeme();
|
||||||
@ -794,7 +788,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
|
|||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(name.error(CompilationErrorKind::UnknownSetting {
|
Err(name.error(CompileErrorKind::UnknownSetting {
|
||||||
setting: name.lexeme(),
|
setting: name.lexeme(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -806,7 +800,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
|
|
||||||
macro_rules! test {
|
macro_rules! test {
|
||||||
{
|
{
|
||||||
@ -860,14 +854,14 @@ mod tests {
|
|||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
length: usize,
|
length: usize,
|
||||||
kind: CompilationErrorKind,
|
kind: CompileErrorKind,
|
||||||
) {
|
) {
|
||||||
let tokens = Lexer::lex(src).expect("Lexing failed in parse test...");
|
let tokens = Lexer::lex(src).expect("Lexing failed in parse test...");
|
||||||
|
|
||||||
match Parser::parse(&tokens) {
|
match Parser::parse(&tokens) {
|
||||||
Ok(_) => panic!("Parsing unexpectedly succeeded"),
|
Ok(_) => panic!("Parsing unexpectedly succeeded"),
|
||||||
Err(have) => {
|
Err(have) => {
|
||||||
let want = CompilationError {
|
let want = CompileError {
|
||||||
token: Token {
|
token: Token {
|
||||||
kind: have.token.kind,
|
kind: have.token.kind,
|
||||||
src,
|
src,
|
||||||
|
@ -2,20 +2,16 @@ use crate::common::*;
|
|||||||
|
|
||||||
use std::process::{ExitStatus, Stdio};
|
use std::process::{ExitStatus, Stdio};
|
||||||
|
|
||||||
/// Return a `RuntimeError::Signal` if the process was terminated by a signal,
|
/// Return a `Error::Signal` if the process was terminated by a signal,
|
||||||
/// otherwise return an `RuntimeError::UnknownFailure`
|
/// otherwise return an `Error::UnknownFailure`
|
||||||
fn error_from_signal(
|
fn error_from_signal(recipe: &str, line_number: Option<usize>, exit_status: ExitStatus) -> Error {
|
||||||
recipe: &str,
|
|
||||||
line_number: Option<usize>,
|
|
||||||
exit_status: ExitStatus,
|
|
||||||
) -> RuntimeError {
|
|
||||||
match Platform::signal_from_exit_status(exit_status) {
|
match Platform::signal_from_exit_status(exit_status) {
|
||||||
Some(signal) => RuntimeError::Signal {
|
Some(signal) => Error::Signal {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
None => RuntimeError::Unknown {
|
None => Error::Unknown {
|
||||||
recipe,
|
recipe,
|
||||||
line_number,
|
line_number,
|
||||||
},
|
},
|
||||||
@ -108,20 +104,18 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let shebang_line = evaluated_lines
|
let shebang_line = evaluated_lines.first().ok_or_else(|| Error::Internal {
|
||||||
.first()
|
|
||||||
.ok_or_else(|| RuntimeError::Internal {
|
|
||||||
message: "evaluated_lines was empty".to_owned(),
|
message: "evaluated_lines was empty".to_owned(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let shebang = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
|
let shebang = Shebang::new(shebang_line).ok_or_else(|| Error::Internal {
|
||||||
message: format!("bad shebang line: {}", shebang_line),
|
message: format!("bad shebang line: {}", shebang_line),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let tmp = tempfile::Builder::new()
|
let tmp = tempfile::Builder::new()
|
||||||
.prefix("just")
|
.prefix("just")
|
||||||
.tempdir()
|
.tempdir()
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError {
|
.map_err(|error| Error::TmpdirIo {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
io_error: error,
|
io_error: error,
|
||||||
})?;
|
})?;
|
||||||
@ -130,7 +124,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
path.push(shebang.script_filename(self.name()));
|
path.push(shebang.script_filename(self.name()));
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
let mut f = fs::File::create(&path).map_err(|error| Error::TmpdirIo {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
io_error: error,
|
io_error: error,
|
||||||
})?;
|
})?;
|
||||||
@ -158,14 +152,14 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.write_all(text.as_bytes())
|
f.write_all(text.as_bytes())
|
||||||
.map_err(|error| RuntimeError::TmpdirIoError {
|
.map_err(|error| Error::TmpdirIo {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
io_error: error,
|
io_error: error,
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// make the script executable
|
// make the script executable
|
||||||
Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
|
Platform::set_execute_permission(&path).map_err(|error| Error::TmpdirIo {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
io_error: error,
|
io_error: error,
|
||||||
})?;
|
})?;
|
||||||
@ -173,7 +167,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
// create a command to run the script
|
// create a command to run the script
|
||||||
let mut command =
|
let mut command =
|
||||||
Platform::make_shebang_command(&path, &context.search.working_directory, shebang).map_err(
|
Platform::make_shebang_command(&path, &context.search.working_directory, shebang).map_err(
|
||||||
|output_error| RuntimeError::Cygpath {
|
|output_error| Error::Cygpath {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
output_error,
|
output_error,
|
||||||
},
|
},
|
||||||
@ -190,7 +184,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
Ok(exit_status) =>
|
Ok(exit_status) =>
|
||||||
if let Some(code) = exit_status.code() {
|
if let Some(code) = exit_status.code() {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
return Err(RuntimeError::Code {
|
return Err(Error::Code {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
line_number: None,
|
line_number: None,
|
||||||
code,
|
code,
|
||||||
@ -200,7 +194,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
return Err(error_from_signal(self.name(), None, exit_status));
|
return Err(error_from_signal(self.name(), None, exit_status));
|
||||||
},
|
},
|
||||||
Err(io_error) => {
|
Err(io_error) => {
|
||||||
return Err(RuntimeError::Shebang {
|
return Err(Error::Shebang {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
command: shebang.interpreter.to_owned(),
|
command: shebang.interpreter.to_owned(),
|
||||||
argument: shebang.argument.map(String::from),
|
argument: shebang.argument.map(String::from),
|
||||||
@ -283,7 +277,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
Ok(exit_status) =>
|
Ok(exit_status) =>
|
||||||
if let Some(code) = exit_status.code() {
|
if let Some(code) = exit_status.code() {
|
||||||
if code != 0 && !infallable_command {
|
if code != 0 && !infallable_command {
|
||||||
return Err(RuntimeError::Code {
|
return Err(Error::Code {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
line_number: Some(line_number),
|
line_number: Some(line_number),
|
||||||
code,
|
code,
|
||||||
@ -297,7 +291,7 @@ impl<'src, D> Recipe<'src, D> {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
Err(io_error) => {
|
Err(io_error) => {
|
||||||
return Err(RuntimeError::IoError {
|
return Err(Error::Io {
|
||||||
recipe: self.name(),
|
recipe: self.name(),
|
||||||
io_error,
|
io_error,
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
use CompilationErrorKind::*;
|
use CompileErrorKind::*;
|
||||||
|
|
||||||
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
|
pub(crate) struct RecipeResolver<'src: 'run, 'run> {
|
||||||
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||||
@ -12,7 +12,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
pub(crate) fn resolve_recipes(
|
pub(crate) fn resolve_recipes(
|
||||||
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
unresolved_recipes: Table<'src, UnresolvedRecipe<'src>>,
|
||||||
assignments: &Table<'src, Assignment<'src>>,
|
assignments: &Table<'src, Assignment<'src>>,
|
||||||
) -> CompilationResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
) -> CompileResult<'src, Table<'src, Rc<Recipe<'src>>>> {
|
||||||
let mut resolver = RecipeResolver {
|
let mut resolver = RecipeResolver {
|
||||||
resolved_recipes: Table::new(),
|
resolved_recipes: Table::new(),
|
||||||
unresolved_recipes,
|
unresolved_recipes,
|
||||||
@ -58,7 +58,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
&self,
|
&self,
|
||||||
variable: &Token<'src>,
|
variable: &Token<'src>,
|
||||||
parameters: &[Parameter],
|
parameters: &[Parameter],
|
||||||
) -> CompilationResult<'src, ()> {
|
) -> CompileResult<'src, ()> {
|
||||||
let name = variable.lexeme();
|
let name = variable.lexeme();
|
||||||
let undefined =
|
let undefined =
|
||||||
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
!self.assignments.contains_key(name) && !parameters.iter().any(|p| p.name.lexeme() == name);
|
||||||
@ -74,7 +74,7 @@ impl<'src: 'run, 'run> RecipeResolver<'src, 'run> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
stack: &mut Vec<&'src str>,
|
stack: &mut Vec<&'src str>,
|
||||||
recipe: UnresolvedRecipe<'src>,
|
recipe: UnresolvedRecipe<'src>,
|
||||||
) -> CompilationResult<'src, Rc<Recipe<'src>>> {
|
) -> CompileResult<'src, Rc<Recipe<'src>>> {
|
||||||
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
|
if let Some(resolved) = self.resolved_recipes.get(recipe.name()) {
|
||||||
return Ok(Rc::clone(resolved));
|
return Ok(Rc::clone(resolved));
|
||||||
}
|
}
|
||||||
|
19
src/run.rs
19
src/run.rs
@ -16,7 +16,22 @@ pub fn run() -> Result<(), i32> {
|
|||||||
info!("Parsing command line arguments…");
|
info!("Parsing command line arguments…");
|
||||||
let matches = app.get_matches();
|
let matches = app.get_matches();
|
||||||
|
|
||||||
let config = Config::from_matches(&matches).eprint(Color::auto())?;
|
let loader = Loader::new();
|
||||||
|
|
||||||
config.run_subcommand()
|
let mut color = Color::auto();
|
||||||
|
let mut verbosity = Verbosity::default();
|
||||||
|
|
||||||
|
Config::from_matches(&matches)
|
||||||
|
.map_err(Error::from)
|
||||||
|
.and_then(|config| {
|
||||||
|
color = config.color;
|
||||||
|
verbosity = config.verbosity;
|
||||||
|
config.run_subcommand(&loader)
|
||||||
|
})
|
||||||
|
.map_err(|error| {
|
||||||
|
if !verbosity.quiet() {
|
||||||
|
error.write(&mut io::stderr(), color).ok();
|
||||||
|
}
|
||||||
|
error.code().unwrap_or(EXIT_FAILURE)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,437 +0,0 @@
|
|||||||
use crate::common::*;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) enum RuntimeError<'src> {
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe: &'src str,
|
|
||||||
parameters: Vec<&'src Parameter<'src>>,
|
|
||||||
found: usize,
|
|
||||||
min: usize,
|
|
||||||
max: usize,
|
|
||||||
},
|
|
||||||
Backtick {
|
|
||||||
token: Token<'src>,
|
|
||||||
output_error: OutputError,
|
|
||||||
},
|
|
||||||
Code {
|
|
||||||
recipe: &'src str,
|
|
||||||
line_number: Option<usize>,
|
|
||||||
code: i32,
|
|
||||||
},
|
|
||||||
CommandInvocation {
|
|
||||||
binary: OsString,
|
|
||||||
arguments: Vec<OsString>,
|
|
||||||
io_error: io::Error,
|
|
||||||
},
|
|
||||||
Cygpath {
|
|
||||||
recipe: &'src str,
|
|
||||||
output_error: OutputError,
|
|
||||||
},
|
|
||||||
Dotenv {
|
|
||||||
dotenv_error: dotenv::Error,
|
|
||||||
},
|
|
||||||
EvalUnknownVariable {
|
|
||||||
variable: String,
|
|
||||||
suggestion: Option<Suggestion<'src>>,
|
|
||||||
},
|
|
||||||
FunctionCall {
|
|
||||||
function: Name<'src>,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
Internal {
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
IoError {
|
|
||||||
recipe: &'src str,
|
|
||||||
io_error: io::Error,
|
|
||||||
},
|
|
||||||
Shebang {
|
|
||||||
recipe: &'src str,
|
|
||||||
command: String,
|
|
||||||
argument: Option<String>,
|
|
||||||
io_error: io::Error,
|
|
||||||
},
|
|
||||||
Signal {
|
|
||||||
recipe: &'src str,
|
|
||||||
line_number: Option<usize>,
|
|
||||||
signal: i32,
|
|
||||||
},
|
|
||||||
TmpdirIoError {
|
|
||||||
recipe: &'src str,
|
|
||||||
io_error: io::Error,
|
|
||||||
},
|
|
||||||
UnknownOverrides {
|
|
||||||
overrides: Vec<&'src str>,
|
|
||||||
},
|
|
||||||
UnknownRecipes {
|
|
||||||
recipes: Vec<&'src str>,
|
|
||||||
suggestion: Option<Suggestion<'src>>,
|
|
||||||
},
|
|
||||||
Unknown {
|
|
||||||
recipe: &'src str,
|
|
||||||
line_number: Option<usize>,
|
|
||||||
},
|
|
||||||
NoRecipes,
|
|
||||||
DefaultRecipeRequiresArguments {
|
|
||||||
recipe: &'src str,
|
|
||||||
min_arguments: usize,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> Error for RuntimeError<'src> {
|
|
||||||
fn code(&self) -> i32 {
|
|
||||||
match *self {
|
|
||||||
Self::Code { code, .. }
|
|
||||||
| Self::Backtick {
|
|
||||||
output_error: OutputError::Code(code),
|
|
||||||
..
|
|
||||||
} => code,
|
|
||||||
_ => EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> RuntimeError<'src> {
|
|
||||||
fn context(&self) -> Option<Token> {
|
|
||||||
use RuntimeError::*;
|
|
||||||
match self {
|
|
||||||
FunctionCall { function, .. } => Some(function.token()),
|
|
||||||
Backtick { token, .. } => Some(*token),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> Display for RuntimeError<'src> {
|
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
|
||||||
use RuntimeError::*;
|
|
||||||
|
|
||||||
let color = if f.alternate() {
|
|
||||||
Color::always()
|
|
||||||
} else {
|
|
||||||
Color::never()
|
|
||||||
};
|
|
||||||
let message = color.message();
|
|
||||||
write!(f, "{}", message.prefix())?;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
EvalUnknownVariable {
|
|
||||||
variable,
|
|
||||||
suggestion,
|
|
||||||
} => {
|
|
||||||
write!(f, "Justfile does not contain variable `{}`.", variable,)?;
|
|
||||||
if let Some(suggestion) = *suggestion {
|
|
||||||
write!(f, "\n{}", suggestion)?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UnknownRecipes {
|
|
||||||
recipes,
|
|
||||||
suggestion,
|
|
||||||
} => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Justfile does not contain {} {}.",
|
|
||||||
Count("recipe", recipes.len()),
|
|
||||||
List::or_ticked(recipes),
|
|
||||||
)?;
|
|
||||||
if let Some(suggestion) = *suggestion {
|
|
||||||
write!(f, "\n{}", suggestion)?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UnknownOverrides { overrides } => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{} {} overridden on the command line but not present in justfile",
|
|
||||||
Count("Variable", overrides.len()),
|
|
||||||
List::and_ticked(overrides),
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
ArgumentCountMismatch {
|
|
||||||
recipe,
|
|
||||||
parameters,
|
|
||||||
found,
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
} => {
|
|
||||||
if min == max {
|
|
||||||
let expected = min;
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` got {} {} but {}takes {}",
|
|
||||||
recipe,
|
|
||||||
found,
|
|
||||||
Count("argument", *found),
|
|
||||||
if expected < found { "only " } else { "" },
|
|
||||||
expected
|
|
||||||
)?;
|
|
||||||
} else if found < min {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` got {} {} but takes at least {}",
|
|
||||||
recipe,
|
|
||||||
found,
|
|
||||||
Count("argument", *found),
|
|
||||||
min
|
|
||||||
)?;
|
|
||||||
} else if found > max {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` got {} {} but takes at most {}",
|
|
||||||
recipe,
|
|
||||||
found,
|
|
||||||
Count("argument", *found),
|
|
||||||
max
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
write!(f, "\nusage:\n just {}", recipe)?;
|
|
||||||
for param in parameters {
|
|
||||||
if color.stderr().active() {
|
|
||||||
write!(f, " {:#}", param)?;
|
|
||||||
} else {
|
|
||||||
write!(f, " {}", param)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Code {
|
|
||||||
recipe,
|
|
||||||
line_number,
|
|
||||||
code,
|
|
||||||
} =>
|
|
||||||
if let Some(n) = line_number {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` failed on line {} with exit code {}",
|
|
||||||
recipe, n, code
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
|
|
||||||
},
|
|
||||||
CommandInvocation {
|
|
||||||
binary,
|
|
||||||
arguments,
|
|
||||||
io_error,
|
|
||||||
} => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Failed to invoke {}: {}",
|
|
||||||
iter::once(binary)
|
|
||||||
.chain(arguments)
|
|
||||||
.map(|value| Enclosure::tick(value.to_string_lossy()).to_string())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(" "),
|
|
||||||
io_error,
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
Cygpath {
|
|
||||||
recipe,
|
|
||||||
output_error,
|
|
||||||
} => match output_error {
|
|
||||||
OutputError::Code(code) => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Cygpath failed with exit code {} while translating recipe `{}` shebang interpreter \
|
|
||||||
path",
|
|
||||||
code, recipe
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
OutputError::Signal(signal) => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Cygpath terminated by signal {} while translating recipe `{}` shebang interpreter \
|
|
||||||
path",
|
|
||||||
signal, recipe
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
OutputError::Unknown => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Cygpath experienced an unknown failure while translating recipe `{}` shebang \
|
|
||||||
interpreter path",
|
|
||||||
recipe
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
OutputError::Io(io_error) => {
|
|
||||||
match io_error.kind() {
|
|
||||||
io::ErrorKind::NotFound => write!(
|
|
||||||
f,
|
|
||||||
"Could not find `cygpath` executable to translate recipe `{}` shebang interpreter \
|
|
||||||
path:\n{}",
|
|
||||||
recipe, io_error
|
|
||||||
),
|
|
||||||
io::ErrorKind::PermissionDenied => write!(
|
|
||||||
f,
|
|
||||||
"Could not run `cygpath` executable to translate recipe `{}` shebang interpreter \
|
|
||||||
path:\n{}",
|
|
||||||
recipe, io_error
|
|
||||||
),
|
|
||||||
_ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
|
|
||||||
}?;
|
|
||||||
},
|
|
||||||
OutputError::Utf8(utf8_error) => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Cygpath successfully translated recipe `{}` shebang interpreter path, but output was \
|
|
||||||
not utf8: {}",
|
|
||||||
recipe, utf8_error
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Dotenv { dotenv_error } => {
|
|
||||||
writeln!(f, "Failed to load .env: {}", dotenv_error)?;
|
|
||||||
},
|
|
||||||
FunctionCall { function, message } => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Call to function `{}` failed: {}",
|
|
||||||
function.lexeme(),
|
|
||||||
message
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
Shebang {
|
|
||||||
recipe,
|
|
||||||
command,
|
|
||||||
argument,
|
|
||||||
io_error,
|
|
||||||
} =>
|
|
||||||
if let Some(argument) = argument {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` with shebang `#!{} {}` execution error: {}",
|
|
||||||
recipe, command, argument, io_error
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` with shebang `#!{}` execution error: {}",
|
|
||||||
recipe, command, io_error
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
Signal {
|
|
||||||
recipe,
|
|
||||||
line_number,
|
|
||||||
signal,
|
|
||||||
} =>
|
|
||||||
if let Some(n) = line_number {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` was terminated on line {} by signal {}",
|
|
||||||
recipe, n, signal
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
|
|
||||||
},
|
|
||||||
Unknown {
|
|
||||||
recipe,
|
|
||||||
line_number,
|
|
||||||
} =>
|
|
||||||
if let Some(n) = line_number {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` failed on line {} for an unknown reason",
|
|
||||||
recipe, n
|
|
||||||
)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "Recipe `{}` failed for an unknown reason", recipe)?;
|
|
||||||
},
|
|
||||||
IoError { recipe, io_error } => {
|
|
||||||
match io_error.kind() {
|
|
||||||
io::ErrorKind::NotFound => writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` could not be run because just could not find `sh`:{}",
|
|
||||||
recipe, io_error
|
|
||||||
),
|
|
||||||
io::ErrorKind::PermissionDenied => writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` could not be run because just could not run `sh`:{}",
|
|
||||||
recipe, io_error
|
|
||||||
),
|
|
||||||
_ => writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` could not be run because of an IO error while launching `sh`:{}",
|
|
||||||
recipe, io_error
|
|
||||||
),
|
|
||||||
}?;
|
|
||||||
},
|
|
||||||
TmpdirIoError { recipe, io_error } => writeln!(
|
|
||||||
f,
|
|
||||||
"Recipe `{}` could not be run because of an IO error while trying to create a temporary \
|
|
||||||
directory or write a file to that directory`:{}",
|
|
||||||
recipe, io_error
|
|
||||||
)?,
|
|
||||||
Backtick { output_error, .. } => match output_error {
|
|
||||||
OutputError::Code(code) => {
|
|
||||||
writeln!(f, "Backtick failed with exit code {}", code)?;
|
|
||||||
},
|
|
||||||
OutputError::Signal(signal) => {
|
|
||||||
writeln!(f, "Backtick was terminated by signal {}", signal)?;
|
|
||||||
},
|
|
||||||
OutputError::Unknown => {
|
|
||||||
writeln!(f, "Backtick failed for an unknown reason")?;
|
|
||||||
},
|
|
||||||
OutputError::Io(io_error) => {
|
|
||||||
match io_error.kind() {
|
|
||||||
io::ErrorKind::NotFound => write!(
|
|
||||||
f,
|
|
||||||
"Backtick could not be run because just could not find `sh`:\n{}",
|
|
||||||
io_error
|
|
||||||
),
|
|
||||||
io::ErrorKind::PermissionDenied => write!(
|
|
||||||
f,
|
|
||||||
"Backtick could not be run because just could not run `sh`:\n{}",
|
|
||||||
io_error
|
|
||||||
),
|
|
||||||
_ => write!(
|
|
||||||
f,
|
|
||||||
"Backtick could not be run because of an IO error while launching `sh`:\n{}",
|
|
||||||
io_error
|
|
||||||
),
|
|
||||||
}?;
|
|
||||||
},
|
|
||||||
OutputError::Utf8(utf8_error) => {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"Backtick succeeded but stdout was not utf8: {}",
|
|
||||||
utf8_error
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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 { message } => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Internal runtime error, this may indicate a bug in just: {} \
|
|
||||||
consider filing an issue: https://github.com/casey/just/issues/new",
|
|
||||||
message
|
|
||||||
)?;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(f, "{}", message.suffix())?;
|
|
||||||
|
|
||||||
if let Some(token) = self.context() {
|
|
||||||
token.write_context(f, Color::fmt(f).error())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'src> From<dotenv::Error> for RuntimeError<'src> {
|
|
||||||
fn from(dotenv_error: dotenv::Error) -> RuntimeError<'src> {
|
|
||||||
RuntimeError::Dotenv { dotenv_error }
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,17 @@ use crate::common::*;
|
|||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub(crate)))]
|
#[snafu(visibility(pub(crate)))]
|
||||||
pub(crate) enum SearchError {
|
pub(crate) enum SearchError {
|
||||||
|
#[snafu(display(
|
||||||
|
"I/O error reading directory `{}`: {}",
|
||||||
|
directory.display(),
|
||||||
|
io_error
|
||||||
|
))]
|
||||||
|
Io {
|
||||||
|
directory: PathBuf,
|
||||||
|
io_error: io::Error,
|
||||||
|
},
|
||||||
|
#[snafu(display("Justfile path had no parent: {}", path.display()))]
|
||||||
|
JustfileHadNoParent { path: PathBuf },
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"Multiple candidate justfiles found in `{}`: {}",
|
"Multiple candidate justfiles found in `{}`: {}",
|
||||||
candidates[0].parent().unwrap().display(),
|
candidates[0].parent().unwrap().display(),
|
||||||
@ -13,23 +24,10 @@ pub(crate) enum SearchError {
|
|||||||
),
|
),
|
||||||
))]
|
))]
|
||||||
MultipleCandidates { candidates: Vec<PathBuf> },
|
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"))]
|
#[snafu(display("No justfile found"))]
|
||||||
NotFound,
|
NotFound,
|
||||||
#[snafu(display("Justfile path had no parent: {}", path.display()))]
|
|
||||||
JustfileHadNoParent { path: PathBuf },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for SearchError {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -55,11 +55,11 @@ impl StringKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unterminated_error_kind(self) -> CompilationErrorKind<'static> {
|
pub(crate) fn unterminated_error_kind(self) -> CompileErrorKind<'static> {
|
||||||
match self.delimiter {
|
match self.delimiter {
|
||||||
StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle =>
|
StringDelimiter::QuoteDouble | StringDelimiter::QuoteSingle =>
|
||||||
CompilationErrorKind::UnterminatedString,
|
CompileErrorKind::UnterminatedString,
|
||||||
StringDelimiter::Backtick => CompilationErrorKind::UnterminatedBacktick,
|
StringDelimiter::Backtick => CompileErrorKind::UnterminatedBacktick,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,9 +74,9 @@ impl StringKind {
|
|||||||
self.indented
|
self.indented
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn from_string_or_backtick(token: Token) -> CompilationResult<Self> {
|
pub(crate) fn from_string_or_backtick(token: Token) -> CompileResult<Self> {
|
||||||
Self::from_token_start(token.lexeme()).ok_or_else(|| {
|
Self::from_token_start(token.lexeme()).ok_or_else(|| {
|
||||||
token.error(CompilationErrorKind::Internal {
|
token.error(CompileErrorKind::Internal {
|
||||||
message: "StringKind::from_token: Expected String or Backtick".to_owned(),
|
message: "StringKind::from_token: Expected String or Backtick".to_owned(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -210,26 +210,18 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
|
|||||||
)];
|
)];
|
||||||
|
|
||||||
impl Subcommand {
|
impl Subcommand {
|
||||||
pub(crate) fn completions(verbosity: Verbosity, shell: &str) -> Result<(), i32> {
|
pub(crate) fn completions(shell: &str) -> RunResult<'static, ()> {
|
||||||
use clap::Shell;
|
use clap::Shell;
|
||||||
|
|
||||||
fn replace(
|
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
|
||||||
verbosity: Verbosity,
|
|
||||||
haystack: &mut String,
|
|
||||||
needle: &str,
|
|
||||||
replacement: &str,
|
|
||||||
) -> Result<(), i32> {
|
|
||||||
if let Some(index) = haystack.find(needle) {
|
if let Some(index) = haystack.find(needle) {
|
||||||
haystack.replace_range(index..index + needle.len(), replacement);
|
haystack.replace_range(index..index + needle.len(), replacement);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
if verbosity.loud() {
|
Err(Error::internal(format!(
|
||||||
eprintln!("Failed to find text:");
|
"Failed to find text:\n{}\n…in completion script:\n{}",
|
||||||
eprintln!("{}", needle);
|
needle, haystack
|
||||||
eprintln!("…in completion script:");
|
)))
|
||||||
eprintln!("{}", haystack);
|
|
||||||
}
|
|
||||||
Err(EXIT_FAILURE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,19 +238,19 @@ impl Subcommand {
|
|||||||
match shell {
|
match shell {
|
||||||
Shell::Bash =>
|
Shell::Bash =>
|
||||||
for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS {
|
||||||
replace(verbosity, &mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
Shell::Fish => {
|
Shell::Fish => {
|
||||||
script.insert_str(0, FISH_RECIPE_COMPLETIONS);
|
script.insert_str(0, FISH_RECIPE_COMPLETIONS);
|
||||||
},
|
},
|
||||||
Shell::PowerShell =>
|
Shell::PowerShell =>
|
||||||
for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS {
|
||||||
replace(verbosity, &mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
|
|
||||||
Shell::Zsh =>
|
Shell::Zsh =>
|
||||||
for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS {
|
for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS {
|
||||||
replace(verbosity, &mut script, needle, replacement)?;
|
replace(&mut script, needle, replacement)?;
|
||||||
},
|
},
|
||||||
Shell::Elvish => {},
|
Shell::Elvish => {},
|
||||||
}
|
}
|
||||||
|
@ -61,7 +61,7 @@ pub(crate) fn analysis_error(
|
|||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
length: usize,
|
length: usize,
|
||||||
kind: CompilationErrorKind,
|
kind: CompileErrorKind,
|
||||||
) {
|
) {
|
||||||
let tokens = Lexer::lex(src).expect("Lexing failed in parse test...");
|
let tokens = Lexer::lex(src).expect("Lexing failed in parse test...");
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ pub(crate) fn analysis_error(
|
|||||||
match Analyzer::analyze(ast) {
|
match Analyzer::analyze(ast) {
|
||||||
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
Ok(_) => panic!("Analysis unexpectedly succeeded"),
|
||||||
Err(have) => {
|
Err(have) => {
|
||||||
let want = CompilationError {
|
let want = CompileError {
|
||||||
token: Token {
|
token: Token {
|
||||||
kind: have.token.kind,
|
kind: have.token.kind,
|
||||||
src,
|
src,
|
||||||
|
10
src/thunk.rs
10
src/thunk.rs
@ -32,7 +32,7 @@ impl<'src> Thunk<'src> {
|
|||||||
pub(crate) fn resolve(
|
pub(crate) fn resolve(
|
||||||
name: Name<'src>,
|
name: Name<'src>,
|
||||||
mut arguments: Vec<Expression<'src>>,
|
mut arguments: Vec<Expression<'src>>,
|
||||||
) -> CompilationResult<'src, Thunk<'src>> {
|
) -> CompileResult<'src, Thunk<'src>> {
|
||||||
if let Some(function) = crate::function::TABLE.get(&name.lexeme()) {
|
if let Some(function) = crate::function::TABLE.get(&name.lexeme()) {
|
||||||
match (function, arguments.len()) {
|
match (function, arguments.len()) {
|
||||||
(Function::Nullary(function), 0) => Ok(Thunk::Nullary {
|
(Function::Nullary(function), 0) => Ok(Thunk::Nullary {
|
||||||
@ -63,16 +63,14 @@ impl<'src> Thunk<'src> {
|
|||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
_ => Err(
|
_ => Err(name.error(CompileErrorKind::FunctionArgumentCountMismatch {
|
||||||
name.error(CompilationErrorKind::FunctionArgumentCountMismatch {
|
|
||||||
function: name.lexeme(),
|
function: name.lexeme(),
|
||||||
found: arguments.len(),
|
found: arguments.len(),
|
||||||
expected: function.argc(),
|
expected: function.argc(),
|
||||||
}),
|
})),
|
||||||
),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(name.error(CompilationErrorKind::UnknownFunction {
|
Err(name.error(CompileErrorKind::UnknownFunction {
|
||||||
function: name.lexeme(),
|
function: name.lexeme(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
17
src/token.rs
17
src/token.rs
@ -15,11 +15,11 @@ impl<'src> Token<'src> {
|
|||||||
&self.src[self.offset..self.offset + self.length]
|
&self.src[self.offset..self.offset + self.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn error(&self, kind: CompilationErrorKind<'src>) -> CompilationError<'src> {
|
pub(crate) fn error(&self, kind: CompileErrorKind<'src>) -> CompileError<'src> {
|
||||||
CompilationError { token: *self, kind }
|
CompileError { token: *self, kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn write_context(&self, f: &mut Formatter, color: Color) -> fmt::Result {
|
pub(crate) fn write_context(&self, w: &mut dyn Write, color: Color) -> io::Result<()> {
|
||||||
let width = if self.length == 0 { 1 } else { self.length };
|
let width = if self.length == 0 { 1 } else { self.length };
|
||||||
|
|
||||||
let line_number = self.line.ordinal();
|
let line_number = self.line.ordinal();
|
||||||
@ -50,11 +50,11 @@ impl<'src> Token<'src> {
|
|||||||
i += c.len_utf8();
|
i += c.len_utf8();
|
||||||
}
|
}
|
||||||
let line_number_width = line_number.to_string().len();
|
let line_number_width = line_number.to_string().len();
|
||||||
writeln!(f, "{0:1$} |", "", line_number_width)?;
|
writeln!(w, "{0:1$} |", "", line_number_width)?;
|
||||||
writeln!(f, "{} | {}", line_number, space_line)?;
|
writeln!(w, "{} | {}", line_number, space_line)?;
|
||||||
write!(f, "{0:1$} |", "", line_number_width)?;
|
write!(w, "{0:1$} |", "", line_number_width)?;
|
||||||
write!(
|
write!(
|
||||||
f,
|
w,
|
||||||
" {0:1$}{2}{3:^<4$}{5}",
|
" {0:1$}{2}{3:^<4$}{5}",
|
||||||
"",
|
"",
|
||||||
space_column,
|
space_column,
|
||||||
@ -67,12 +67,13 @@ impl<'src> Token<'src> {
|
|||||||
None =>
|
None =>
|
||||||
if self.offset != self.src.len() {
|
if self.offset != self.src.len() {
|
||||||
write!(
|
write!(
|
||||||
f,
|
w,
|
||||||
"internal error: Error has invalid line number: {}",
|
"internal error: Error has invalid line number: {}",
|
||||||
line_number
|
line_number
|
||||||
)?;
|
)?;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ impl<'src> UnresolvedRecipe<'src> {
|
|||||||
pub(crate) fn resolve(
|
pub(crate) fn resolve(
|
||||||
self,
|
self,
|
||||||
resolved: Vec<Rc<Recipe<'src>>>,
|
resolved: Vec<Rc<Recipe<'src>>>,
|
||||||
) -> CompilationResult<'src, Recipe<'src>> {
|
) -> CompileResult<'src, Recipe<'src>> {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
self.dependencies.len(),
|
self.dependencies.len(),
|
||||||
resolved.len(),
|
resolved.len(),
|
||||||
@ -21,14 +21,16 @@ impl<'src> UnresolvedRecipe<'src> {
|
|||||||
.argument_range()
|
.argument_range()
|
||||||
.contains(&unresolved.arguments.len())
|
.contains(&unresolved.arguments.len())
|
||||||
{
|
{
|
||||||
return Err(unresolved.recipe.error(
|
return Err(
|
||||||
CompilationErrorKind::DependencyArgumentCountMismatch {
|
unresolved
|
||||||
|
.recipe
|
||||||
|
.error(CompileErrorKind::DependencyArgumentCountMismatch {
|
||||||
dependency: unresolved.recipe.lexeme(),
|
dependency: unresolved.recipe.lexeme(),
|
||||||
found: unresolved.arguments.len(),
|
found: unresolved.arguments.len(),
|
||||||
min: resolved.min_arguments(),
|
min: resolved.min_arguments(),
|
||||||
max: resolved.max_arguments(),
|
max: resolved.max_arguments(),
|
||||||
},
|
}),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,19 +13,17 @@ impl Warning {
|
|||||||
Self::DotenvLoad => None,
|
Self::DotenvLoad => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Warning {
|
pub(crate) fn write(&self, w: &mut dyn Write, color: Color) -> io::Result<()> {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
let warning = color.warning();
|
||||||
let warning = Color::fmt(f).warning();
|
let message = color.message();
|
||||||
let message = Color::fmt(f).message();
|
|
||||||
|
|
||||||
write!(f, "{} {}", warning.paint("warning:"), message.prefix())?;
|
write!(w, "{} {}", warning.paint("warning:"), message.prefix())?;
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::DotenvLoad => {
|
Self::DotenvLoad => {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
write!(f, "\
|
write!(w, "\
|
||||||
A `.env` file was found and loaded, but this behavior will change in the future.
|
A `.env` file was found and loaded, but this behavior will change in the future.
|
||||||
To silence this warning and continue loading `.env` files, add:
|
To silence this warning and continue loading `.env` files, add:
|
||||||
|
|
||||||
@ -39,11 +37,10 @@ See https://github.com/casey/just/issues/469 for more details.")?;
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "{}", message.suffix())?;
|
writeln!(w, "{}", message.suffix())?;
|
||||||
|
|
||||||
if let Some(token) = self.context() {
|
if let Some(token) = self.context() {
|
||||||
writeln!(f)?;
|
token.write_context(w, warning)?;
|
||||||
token.write_context(f, Color::fmt(f).warning())?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -95,7 +95,7 @@ test! {
|
|||||||
",
|
",
|
||||||
args: ("--choose"),
|
args: ("--choose"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "Justfile contains no choosable recipes.\n",
|
stderr: "error: Justfile contains no choosable recipes.\n",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +113,61 @@ test! {
|
|||||||
stderr: "echo foo\necho bar\n",
|
stderr: "echo foo\necho bar\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invoke_error_function() {
|
||||||
|
Test::new()
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
foo:
|
||||||
|
echo foo
|
||||||
|
|
||||||
|
bar:
|
||||||
|
echo bar
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.stderr(if cfg!(windows) {
|
||||||
|
"error: Chooser `/ -cu fzf` invocation failed: Access is denied. (os error 5)\n"
|
||||||
|
} else {
|
||||||
|
"error: Chooser `/ -cu fzf` invocation failed: Permission denied (os error 13)\n"
|
||||||
|
})
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.shell(false)
|
||||||
|
.args(&["--shell", "/", "--choose"])
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn status_error() {
|
||||||
|
let tmp = temptree! {
|
||||||
|
justfile: "foo:\n echo foo\nbar:\n echo bar\n",
|
||||||
|
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd_unit!(%"chmod +x", tmp.path().join("exit-2"));
|
||||||
|
|
||||||
|
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("--choose")
|
||||||
|
.arg("--chooser")
|
||||||
|
.arg("exit-2")
|
||||||
|
.env("PATH", path)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
"error: Chooser `exit-2` failed: exit code: 2\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(output.status.code().unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default() {
|
fn default() {
|
||||||
let tmp = temptree! {
|
let tmp = temptree! {
|
||||||
|
@ -91,6 +91,7 @@ test! {
|
|||||||
echo XYZ
|
echo XYZ
|
||||||
",
|
",
|
||||||
args: ("--command", "false"),
|
args: ("--command", "false"),
|
||||||
|
stderr: "error: Command `false` failed: exit code: 1\n",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,21 +2,26 @@ pub(crate) use std::{
|
|||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
env::{self, consts::EXE_SUFFIX},
|
env::{self, consts::EXE_SUFFIX},
|
||||||
error::Error,
|
error::Error,
|
||||||
|
fmt::Debug,
|
||||||
fs,
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
iter,
|
iter,
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
process::{Command, Output, Stdio},
|
process::{Command, Output, Stdio},
|
||||||
str,
|
str,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) use cradle::cmd_unit;
|
||||||
pub(crate) use executable_path::executable_path;
|
pub(crate) use executable_path::executable_path;
|
||||||
pub(crate) use just::unindent;
|
pub(crate) use just::unindent;
|
||||||
pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
pub(crate) use libc::{EXIT_FAILURE, EXIT_SUCCESS};
|
||||||
|
pub(crate) use pretty_assertions::Comparison;
|
||||||
|
pub(crate) use regex::Regex;
|
||||||
|
pub(crate) use tempfile::TempDir;
|
||||||
pub(crate) use temptree::temptree;
|
pub(crate) use temptree::temptree;
|
||||||
pub(crate) use which::which;
|
pub(crate) use which::which;
|
||||||
pub(crate) use yaml_rust::YamlLoader;
|
pub(crate) use yaml_rust::YamlLoader;
|
||||||
|
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir,
|
assert_stdout::assert_stdout, assert_success::assert_success, tempdir::tempdir, test::Test,
|
||||||
};
|
};
|
||||||
|
@ -26,6 +26,67 @@ fn invalid_justfile() {
|
|||||||
assert_stdout(&output, JUSTFILE);
|
assert_stdout(&output, JUSTFILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invoke_error() {
|
||||||
|
let tmp = temptree! {
|
||||||
|
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", "/")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
if cfg!(windows) {
|
||||||
|
"error: Editor `/` invocation failed: Access is denied. (os error 5)\n"
|
||||||
|
} else {
|
||||||
|
"error: Editor `/` invocation failed: Permission denied (os error 13)\n"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn status_error() {
|
||||||
|
let tmp = temptree! {
|
||||||
|
justfile: JUSTFILE,
|
||||||
|
"exit-2": "#!/usr/bin/env bash\nexit 2\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd_unit!(%"chmod +x", tmp.path().join("exit-2"));
|
||||||
|
|
||||||
|
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("VISUAL", "exit-2")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&output.stderr),
|
||||||
|
"error: Editor `exit-2` failed: exit code: 2\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(output.status.code().unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
/// Test that editor is $VISUAL, $EDITOR, or "vim" in that order
|
/// Test that editor is $VISUAL, $EDITOR, or "vim" in that order
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_precedence() {
|
fn editor_precedence() {
|
||||||
|
31
tests/fmt.rs
31
tests/fmt.rs
@ -5,7 +5,8 @@ test! {
|
|||||||
justfile: "",
|
justfile: "",
|
||||||
args: ("--fmt"),
|
args: ("--fmt"),
|
||||||
stderr: "
|
stderr: "
|
||||||
The `--fmt` command is currently unstable. Pass the `--unstable` flag to enable it.
|
error: The `--fmt` command is currently unstable. \
|
||||||
|
Invoke `just` with the `--unstable` flag to enable unstable features.
|
||||||
",
|
",
|
||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
@ -34,6 +35,34 @@ fn unstable_passed() {
|
|||||||
assert_eq!(fs::read_to_string(&justfile).unwrap(), "x := 'hello'\n",);
|
assert_eq!(fs::read_to_string(&justfile).unwrap(), "x := 'hello'\n",);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_error() {
|
||||||
|
let tempdir = temptree! {
|
||||||
|
justfile: "x := 'hello' ",
|
||||||
|
};
|
||||||
|
|
||||||
|
let test = Test::with_tempdir(tempdir)
|
||||||
|
.no_justfile()
|
||||||
|
.args(&["--fmt", "--unstable"])
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.stderr_regex(if cfg!(windows) {
|
||||||
|
r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n"
|
||||||
|
} else {
|
||||||
|
r"error: Failed to write justfile to `.*`: Permission denied \(os error 13\)\n"
|
||||||
|
});
|
||||||
|
|
||||||
|
let justfile_path = test.justfile_path();
|
||||||
|
|
||||||
|
cmd_unit!(%"chmod 400", &justfile_path);
|
||||||
|
|
||||||
|
let _tempdir = test.run();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(&justfile_path).unwrap(),
|
||||||
|
"x := 'hello' "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: alias_good,
|
name: alias_good,
|
||||||
justfile: "
|
justfile: "
|
||||||
|
@ -22,23 +22,38 @@ fn current_dir() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exists() {
|
fn exists() {
|
||||||
let tmp = tempdir();
|
let tempdir = Test::new()
|
||||||
|
.no_justfile()
|
||||||
let output = Command::new(executable_path("just"))
|
|
||||||
.current_dir(tmp.path())
|
|
||||||
.arg("--init")
|
.arg("--init")
|
||||||
.output()
|
.stderr_regex("Wrote justfile to `.*`\n")
|
||||||
.unwrap();
|
.run();
|
||||||
|
|
||||||
assert!(output.status.success());
|
Test::with_tempdir(tempdir)
|
||||||
|
.no_justfile()
|
||||||
let output = Command::new(executable_path("just"))
|
|
||||||
.current_dir(tmp.path())
|
|
||||||
.arg("--init")
|
.arg("--init")
|
||||||
.output()
|
.status(EXIT_FAILURE)
|
||||||
.unwrap();
|
.stderr_regex("error: Justfile `.*` already exists\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
assert!(!output.status.success());
|
#[test]
|
||||||
|
fn write_error() {
|
||||||
|
let test = Test::new();
|
||||||
|
|
||||||
|
let justfile_path = test.justfile_path();
|
||||||
|
|
||||||
|
fs::create_dir(&justfile_path).unwrap();
|
||||||
|
|
||||||
|
test
|
||||||
|
.no_justfile()
|
||||||
|
.args(&["--init"])
|
||||||
|
.status(EXIT_FAILURE)
|
||||||
|
.stderr_regex(if cfg!(windows) {
|
||||||
|
r"error: Failed to write justfile to `.*`: Access is denied. \(os error 5\)\n"
|
||||||
|
} else {
|
||||||
|
r"error: Failed to write justfile to `.*`: Is a directory \(os error 21\)\n"
|
||||||
|
})
|
||||||
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -47,18 +62,17 @@ fn invocation_directory() {
|
|||||||
".git": {},
|
".git": {},
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = Command::new(executable_path("just"))
|
let test = Test::with_tempdir(tmp);
|
||||||
.current_dir(tmp.path())
|
|
||||||
|
let justfile_path = test.justfile_path();
|
||||||
|
|
||||||
|
let _tmp = test
|
||||||
|
.no_justfile()
|
||||||
|
.stderr_regex("Wrote justfile to `.*`\n")
|
||||||
.arg("--init")
|
.arg("--init")
|
||||||
.output()
|
.run();
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(output.status.success());
|
assert_eq!(fs::read_to_string(justfile_path).unwrap(), EXPECTED);
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
fs::read_to_string(tmp.path().join("justfile")).unwrap(),
|
|
||||||
EXPECTED
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -27,6 +27,7 @@ mod readme;
|
|||||||
mod search;
|
mod search;
|
||||||
mod shebang;
|
mod shebang;
|
||||||
mod shell;
|
mod shell;
|
||||||
|
mod show;
|
||||||
mod string;
|
mod string;
|
||||||
mod sublime_syntax;
|
mod sublime_syntax;
|
||||||
mod subsequents;
|
mod subsequents;
|
||||||
|
101
tests/misc.rs
101
tests/misc.rs
@ -123,30 +123,6 @@ test! {
|
|||||||
status: EXIT_FAILURE,
|
status: EXIT_FAILURE,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: alias_show,
|
|
||||||
justfile: "foo:\n bar\nalias f := foo",
|
|
||||||
args: ("--show", "f"),
|
|
||||||
stdout: "
|
|
||||||
alias f := foo
|
|
||||||
foo:
|
|
||||||
bar
|
|
||||||
",
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: alias_show_missing_target,
|
|
||||||
justfile: "alias f := foo",
|
|
||||||
args: ("--show", "f"),
|
|
||||||
stderr: "
|
|
||||||
error: Alias `f` has an unknown target `foo`
|
|
||||||
|
|
|
||||||
1 | alias f := foo
|
|
||||||
| ^
|
|
||||||
",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: default,
|
name: default,
|
||||||
justfile: "default:\n echo hello\nother: \n echo bar",
|
justfile: "default:\n echo hello\nother: \n echo bar",
|
||||||
@ -256,19 +232,6 @@ c:
|
|||||||
stderr: "echo d\necho c\n",
|
stderr: "echo d\necho c\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: show,
|
|
||||||
justfile: r#"hello := "foo"
|
|
||||||
bar := hello + hello
|
|
||||||
recipe:
|
|
||||||
echo {{hello + "bar" + bar}}"#,
|
|
||||||
args: ("--show", "recipe"),
|
|
||||||
stdout: r#"
|
|
||||||
recipe:
|
|
||||||
echo {{ hello + "bar" + bar }}
|
|
||||||
"#,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: status_passthrough,
|
name: status_passthrough,
|
||||||
justfile: "
|
justfile: "
|
||||||
@ -700,8 +663,8 @@ test! {
|
|||||||
justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
|
justfile: "b := a\na := `exit 100`\nbar:\n echo '{{`exit 200`}}'",
|
||||||
args: ("--color", "always"),
|
args: ("--color", "always"),
|
||||||
stdout: "",
|
stdout: "",
|
||||||
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100
|
stderr: "\u{1b}[1;31merror\u{1b}[0m: \u{1b}[1mBacktick failed with exit code 100\u{1b}[0m
|
||||||
\u{1b}[0m |\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
|
|\n2 | a := `exit 100`\n | \u{1b}[1;31m^^^^^^^^^^\u{1b}[0m\n",
|
||||||
status: 100,
|
status: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1021,66 +984,6 @@ b:
|
|||||||
"#,
|
"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
test! {
|
|
||||||
name: show_suggestion,
|
|
||||||
justfile: r#"
|
|
||||||
hello a b='B ' c='C':
|
|
||||||
echo {{a}} {{b}} {{c}}
|
|
||||||
|
|
||||||
a Z="\t z":
|
|
||||||
"#,
|
|
||||||
args: ("--show", "hell"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: show_alias_suggestion,
|
|
||||||
justfile: r#"
|
|
||||||
hello a b='B ' c='C':
|
|
||||||
echo {{a}} {{b}} {{c}}
|
|
||||||
|
|
||||||
alias foo := hello
|
|
||||||
|
|
||||||
a Z="\t z":
|
|
||||||
"#,
|
|
||||||
args: ("--show", "fo"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "Justfile does not contain recipe `fo`.\nDid you mean `foo`, an alias for `hello`?\n",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: show_no_suggestion,
|
|
||||||
justfile: r#"
|
|
||||||
helloooooo a b='B ' c='C':
|
|
||||||
echo {{a}} {{b}} {{c}}
|
|
||||||
|
|
||||||
a Z="\t z":
|
|
||||||
"#,
|
|
||||||
args: ("--show", "hell"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "Justfile does not contain recipe `hell`.\n",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
|
||||||
name: show_no_alias_suggestion,
|
|
||||||
justfile: r#"
|
|
||||||
hello a b='B ' c='C':
|
|
||||||
echo {{a}} {{b}} {{c}}
|
|
||||||
|
|
||||||
alias foo := hello
|
|
||||||
|
|
||||||
a Z="\t z":
|
|
||||||
"#,
|
|
||||||
args: ("--show", "fooooooo"),
|
|
||||||
stdout: "",
|
|
||||||
stderr: "Justfile does not contain recipe `fooooooo`.\n",
|
|
||||||
status: EXIT_FAILURE,
|
|
||||||
}
|
|
||||||
|
|
||||||
test! {
|
test! {
|
||||||
name: run_suggestion,
|
name: run_suggestion,
|
||||||
justfile: r#"
|
justfile: r#"
|
||||||
|
101
tests/show.rs
Normal file
101
tests/show.rs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: show,
|
||||||
|
justfile: r#"hello := "foo"
|
||||||
|
bar := hello + hello
|
||||||
|
recipe:
|
||||||
|
echo {{hello + "bar" + bar}}"#,
|
||||||
|
args: ("--show", "recipe"),
|
||||||
|
stdout: r#"
|
||||||
|
recipe:
|
||||||
|
echo {{ hello + "bar" + bar }}
|
||||||
|
"#,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: alias_show,
|
||||||
|
justfile: "foo:\n bar\nalias f := foo",
|
||||||
|
args: ("--show", "f"),
|
||||||
|
stdout: "
|
||||||
|
alias f := foo
|
||||||
|
foo:
|
||||||
|
bar
|
||||||
|
",
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: alias_show_missing_target,
|
||||||
|
justfile: "alias f := foo",
|
||||||
|
args: ("--show", "f"),
|
||||||
|
stderr: "
|
||||||
|
error: Alias `f` has an unknown target `foo`
|
||||||
|
|
|
||||||
|
1 | alias f := foo
|
||||||
|
| ^
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: show_suggestion,
|
||||||
|
justfile: r#"
|
||||||
|
hello a b='B ' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
|
||||||
|
a Z="\t z":
|
||||||
|
"#,
|
||||||
|
args: ("--show", "hell"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Justfile does not contain recipe `hell`.\nDid you mean `hello`?\n",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: show_alias_suggestion,
|
||||||
|
justfile: r#"
|
||||||
|
hello a b='B ' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
|
||||||
|
alias foo := hello
|
||||||
|
|
||||||
|
a Z="\t z":
|
||||||
|
"#,
|
||||||
|
args: ("--show", "fo"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "
|
||||||
|
error: Justfile does not contain recipe `fo`.
|
||||||
|
Did you mean `foo`, an alias for `hello`?
|
||||||
|
",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: show_no_suggestion,
|
||||||
|
justfile: r#"
|
||||||
|
helloooooo a b='B ' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
|
||||||
|
a Z="\t z":
|
||||||
|
"#,
|
||||||
|
args: ("--show", "hell"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Justfile does not contain recipe `hell`.\n",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
||||||
|
|
||||||
|
test! {
|
||||||
|
name: show_no_alias_suggestion,
|
||||||
|
justfile: r#"
|
||||||
|
hello a b='B ' c='C':
|
||||||
|
echo {{a}} {{b}} {{c}}
|
||||||
|
|
||||||
|
alias foo := hello
|
||||||
|
|
||||||
|
a Z="\t z":
|
||||||
|
"#,
|
||||||
|
args: ("--show", "fooooooo"),
|
||||||
|
stdout: "",
|
||||||
|
stderr: "error: Justfile does not contain recipe `fooooooo`.\n",
|
||||||
|
status: EXIT_FAILURE,
|
||||||
|
}
|
192
tests/test.rs
192
tests/test.rs
@ -1,13 +1,13 @@
|
|||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
macro_rules! test {
|
macro_rules! test {
|
||||||
(
|
(
|
||||||
name: $name:ident,
|
name: $name:ident,
|
||||||
justfile: $justfile:expr,
|
$(justfile: $justfile:expr,)?
|
||||||
$(args: ($($arg:tt)*),)?
|
$(args: ($($arg:tt),*),)?
|
||||||
$(env: {
|
$(env: { $($env_key:literal : $env_value:literal,)* },)?
|
||||||
$($env_key:literal : $env_value:literal,)*
|
|
||||||
},)?
|
|
||||||
$(stdin: $stdin:expr,)?
|
$(stdin: $stdin:expr,)?
|
||||||
$(stdout: $stdout:expr,)?
|
$(stdout: $stdout:expr,)?
|
||||||
$(stderr: $stderr:expr,)?
|
$(stderr: $stderr:expr,)?
|
||||||
@ -16,66 +16,128 @@ macro_rules! test {
|
|||||||
) => {
|
) => {
|
||||||
#[test]
|
#[test]
|
||||||
fn $name() {
|
fn $name() {
|
||||||
#[allow(unused_mut)]
|
let test = crate::test::Test::new();
|
||||||
let mut env = std::collections::BTreeMap::new();
|
|
||||||
|
|
||||||
$($(env.insert($env_key.to_string(), $env_value.to_string());)*)?
|
$($(let test = test.arg($arg);)*)?
|
||||||
|
$($(let test = test.env($env_key, $env_value);)*)?
|
||||||
|
$(let test = test.justfile($justfile);)?
|
||||||
|
$(let test = test.shell($shell);)?
|
||||||
|
$(let test = test.status($status);)?
|
||||||
|
$(let test = test.stderr($stderr);)?
|
||||||
|
$(let test = test.stdin($stdin);)?
|
||||||
|
$(let test = test.stdout($stdout);)?
|
||||||
|
|
||||||
crate::test::Test {
|
test.run();
|
||||||
justfile: $justfile,
|
|
||||||
$(args: &[$($arg)*],)?
|
|
||||||
$(stdin: $stdin,)?
|
|
||||||
$(stdout: $stdout,)?
|
|
||||||
$(stderr: $stderr,)?
|
|
||||||
$(status: $status,)?
|
|
||||||
$(shell: $shell,)?
|
|
||||||
env,
|
|
||||||
..crate::test::Test::default()
|
|
||||||
}.run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Test<'a> {
|
pub(crate) struct Test {
|
||||||
pub(crate) justfile: &'a str,
|
pub(crate) tempdir: TempDir,
|
||||||
pub(crate) args: &'a [&'a str],
|
pub(crate) justfile: Option<String>,
|
||||||
|
pub(crate) args: Vec<String>,
|
||||||
pub(crate) env: BTreeMap<String, String>,
|
pub(crate) env: BTreeMap<String, String>,
|
||||||
pub(crate) stdin: &'a str,
|
pub(crate) stdin: String,
|
||||||
pub(crate) stdout: &'a str,
|
pub(crate) stdout: String,
|
||||||
pub(crate) stderr: &'a str,
|
pub(crate) stderr: String,
|
||||||
|
pub(crate) stderr_regex: Option<Regex>,
|
||||||
pub(crate) status: i32,
|
pub(crate) status: i32,
|
||||||
pub(crate) shell: bool,
|
pub(crate) shell: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Default for Test<'a> {
|
impl Test {
|
||||||
fn default() -> Test<'a> {
|
pub(crate) fn new() -> Self {
|
||||||
Test {
|
Self::with_tempdir(tempdir())
|
||||||
justfile: "",
|
}
|
||||||
args: &[],
|
|
||||||
|
pub(crate) fn with_tempdir(tempdir: TempDir) -> Self {
|
||||||
|
Self {
|
||||||
|
args: Vec::new(),
|
||||||
env: BTreeMap::new(),
|
env: BTreeMap::new(),
|
||||||
stdin: "",
|
justfile: Some(String::new()),
|
||||||
stdout: "",
|
stderr_regex: None,
|
||||||
stderr: "",
|
|
||||||
status: EXIT_SUCCESS,
|
|
||||||
shell: true,
|
shell: true,
|
||||||
|
status: EXIT_SUCCESS,
|
||||||
|
stderr: String::new(),
|
||||||
|
stdin: String::new(),
|
||||||
|
stdout: String::new(),
|
||||||
|
tempdir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn arg(mut self, val: &str) -> Self {
|
||||||
|
self.args.push(val.to_owned());
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Test<'a> {
|
pub(crate) fn args(mut self, args: &[&str]) -> Self {
|
||||||
pub(crate) fn run(self) {
|
for arg in args {
|
||||||
let tmp = tempdir();
|
self = self.arg(arg);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
let justfile = unindent(self.justfile);
|
pub(crate) fn env(mut self, key: &str, val: &str) -> Self {
|
||||||
|
self.env.insert(key.to_string(), val.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
let stdout = unindent(self.stdout);
|
pub(crate) fn justfile(mut self, justfile: impl Into<String>) -> Self {
|
||||||
let stderr = unindent(self.stderr);
|
self.justfile = Some(justfile.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
let mut justfile_path = tmp.path().to_path_buf();
|
pub(crate) fn justfile_path(&self) -> PathBuf {
|
||||||
justfile_path.push("justfile");
|
self.tempdir.path().join("justfile")
|
||||||
fs::write(&justfile_path, justfile).unwrap();
|
}
|
||||||
|
|
||||||
let mut dotenv_path = tmp.path().to_path_buf();
|
pub(crate) fn no_justfile(mut self) -> Self {
|
||||||
|
self.justfile = None;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shell(mut self, shell: bool) -> Self {
|
||||||
|
self.shell = shell;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn status(mut self, exit_status: i32) -> Self {
|
||||||
|
self.status = exit_status;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stderr(mut self, stderr: impl Into<String>) -> Self {
|
||||||
|
self.stderr = stderr.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stderr_regex(mut self, stderr_regex: impl AsRef<str>) -> Self {
|
||||||
|
self.stderr_regex = Some(Regex::new(&format!("^{}$", stderr_regex.as_ref())).unwrap());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stdin(mut self, stdin: impl Into<String>) -> Self {
|
||||||
|
self.stdin = stdin.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stdout(mut self, stdout: impl Into<String>) -> Self {
|
||||||
|
self.stdout = stdout.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Test {
|
||||||
|
pub(crate) fn run(self) -> TempDir {
|
||||||
|
if let Some(justfile) = &self.justfile {
|
||||||
|
let justfile = unindent(justfile);
|
||||||
|
fs::write(self.justfile_path(), justfile).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = unindent(&self.stdout);
|
||||||
|
let stderr = unindent(&self.stderr);
|
||||||
|
|
||||||
|
let mut dotenv_path = self.tempdir.path().to_path_buf();
|
||||||
dotenv_path.push(".env");
|
dotenv_path.push(".env");
|
||||||
fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();
|
fs::write(dotenv_path, "DOTENV_KEY=dotenv-value").unwrap();
|
||||||
|
|
||||||
@ -87,8 +149,8 @@ impl<'a> Test<'a> {
|
|||||||
|
|
||||||
let mut child = command
|
let mut child = command
|
||||||
.args(self.args)
|
.args(self.args)
|
||||||
.envs(self.env)
|
.envs(&self.env)
|
||||||
.current_dir(tmp.path())
|
.current_dir(self.tempdir.path())
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
@ -107,23 +169,37 @@ impl<'a> Test<'a> {
|
|||||||
.wait_with_output()
|
.wait_with_output()
|
||||||
.expect("failed to wait for just process");
|
.expect("failed to wait for just process");
|
||||||
|
|
||||||
let have = Output {
|
fn compare<T: PartialEq + Debug>(name: &str, have: T, want: T) -> bool {
|
||||||
status: output.status.code().unwrap(),
|
let equal = have == want;
|
||||||
stdout: str::from_utf8(&output.stdout).unwrap(),
|
if !equal {
|
||||||
stderr: str::from_utf8(&output.stderr).unwrap(),
|
eprintln!("Bad {}: {}", name, Comparison::new(&have, &want));
|
||||||
};
|
}
|
||||||
|
equal
|
||||||
|
}
|
||||||
|
|
||||||
let want = Output {
|
let output_stderr = str::from_utf8(&output.stderr).unwrap();
|
||||||
status: self.status,
|
|
||||||
stdout: &stdout,
|
|
||||||
stderr: &stderr,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(have, want, "bad output");
|
if let Some(ref stderr_regex) = self.stderr_regex {
|
||||||
|
if !stderr_regex.is_match(output_stderr) {
|
||||||
|
panic!(
|
||||||
|
"Stderr regex mismatch: {} !~= /{}/",
|
||||||
|
output_stderr, stderr_regex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !compare("status", output.status.code().unwrap(), self.status)
|
||||||
|
| !compare("stdout", str::from_utf8(&output.stdout).unwrap(), &stdout)
|
||||||
|
| (self.stderr_regex.is_none() && !compare("stderr", output_stderr, &stderr))
|
||||||
|
{
|
||||||
|
panic!("Output mismatch.");
|
||||||
|
}
|
||||||
|
|
||||||
if self.status == EXIT_SUCCESS {
|
if self.status == EXIT_SUCCESS {
|
||||||
test_round_trip(tmp.path());
|
test_round_trip(self.tempdir.path());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.tempdir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user