Förmatterdämmerung (#346)

Format with rustfmt
This commit is contained in:
Casey Rodarmor 2018-12-08 14:29:41 -08:00 committed by GitHub
parent ec82cc20d3
commit 3d67786aaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1645 additions and 985 deletions

View File

@ -5,30 +5,30 @@ use brev;
pub struct AssignmentEvaluator<'a: 'b, 'b> { pub struct AssignmentEvaluator<'a: 'b, 'b> {
pub assignments: &'b Map<&'a str, Expression<'a>>, pub assignments: &'b Map<&'a str, Expression<'a>>,
pub invocation_directory: &'b Result<PathBuf, String>, pub invocation_directory: &'b Result<PathBuf, String>,
pub dotenv: &'b Map<String, String>, pub dotenv: &'b Map<String, String>,
pub dry_run: bool, pub dry_run: bool,
pub evaluated: Map<&'a str, String>, pub evaluated: Map<&'a str, String>,
pub exports: &'b Set<&'a str>, pub exports: &'b Set<&'a str>,
pub overrides: &'b Map<&'b str, &'b str>, pub overrides: &'b Map<&'b str, &'b str>,
pub quiet: bool, pub quiet: bool,
pub scope: &'b Map<&'a str, String>, pub scope: &'b Map<&'a str, String>,
pub shell: &'b str, pub shell: &'b str,
} }
impl<'a, 'b> AssignmentEvaluator<'a, 'b> { impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
pub fn evaluate_assignments( pub fn evaluate_assignments(
assignments: &Map<&'a str, Expression<'a>>, assignments: &Map<&'a str, Expression<'a>>,
invocation_directory: &Result<PathBuf, String>, invocation_directory: &Result<PathBuf, String>,
dotenv: &'b Map<String, String>, dotenv: &'b Map<String, String>,
overrides: &Map<&str, &str>, overrides: &Map<&str, &str>,
quiet: bool, quiet: bool,
shell: &'a str, shell: &'a str,
dry_run: bool, dry_run: bool,
) -> RunResult<'a, Map<&'a str, String>> { ) -> RunResult<'a, Map<&'a str, String>> {
let mut evaluator = AssignmentEvaluator { let mut evaluator = AssignmentEvaluator {
evaluated: empty(), evaluated: empty(),
exports: &empty(), exports: &empty(),
scope: &empty(), scope: &empty(),
assignments, assignments,
invocation_directory, invocation_directory,
dotenv, dotenv,
@ -47,14 +47,14 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
pub fn evaluate_line( pub fn evaluate_line(
&mut self, &mut self,
line: &[Fragment<'a>], line: &[Fragment<'a>],
arguments: &Map<&str, Cow<str>> arguments: &Map<&str, Cow<str>>,
) -> RunResult<'a, String> { ) -> RunResult<'a, String> {
let mut evaluated = String::new(); let mut evaluated = String::new();
for fragment in line { for fragment in line {
match *fragment { match *fragment {
Fragment::Text{ref text} => evaluated += text.lexeme, Fragment::Text { ref text } => evaluated += text.lexeme,
Fragment::Expression{ref expression} => { Fragment::Expression { ref expression } => {
evaluated += &self.evaluate_expression(expression, arguments)?; evaluated += &self.evaluate_expression(expression, arguments)?;
} }
} }
@ -76,7 +76,7 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
} }
} else { } else {
return Err(RuntimeError::Internal { return Err(RuntimeError::Internal {
message: format!("attempted to evaluated unknown assignment {}", name) message: format!("attempted to evaluated unknown assignment {}", name),
}); });
} }
@ -86,10 +86,10 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
fn evaluate_expression( fn evaluate_expression(
&mut self, &mut self,
expression: &Expression<'a>, expression: &Expression<'a>,
arguments: &Map<&str, Cow<str>> arguments: &Map<&str, Cow<str>>,
) -> RunResult<'a, String> { ) -> RunResult<'a, String> {
match *expression { match *expression {
Expression::Variable{name, ..} => { Expression::Variable { name, .. } => {
if self.evaluated.contains_key(name) { if self.evaluated.contains_key(name) {
Ok(self.evaluated[name].clone()) Ok(self.evaluated[name].clone())
} else if self.scope.contains_key(name) { } else if self.scope.contains_key(name) {
@ -101,50 +101,50 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
Ok(arguments[name].to_string()) Ok(arguments[name].to_string())
} else { } else {
Err(RuntimeError::Internal { Err(RuntimeError::Internal {
message: format!("attempted to evaluate undefined variable `{}`", name) message: format!("attempted to evaluate undefined variable `{}`", name),
}) })
} }
} }
Expression::Call{name, arguments: ref call_arguments, ref token} => { Expression::Call {
let call_arguments = call_arguments.iter().map(|argument| { name,
self.evaluate_expression(argument, arguments) arguments: ref call_arguments,
}).collect::<Result<Vec<String>, RuntimeError>>()?; ref token,
} => {
let call_arguments = call_arguments
.iter()
.map(|argument| self.evaluate_expression(argument, arguments))
.collect::<Result<Vec<String>, RuntimeError>>()?;
let context = FunctionContext { let context = FunctionContext {
invocation_directory: &self.invocation_directory, invocation_directory: &self.invocation_directory,
dotenv: self.dotenv, dotenv: self.dotenv,
}; };
evaluate_function(token, name, &context, &call_arguments) evaluate_function(token, name, &context, &call_arguments)
} }
Expression::String{ref cooked_string} => Ok(cooked_string.cooked.clone()), Expression::String { ref cooked_string } => Ok(cooked_string.cooked.clone()),
Expression::Backtick{raw, ref token} => { Expression::Backtick { raw, ref token } => {
if self.dry_run { if self.dry_run {
Ok(format!("`{}`", raw)) Ok(format!("`{}`", raw))
} else { } else {
Ok(self.run_backtick(self.dotenv, raw, token)?) Ok(self.run_backtick(self.dotenv, raw, token)?)
} }
} }
Expression::Concatination{ref lhs, ref rhs} => { Expression::Concatination { ref lhs, ref rhs } => {
Ok( Ok(self.evaluate_expression(lhs, arguments)? + &self.evaluate_expression(rhs, arguments)?)
self.evaluate_expression(lhs, arguments)?
+
&self.evaluate_expression(rhs, arguments)?
)
} }
} }
} }
fn run_backtick( fn run_backtick(
&self, &self,
dotenv: &Map<String, String>, dotenv: &Map<String, String>,
raw: &str, raw: &str,
token: &Token<'a>, token: &Token<'a>,
) -> RunResult<'a, String> { ) -> RunResult<'a, String> {
let mut cmd = Command::new(self.shell); let mut cmd = Command::new(self.shell);
cmd.export_environment_variables(self.scope, dotenv, self.exports)?; cmd.export_environment_variables(self.scope, dotenv, self.exports)?;
cmd.arg("-cu") cmd.arg("-cu").arg(raw);
.arg(raw);
cmd.stderr(if self.quiet { cmd.stderr(if self.quiet {
process::Stdio::null() process::Stdio::null()
@ -152,13 +152,15 @@ impl<'a, 'b> AssignmentEvaluator<'a, 'b> {
process::Stdio::inherit() process::Stdio::inherit()
}); });
InterruptHandler::guard(|| brev::output(cmd) InterruptHandler::guard(|| {
.map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error}) brev::output(cmd).map_err(|output_error| RuntimeError::Backtick {
) token: token.clone(),
output_error,
})
})
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -173,11 +175,16 @@ mod test {
#[test] #[test]
fn backtick_code() { fn backtick_code() {
match parse_success("a:\n echo {{`f() { return 100; }; f`}}") match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
.run(&no_cwd_err(), &["a"], &Default::default()).unwrap_err() { .run(&no_cwd_err(), &["a"], &Default::default())
RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => { .unwrap_err()
{
RuntimeError::Backtick {
token,
output_error: OutputError::Code(code),
} => {
assert_eq!(code, 100); assert_eq!(code, 100);
assert_eq!(token.lexeme, "`f() { return 100; }; f`"); assert_eq!(token.lexeme, "`f() { return 100; }; f`");
}, }
other => panic!("expected a code run error, but got: {}", other), other => panic!("expected a code run error, but got: {}", other),
} }
} }
@ -196,10 +203,16 @@ recipe:
..Default::default() ..Default::default()
}; };
match parse_success(text).run(&no_cwd_err(), &["recipe"], &configuration).unwrap_err() { match parse_success(text)
RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => { .run(&no_cwd_err(), &["recipe"], &configuration)
.unwrap_err()
{
RuntimeError::Backtick {
token,
output_error: OutputError::Code(_),
} => {
assert_eq!(token.lexeme, "`echo $exported_variable`"); assert_eq!(token.lexeme, "`echo $exported_variable`");
}, }
other => panic!("expected a backtick code errror, but got: {}", other), other => panic!("expected a backtick code errror, but got: {}", other),
} }
} }

View File

@ -3,22 +3,21 @@ use common::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
pub struct AssignmentResolver<'a: 'b, 'b> { pub struct AssignmentResolver<'a: 'b, 'b> {
assignments: &'b Map<&'a str, Expression<'a>>, assignments: &'b Map<&'a str, Expression<'a>>,
assignment_tokens: &'b Map<&'a str, Token<'a>>, assignment_tokens: &'b Map<&'a str, Token<'a>>,
stack: Vec<&'a str>, stack: Vec<&'a str>,
seen: Set<&'a str>, seen: Set<&'a str>,
evaluated: Set<&'a str>, evaluated: Set<&'a str>,
} }
impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> { impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
pub fn resolve_assignments( pub fn resolve_assignments(
assignments: &Map<&'a str, Expression<'a>>, assignments: &Map<&'a str, Expression<'a>>,
assignment_tokens: &Map<&'a str, Token<'a>>, assignment_tokens: &Map<&'a str, Token<'a>>,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'a, ()> {
let mut resolver = AssignmentResolver { let mut resolver = AssignmentResolver {
stack: empty(), stack: empty(),
seen: empty(), seen: empty(),
evaluated: empty(), evaluated: empty(),
assignments, assignments,
assignment_tokens, assignment_tokens,
@ -45,21 +44,20 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
} else { } else {
let message = format!("attempted to resolve unknown assignment `{}`", name); let message = format!("attempted to resolve unknown assignment `{}`", name);
return Err(CompilationError { return Err(CompilationError {
text: "", text: "",
index: 0, index: 0,
line: 0, line: 0,
column: 0, column: 0,
width: None, width: None,
kind: Internal{message} kind: Internal { message },
}); });
} }
Ok(()) Ok(())
} }
fn resolve_expression( fn resolve_expression(&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
&mut self, expression: &Expression<'a>) -> CompilationResult<'a, ()> {
match *expression { match *expression {
Expression::Variable{name, ref token} => { Expression::Variable { name, ref token } => {
if self.evaluated.contains(name) { if self.evaluated.contains(name) {
return Ok(()); return Ok(());
} else if self.seen.contains(name) { } else if self.seen.contains(name) {
@ -67,22 +65,24 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
self.stack.push(name); self.stack.push(name);
return Err(token.error(CircularVariableDependency { return Err(token.error(CircularVariableDependency {
variable: name, variable: name,
circle: self.stack.clone(), circle: self.stack.clone(),
})); }));
} else if self.assignments.contains_key(name) { } else if self.assignments.contains_key(name) {
self.resolve_assignment(name)?; self.resolve_assignment(name)?;
} else { } else {
return Err(token.error(UndefinedVariable{variable: name})); return Err(token.error(UndefinedVariable { variable: name }));
} }
} }
Expression::Call{ref token, ref arguments, ..} => { Expression::Call {
resolve_function(token, arguments.len())? ref token,
} ref arguments,
Expression::Concatination{ref lhs, ref rhs} => { ..
} => resolve_function(token, arguments.len())?,
Expression::Concatination { ref lhs, ref rhs } => {
self.resolve_expression(lhs)?; self.resolve_expression(lhs)?;
self.resolve_expression(rhs)?; self.resolve_expression(rhs)?;
} }
Expression::String{..} | Expression::Backtick{..} => {} Expression::String { .. } | Expression::Backtick { .. } => {}
} }
Ok(()) Ok(())
} }

View File

@ -3,8 +3,8 @@ extern crate atty;
use common::*; use common::*;
use self::ansi_term::{Style, Prefix, Suffix, ANSIGenericString};
use self::ansi_term::Color::*; use self::ansi_term::Color::*;
use self::ansi_term::{ANSIGenericString, Prefix, Style, Suffix};
use self::atty::is as is_atty; use self::atty::is as is_atty;
use self::atty::Stream; use self::atty::Stream;
@ -18,26 +18,23 @@ pub enum UseColor {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Color { pub struct Color {
use_color: UseColor, use_color: UseColor,
atty: bool, atty: bool,
style: Style, style: Style,
} }
impl Default for Color { impl Default for Color {
fn default() -> Color { fn default() -> Color {
Color { Color {
use_color: UseColor::Never, use_color: UseColor::Never,
atty: false, atty: false,
style: Style::new(), style: Style::new(),
} }
} }
} }
impl Color { impl Color {
fn restyle(self, style: Style) -> Color { fn restyle(self, style: Style) -> Color {
Color { Color { style, ..self }
style,
..self
}
} }
fn redirect(self, stream: Stream) -> Color { fn redirect(self, stream: Stream) -> Color {
@ -127,8 +124,8 @@ impl Color {
pub fn active(&self) -> bool { pub fn active(&self) -> bool {
match self.use_color { match self.use_color {
UseColor::Always => true, UseColor::Always => true,
UseColor::Never => false, UseColor::Never => false,
UseColor::Auto => self.atty, UseColor::Auto => self.atty,
} }
} }

View File

@ -3,18 +3,18 @@ use common::*;
pub trait CommandExt { pub trait CommandExt {
fn export_environment_variables<'a>( fn export_environment_variables<'a>(
&mut self, &mut self,
scope: &Map<&'a str, String>, scope: &Map<&'a str, String>,
dotenv: &Map<String, String>, dotenv: &Map<String, String>,
exports: &Set<&'a str> exports: &Set<&'a str>,
) -> RunResult<'a, ()>; ) -> RunResult<'a, ()>;
} }
impl CommandExt for Command { impl CommandExt for Command {
fn export_environment_variables<'a>( fn export_environment_variables<'a>(
&mut self, &mut self,
scope: &Map<&'a str, String>, scope: &Map<&'a str, String>,
dotenv: &Map<String, String>, dotenv: &Map<String, String>,
exports: &Set<&'a str> exports: &Set<&'a str>,
) -> RunResult<'a, ()> { ) -> RunResult<'a, ()> {
for (name, value) in dotenv { for (name, value) in dotenv {
self.env(name, value); self.env(name, value);
@ -24,7 +24,7 @@ impl CommandExt for Command {
self.env(name, value); self.env(name, value);
} else { } else {
return Err(RuntimeError::Internal { return Err(RuntimeError::Internal {
message: format!("scope does not contain exported variable `{}`", name), message: format!("scope does not contain exported variable `{}`", name),
}); });
} }
} }

View File

@ -6,7 +6,7 @@ pub use std::ops::Range;
pub use std::path::{Path, PathBuf}; pub use std::path::{Path, PathBuf};
pub use std::process::Command; pub use std::process::Command;
pub use std::sync::{Mutex, MutexGuard}; pub use std::sync::{Mutex, MutexGuard};
pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize}; pub use std::{cmp, env, fmt, fs, io, iter, process, usize, vec};
pub use color::Color; pub use color::Color;
pub use libc::{EXIT_FAILURE, EXIT_SUCCESS}; pub use libc::{EXIT_FAILURE, EXIT_SUCCESS};
@ -32,7 +32,7 @@ pub use parser::Parser;
pub use range_ext::RangeExt; pub use range_ext::RangeExt;
pub use recipe::{Recipe, RecipeContext}; pub use recipe::{Recipe, RecipeContext};
pub use recipe_resolver::RecipeResolver; pub use recipe_resolver::RecipeResolver;
pub use runtime_error::{RuntimeError, RunResult}; pub use runtime_error::{RunResult, RuntimeError};
pub use shebang::Shebang; pub use shebang::Shebang;
pub use token::{Token, TokenKind}; pub use token::{Token, TokenKind};
pub use verbosity::Verbosity; pub use verbosity::Verbosity;

View File

@ -1,42 +1,91 @@
use common::*; use common::*;
use misc::{Or, write_error_context, show_whitespace, maybe_s}; use misc::{maybe_s, show_whitespace, write_error_context, Or};
pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>; pub type CompilationResult<'a, T> = Result<T, CompilationError<'a>>;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct CompilationError<'a> { pub struct CompilationError<'a> {
pub text: &'a str, pub text: &'a str,
pub index: usize, pub index: usize,
pub line: usize, pub line: usize,
pub column: usize, pub column: usize,
pub width: Option<usize>, pub width: Option<usize>,
pub kind: CompilationErrorKind<'a>, pub kind: CompilationErrorKind<'a>,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum CompilationErrorKind<'a> { pub enum CompilationErrorKind<'a> {
CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>}, CircularRecipeDependency {
CircularVariableDependency{variable: &'a str, circle: Vec<&'a str>}, recipe: &'a str,
DependencyHasParameters{recipe: &'a str, dependency: &'a str}, circle: Vec<&'a str>,
DuplicateDependency{recipe: &'a str, dependency: &'a str}, },
DuplicateParameter{recipe: &'a str, parameter: &'a str}, CircularVariableDependency {
DuplicateRecipe{recipe: &'a str, first: usize}, variable: &'a str,
DuplicateVariable{variable: &'a str}, circle: Vec<&'a str>,
},
DependencyHasParameters {
recipe: &'a str,
dependency: &'a str,
},
DuplicateDependency {
recipe: &'a str,
dependency: &'a str,
},
DuplicateParameter {
recipe: &'a str,
parameter: &'a str,
},
DuplicateRecipe {
recipe: &'a str,
first: usize,
},
DuplicateVariable {
variable: &'a str,
},
ExtraLeadingWhitespace, ExtraLeadingWhitespace,
FunctionArgumentCountMismatch{function: &'a str, found: usize, expected: usize}, FunctionArgumentCountMismatch {
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, function: &'a str,
Internal{message: String}, found: usize,
InvalidEscapeSequence{character: char}, expected: usize,
MixedLeadingWhitespace{whitespace: &'a str}, },
InconsistentLeadingWhitespace {
expected: &'a str,
found: &'a str,
},
Internal {
message: String,
},
InvalidEscapeSequence {
character: char,
},
MixedLeadingWhitespace {
whitespace: &'a str,
},
OuterShebang, OuterShebang,
ParameterFollowsVariadicParameter{parameter: &'a str}, ParameterFollowsVariadicParameter {
ParameterShadowsVariable{parameter: &'a str}, parameter: &'a str,
RequiredParameterFollowsDefaultParameter{parameter: &'a str}, },
UndefinedVariable{variable: &'a str}, ParameterShadowsVariable {
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind}, parameter: &'a str,
UnknownDependency{recipe: &'a str, unknown: &'a str}, },
UnknownFunction{function: &'a str}, RequiredParameterFollowsDefaultParameter {
parameter: &'a str,
},
UndefinedVariable {
variable: &'a str,
},
UnexpectedToken {
expected: Vec<TokenKind>,
found: TokenKind,
},
UnknownDependency {
recipe: &'a str,
unknown: &'a str,
},
UnknownFunction {
function: &'a str,
},
UnknownStartOfToken, UnknownStartOfToken,
UnterminatedInterpolation, UnterminatedInterpolation,
UnterminatedString, UnterminatedString,
@ -45,32 +94,43 @@ pub enum CompilationErrorKind<'a> {
impl<'a> Display for CompilationError<'a> { impl<'a> Display for CompilationError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use CompilationErrorKind::*; use CompilationErrorKind::*;
let error = Color::fmt(f).error(); let error = Color::fmt(f).error();
let message = Color::fmt(f).message(); let message = Color::fmt(f).message();
write!(f, "{} {}", error.paint("error:"), message.prefix())?; write!(f, "{} {}", error.paint("error:"), message.prefix())?;
match self.kind { match self.kind {
CircularRecipeDependency{recipe, ref circle} => { CircularRecipeDependency { recipe, ref circle } => {
if circle.len() == 2 { if circle.len() == 2 {
writeln!(f, "Recipe `{}` depends on itself", recipe)?; writeln!(f, "Recipe `{}` depends on itself", recipe)?;
} else { } else {
writeln!(f, "Recipe `{}` has circular dependency `{}`", writeln!(
recipe, circle.join(" -> "))?; f,
"Recipe `{}` has circular dependency `{}`",
recipe,
circle.join(" -> ")
)?;
} }
} }
CircularVariableDependency{variable, ref circle} => { CircularVariableDependency {
variable,
ref circle,
} => {
if circle.len() == 2 { if circle.len() == 2 {
writeln!(f, "Variable `{}` is defined in terms of itself", variable)?; writeln!(f, "Variable `{}` is defined in terms of itself", variable)?;
} else { } else {
writeln!(f, "Variable `{}` depends on its own value: `{}`", writeln!(
variable, circle.join(" -> "))?; f,
"Variable `{}` depends on its own value: `{}`",
variable,
circle.join(" -> ")
)?;
} }
} }
InvalidEscapeSequence{character} => { InvalidEscapeSequence { character } => {
let representation = match character { let representation = match character {
'`' => r"\`".to_string(), '`' => r"\`".to_string(),
'\\' => r"\".to_string(), '\\' => r"\".to_string(),
'\'' => r"'".to_string(), '\'' => r"'".to_string(),
'"' => r#"""#.to_string(), '"' => r#"""#.to_string(),
@ -78,69 +138,111 @@ impl<'a> Display for CompilationError<'a> {
}; };
writeln!(f, "`\\{}` is not a valid escape sequence", representation)?; writeln!(f, "`\\{}` is not a valid escape sequence", representation)?;
} }
DuplicateParameter{recipe, parameter} => { DuplicateParameter { recipe, parameter } => {
writeln!(f, "Recipe `{}` has duplicate parameter `{}`", recipe, parameter)?; writeln!(
f,
"Recipe `{}` has duplicate parameter `{}`",
recipe, parameter
)?;
} }
DuplicateVariable{variable} => { DuplicateVariable { variable } => {
writeln!(f, "Variable `{}` has multiple definitions", variable)?; writeln!(f, "Variable `{}` has multiple definitions", variable)?;
} }
UnexpectedToken{ref expected, found} => { UnexpectedToken {
ref expected,
found,
} => {
writeln!(f, "Expected {}, but found {}", Or(expected), found)?; writeln!(f, "Expected {}, but found {}", Or(expected), found)?;
} }
DuplicateDependency{recipe, dependency} => { DuplicateDependency { recipe, dependency } => {
writeln!(f, "Recipe `{}` has duplicate dependency `{}`", recipe, dependency)?; writeln!(
f,
"Recipe `{}` has duplicate dependency `{}`",
recipe, dependency
)?;
} }
DuplicateRecipe{recipe, first} => { DuplicateRecipe { recipe, first } => {
writeln!(f, "Recipe `{}` first defined on line {} is redefined on line {}", writeln!(
recipe, first + 1, self.line + 1)?; f,
"Recipe `{}` first defined on line {} is redefined on line {}",
recipe,
first + 1,
self.line + 1
)?;
} }
DependencyHasParameters{recipe, dependency} => { DependencyHasParameters { recipe, dependency } => {
writeln!(f, "Recipe `{}` depends on `{}` which requires arguments. \ writeln!(
Dependencies may not require arguments", recipe, dependency)?; f,
"Recipe `{}` depends on `{}` which requires arguments. \
Dependencies may not require arguments",
recipe, dependency
)?;
} }
ParameterShadowsVariable{parameter} => { ParameterShadowsVariable { parameter } => {
writeln!(f, "Parameter `{}` shadows variable of the same name", parameter)?; writeln!(
f,
"Parameter `{}` shadows variable of the same name",
parameter
)?;
} }
RequiredParameterFollowsDefaultParameter{parameter} => { RequiredParameterFollowsDefaultParameter { parameter } => {
writeln!(f, "Non-default parameter `{}` follows default parameter", parameter)?; writeln!(
f,
"Non-default parameter `{}` follows default parameter",
parameter
)?;
} }
ParameterFollowsVariadicParameter{parameter} => { ParameterFollowsVariadicParameter { parameter } => {
writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?; writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?;
} }
MixedLeadingWhitespace{whitespace} => { MixedLeadingWhitespace { whitespace } => {
writeln!(f, writeln!(
f,
"Found a mix of tabs and spaces in leading whitespace: `{}`\n\ "Found a mix of tabs and spaces in leading whitespace: `{}`\n\
Leading whitespace may consist of tabs or spaces, but not both", Leading whitespace may consist of tabs or spaces, but not both",
show_whitespace(whitespace) show_whitespace(whitespace)
)?; )?;
} }
ExtraLeadingWhitespace => { ExtraLeadingWhitespace => {
writeln!(f, "Recipe line has extra leading whitespace")?; writeln!(f, "Recipe line has extra leading whitespace")?;
} }
FunctionArgumentCountMismatch{function, found, expected} => { FunctionArgumentCountMismatch {
function,
found,
expected,
} => {
writeln!( writeln!(
f, f,
"Function `{}` called with {} argument{} but takes {}", "Function `{}` called with {} argument{} but takes {}",
function, found, maybe_s(found), expected function,
found,
maybe_s(found),
expected
)?; )?;
} }
InconsistentLeadingWhitespace{expected, found} => { InconsistentLeadingWhitespace { expected, found } => {
writeln!(f, writeln!(
f,
"Recipe line has inconsistent leading whitespace. \ "Recipe line has inconsistent leading whitespace. \
Recipe started with `{}` but found line with `{}`", Recipe started with `{}` but found line with `{}`",
show_whitespace(expected), show_whitespace(found) show_whitespace(expected),
show_whitespace(found)
)?; )?;
} }
OuterShebang => { OuterShebang => {
writeln!(f, "`#!` is reserved syntax outside of recipes")?; writeln!(f, "`#!` is reserved syntax outside of recipes")?;
} }
UnknownDependency{recipe, unknown} => { UnknownDependency { recipe, unknown } => {
writeln!(f, "Recipe `{}` has unknown dependency `{}`", recipe, unknown)?; writeln!(
f,
"Recipe `{}` has unknown dependency `{}`",
recipe, unknown
)?;
} }
UndefinedVariable{variable} => { UndefinedVariable { variable } => {
writeln!(f, "Variable `{}` not defined", variable)?; writeln!(f, "Variable `{}` not defined", variable)?;
} }
UnknownFunction{function} => { UnknownFunction { function } => {
writeln!(f, "Call to unknown function `{}`", function)?; writeln!(f, "Call to unknown function `{}`", function)?;
} }
UnknownStartOfToken => { UnknownStartOfToken => {
@ -152,10 +254,13 @@ impl<'a> Display for CompilationError<'a> {
UnterminatedString => { UnterminatedString => {
writeln!(f, "Unterminated string")?; writeln!(f, "Unterminated string")?;
} }
Internal{ref message} => { Internal { ref message } => {
writeln!(f, "Internal error, this may indicate a bug in just: {}\n\ writeln!(
consider filing an issue: https://github.com/casey/just/issues/new", f,
message)?; "Internal error, this may indicate a bug in just: {}\n\
consider filing an issue: https://github.com/casey/just/issues/new",
message
)?;
} }
} }

View File

@ -3,26 +3,26 @@ use common::*;
pub const DEFAULT_SHELL: &str = "sh"; pub const DEFAULT_SHELL: &str = "sh";
pub struct Configuration<'a> { pub struct Configuration<'a> {
pub dry_run: bool, pub dry_run: bool,
pub evaluate: bool, pub evaluate: bool,
pub highlight: bool, pub highlight: bool,
pub overrides: Map<&'a str, &'a str>, pub overrides: Map<&'a str, &'a str>,
pub quiet: bool, pub quiet: bool,
pub shell: &'a str, pub shell: &'a str,
pub color: Color, pub color: Color,
pub verbosity: Verbosity, pub verbosity: Verbosity,
} }
impl<'a> Default for Configuration<'a> { impl<'a> Default for Configuration<'a> {
fn default() -> Configuration<'static> { fn default() -> Configuration<'static> {
Configuration { Configuration {
dry_run: false, dry_run: false,
evaluate: false, evaluate: false,
highlight: false, highlight: false,
overrides: empty(), overrides: empty(),
quiet: false, quiet: false,
shell: DEFAULT_SHELL, shell: DEFAULT_SHELL,
color: default(), color: default(),
verbosity: Verbosity::from_flag_occurrences(0), verbosity: Verbosity::from_flag_occurrences(0),
} }
} }

View File

@ -2,30 +2,35 @@ use common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct CookedString<'a> { pub struct CookedString<'a> {
pub raw: &'a str, pub raw: &'a str,
pub cooked: String, pub cooked: String,
} }
impl<'a> CookedString<'a> { impl<'a> CookedString<'a> {
pub fn new(token: &Token<'a>) -> CompilationResult<'a, CookedString<'a>> { pub fn new(token: &Token<'a>) -> CompilationResult<'a, CookedString<'a>> {
let raw = &token.lexeme[1..token.lexeme.len()-1]; let raw = &token.lexeme[1..token.lexeme.len() - 1];
if let TokenKind::RawString = token.kind { if let TokenKind::RawString = token.kind {
Ok(CookedString{cooked: raw.to_string(), raw}) Ok(CookedString {
cooked: raw.to_string(),
raw,
})
} else if let TokenKind::StringToken = token.kind { } else if let TokenKind::StringToken = token.kind {
let mut cooked = String::new(); let mut cooked = String::new();
let mut escape = false; let mut escape = false;
for c in raw.chars() { for c in raw.chars() {
if escape { if escape {
match c { match c {
'n' => cooked.push('\n'), 'n' => cooked.push('\n'),
'r' => cooked.push('\r'), 'r' => cooked.push('\r'),
't' => cooked.push('\t'), 't' => cooked.push('\t'),
'\\' => cooked.push('\\'), '\\' => cooked.push('\\'),
'"' => cooked.push('"'), '"' => cooked.push('"'),
other => return Err(token.error(CompilationErrorKind::InvalidEscapeSequence { other => {
character: other, return Err(
})), token.error(CompilationErrorKind::InvalidEscapeSequence { character: other }),
)
}
} }
escape = false; escape = false;
continue; continue;
@ -36,10 +41,10 @@ impl<'a> CookedString<'a> {
} }
cooked.push(c); cooked.push(c);
} }
Ok(CookedString{raw, cooked}) Ok(CookedString { raw, cooked })
} else { } else {
Err(token.error(CompilationErrorKind::Internal { Err(token.error(CompilationErrorKind::Internal {
message: "cook_string() called on non-string token".to_string() message: "cook_string() called on non-string token".to_string(),
})) }))
} }
} }

View File

@ -2,35 +2,50 @@ use common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Expression<'a> { pub enum Expression<'a> {
Backtick{raw: &'a str, token: Token<'a>}, Backtick {
Call{name: &'a str, token: Token<'a>, arguments: Vec<Expression<'a>>}, raw: &'a str,
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>}, token: Token<'a>,
String{cooked_string: CookedString<'a>}, },
Variable{name: &'a str, token: Token<'a>}, Call {
name: &'a str,
token: Token<'a>,
arguments: Vec<Expression<'a>>,
},
Concatination {
lhs: Box<Expression<'a>>,
rhs: Box<Expression<'a>>,
},
String {
cooked_string: CookedString<'a>,
},
Variable {
name: &'a str,
token: Token<'a>,
},
} }
impl<'a> Expression<'a> { impl<'a> Expression<'a> {
pub fn variables(&'a self) -> Variables<'a> { pub fn variables(&'a self) -> Variables<'a> {
Variables { Variables { stack: vec![self] }
stack: vec![self],
}
} }
pub fn functions(&'a self) -> Functions<'a> { pub fn functions(&'a self) -> Functions<'a> {
Functions { Functions { stack: vec![self] }
stack: vec![self],
}
} }
} }
impl<'a> Display for Expression<'a> { impl<'a> Display for Expression<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self { match *self {
Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?, Expression::Backtick { raw, .. } => write!(f, "`{}`", raw)?,
Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, Expression::Concatination { ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?,
Expression::String {ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?, Expression::String { ref cooked_string } => write!(f, "\"{}\"", cooked_string.raw)?,
Expression::Variable {name, .. } => write!(f, "{}", name)?, Expression::Variable { name, .. } => write!(f, "{}", name)?,
Expression::Call {name, ref arguments, ..} => { Expression::Call {
name,
ref arguments,
..
} => {
write!(f, "{}(", name)?; write!(f, "{}(", name)?;
for (i, argument) in arguments.iter().enumerate() { for (i, argument) in arguments.iter().enumerate() {
if i > 0 { if i > 0 {
@ -56,11 +71,11 @@ impl<'a> Iterator for Variables<'a> {
fn next(&mut self) -> Option<&'a Token<'a>> { fn next(&mut self) -> Option<&'a Token<'a>> {
match self.stack.pop() { match self.stack.pop() {
None None
| Some(&Expression::String{..}) | Some(&Expression::String { .. })
| Some(&Expression::Backtick{..}) | Some(&Expression::Backtick { .. })
| Some(&Expression::Call{..}) => None, | Some(&Expression::Call { .. }) => None,
Some(&Expression::Variable{ref token,..}) => Some(token), Some(&Expression::Variable { ref token, .. }) => Some(token),
Some(&Expression::Concatination{ref lhs, ref rhs}) => { Some(&Expression::Concatination { ref lhs, ref rhs }) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);
self.next() self.next()
@ -79,11 +94,15 @@ impl<'a> Iterator for Functions<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() { match self.stack.pop() {
None None
| Some(&Expression::String{..}) | Some(&Expression::String { .. })
| Some(&Expression::Backtick{..}) | Some(&Expression::Backtick { .. })
| Some(&Expression::Variable{..}) => None, | Some(&Expression::Variable { .. }) => None,
Some(&Expression::Call{ref token, ref arguments, ..}) => Some((token, arguments.len())), Some(&Expression::Call {
Some(&Expression::Concatination{ref lhs, ref rhs}) => { ref token,
ref arguments,
..
}) => Some((token, arguments.len())),
Some(&Expression::Concatination { ref lhs, ref rhs }) => {
self.stack.push(lhs); self.stack.push(lhs);
self.stack.push(rhs); self.stack.push(rhs);
self.next() self.next()

View File

@ -2,14 +2,14 @@ use common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum Fragment<'a> { pub enum Fragment<'a> {
Text{text: Token<'a>}, Text { text: Token<'a> },
Expression{expression: Expression<'a>}, Expression { expression: Expression<'a> },
} }
impl<'a> Fragment<'a> { impl<'a> Fragment<'a> {
pub fn continuation(&self) -> bool { pub fn continuation(&self) -> bool {
match *self { match *self {
Fragment::Text{ref text} => text.lexeme.ends_with('\\'), Fragment::Text { ref text } => text.lexeme.ends_with('\\'),
_ => false, _ => false,
} }
} }

View File

@ -6,19 +6,24 @@ use platform::{Platform, PlatformInterface};
lazy_static! { lazy_static! {
static ref FUNCTIONS: Map<&'static str, Function> = vec![ static ref FUNCTIONS: Map<&'static str, Function> = vec![
("arch", Function::Nullary(arch )), ("arch", Function::Nullary(arch)),
("os", Function::Nullary(os )), ("os", Function::Nullary(os)),
("os_family", Function::Nullary(os_family )), ("os_family", Function::Nullary(os_family)),
("env_var", Function::Unary (env_var )), ("env_var", Function::Unary(env_var)),
("env_var_or_default", Function::Binary (env_var_or_default)), ("env_var_or_default", Function::Binary(env_var_or_default)),
("invocation_directory", Function::Nullary(invocation_directory)), (
].into_iter().collect(); "invocation_directory",
Function::Nullary(invocation_directory)
),
]
.into_iter()
.collect();
} }
enum Function { enum Function {
Nullary(fn(&FunctionContext, ) -> Result<String, String>), Nullary(fn(&FunctionContext) -> Result<String, String>),
Unary (fn(&FunctionContext, &str ) -> Result<String, String>), Unary(fn(&FunctionContext, &str) -> Result<String, String>),
Binary (fn(&FunctionContext, &str, &str) -> Result<String, String>), Binary(fn(&FunctionContext, &str, &str) -> Result<String, String>),
} }
impl Function { impl Function {
@ -26,8 +31,8 @@ impl Function {
use self::Function::*; use self::Function::*;
match *self { match *self {
Nullary(_) => 0, Nullary(_) => 0,
Unary(_) => 1, Unary(_) => 1,
Binary(_) => 2, Binary(_) => 2,
} }
} }
} }
@ -42,45 +47,56 @@ pub fn resolve_function<'a>(token: &Token<'a>, argc: usize) -> CompilationResult
if let Some(function) = FUNCTIONS.get(&name) { if let Some(function) = FUNCTIONS.get(&name) {
use self::Function::*; use self::Function::*;
match (function, argc) { match (function, argc) {
(&Nullary(_), 0) | (&Nullary(_), 0) | (&Unary(_), 1) | (&Binary(_), 2) => Ok(()),
(&Unary(_), 1) | _ => Err(
(&Binary(_), 2) => Ok(()), token.error(CompilationErrorKind::FunctionArgumentCountMismatch {
_ => { function: name,
Err(token.error(CompilationErrorKind::FunctionArgumentCountMismatch{ found: argc,
function: name, found: argc, expected: function.argc(), expected: function.argc(),
})) }),
} ),
} }
} else { } else {
Err(token.error(CompilationErrorKind::UnknownFunction{function: token.lexeme})) Err(token.error(CompilationErrorKind::UnknownFunction {
function: token.lexeme,
}))
} }
} }
pub fn evaluate_function<'a>( pub fn evaluate_function<'a>(
token: &Token<'a>, token: &Token<'a>,
name: &'a str, name: &'a str,
context: &FunctionContext, context: &FunctionContext,
arguments: &[String] arguments: &[String],
) -> RunResult<'a, String> { ) -> RunResult<'a, String> {
if let Some(function) = FUNCTIONS.get(name) { if let Some(function) = FUNCTIONS.get(name) {
use self::Function::*; use self::Function::*;
let argc = arguments.len(); let argc = arguments.len();
match (function, argc) { match (function, argc) {
(&Nullary(f), 0) => f(context) (&Nullary(f), 0) => f(context).map_err(|message| RuntimeError::FunctionCall {
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}), token: token.clone(),
(&Unary(f), 1) => f(context, &arguments[0]) message,
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}), }),
(&Binary(f), 2) => f(context, &arguments[0], &arguments[1]) (&Unary(f), 1) => f(context, &arguments[0]).map_err(|message| RuntimeError::FunctionCall {
.map_err(|message| RuntimeError::FunctionCall{token: token.clone(), message}), token: token.clone(),
_ => { message,
Err(RuntimeError::Internal { }),
message: format!("attempted to evaluate function `{}` with {} arguments", name, argc) (&Binary(f), 2) => {
f(context, &arguments[0], &arguments[1]).map_err(|message| RuntimeError::FunctionCall {
token: token.clone(),
message,
}) })
} }
_ => Err(RuntimeError::Internal {
message: format!(
"attempted to evaluate function `{}` with {} arguments",
name, argc
),
}),
} }
} else { } else {
Err(RuntimeError::Internal { Err(RuntimeError::Internal {
message: format!("attempted to evaluate unknown function: `{}`", name) message: format!("attempted to evaluate unknown function: `{}`", name),
}) })
} }
} }
@ -98,9 +114,9 @@ pub fn os_family(_context: &FunctionContext) -> Result<String, String> {
} }
pub fn invocation_directory(context: &FunctionContext) -> Result<String, String> { pub fn invocation_directory(context: &FunctionContext) -> Result<String, String> {
context.invocation_directory.clone() context.invocation_directory.clone().and_then(|s| {
.and_then(|s| Platform::to_shell_path(&s) Platform::to_shell_path(&s).map_err(|e| format!("Error getting shell path: {}", e))
.map_err(|e| format!("Error getting shell path: {}", e))) })
} }
pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> { pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
@ -112,16 +128,18 @@ pub fn env_var(context: &FunctionContext, key: &str) -> Result<String, String> {
match env::var(key) { match env::var(key) {
Err(NotPresent) => Err(format!("environment variable `{}` not present", key)), Err(NotPresent) => Err(format!("environment variable `{}` not present", key)),
Err(NotUnicode(os_string)) => Err(NotUnicode(os_string)) => Err(format!(
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)), "environment variable `{}` not unicode: {:?}",
key, os_string
)),
Ok(value) => Ok(value), Ok(value) => Ok(value),
} }
} }
pub fn env_var_or_default( pub fn env_var_or_default(
context: &FunctionContext, context: &FunctionContext,
key: &str, key: &str,
default: &str, default: &str,
) -> Result<String, String> { ) -> Result<String, String> {
if let Some(value) = context.dotenv.get(key) { if let Some(value) = context.dotenv.get(key) {
return Ok(value.clone()); return Ok(value.clone());
@ -130,8 +148,10 @@ pub fn env_var_or_default(
use std::env::VarError::*; use std::env::VarError::*;
match env::var(key) { match env::var(key) {
Err(NotPresent) => Ok(default.to_string()), Err(NotPresent) => Ok(default.to_string()),
Err(NotUnicode(os_string)) => Err(NotUnicode(os_string)) => Err(format!(
Err(format!("environment variable `{}` not unicode: {:?}", key, os_string)), "environment variable `{}` not unicode: {:?}",
key, os_string
)),
Ok(value) => Ok(value), Ok(value) => Ok(value),
} }
} }

View File

@ -2,7 +2,7 @@ use common::*;
pub fn compile(text: &str) { pub 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 CompilationErrorKind::Internal { .. } = error.kind {
panic!("{}", error) panic!("{}", error)
} }
} }

View File

@ -126,17 +126,15 @@ impl<'a> Justfile<'a> where {
}); });
} }
let context = RecipeContext{invocation_directory, configuration, scope}; let context = RecipeContext {
invocation_directory,
configuration,
scope,
};
let mut ran = empty(); let mut ran = empty();
for (recipe, arguments) in grouped { for (recipe, arguments) in grouped {
self.run_recipe( self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran)?
&context,
recipe,
arguments,
&dotenv,
&mut ran,
)?
} }
Ok(()) Ok(())
@ -152,21 +150,10 @@ impl<'a> Justfile<'a> where {
) -> RunResult<()> { ) -> RunResult<()> {
for dependency_name in &recipe.dependencies { for dependency_name in &recipe.dependencies {
if !ran.contains(dependency_name) { if !ran.contains(dependency_name) {
self.run_recipe( self.run_recipe(context, &self.recipes[dependency_name], &[], dotenv, ran)?;
context,
&self.recipes[dependency_name],
&[],
dotenv,
ran,
)?;
} }
} }
recipe.run( recipe.run(context, arguments, dotenv, &self.exports)?;
context,
arguments,
dotenv,
&self.exports,
)?;
ran.insert(recipe.name); ran.insert(recipe.name);
Ok(()) Ok(())
} }
@ -313,10 +300,7 @@ a return code:
min, min,
max, max,
} => { } => {
let param_names = parameters let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
.iter()
.map(|p| p.name)
.collect::<Vec<&str>>();
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2); assert_eq!(found, 2);
@ -340,10 +324,7 @@ a return code:
min, min,
max, max,
} => { } => {
let param_names = parameters let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
.iter()
.map(|p| p.name)
.collect::<Vec<&str>>();
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2); assert_eq!(found, 2);
@ -367,10 +348,7 @@ a return code:
min, min,
max, max,
} => { } => {
let param_names = parameters let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
.iter()
.map(|p| p.name)
.collect::<Vec<&str>>();
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0); assert_eq!(found, 0);
@ -394,10 +372,7 @@ a return code:
min, min,
max, max,
} => { } => {
let param_names = parameters let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
.iter()
.map(|p| p.name)
.collect::<Vec<&str>>();
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 1); assert_eq!(found, 1);
@ -421,10 +396,7 @@ a return code:
min, min,
max, max,
} => { } => {
let param_names = parameters let param_names = parameters.iter().map(|p| p.name).collect::<Vec<&str>>();
.iter()
.map(|p| p.name)
.collect::<Vec<&str>>();
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]); assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0); assert_eq!(found, 0);

View File

@ -1,7 +1,7 @@
use common::*; use common::*;
use TokenKind::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
use TokenKind::*;
fn re(pattern: &str) -> Regex { fn re(pattern: &str) -> Regex {
Regex::new(pattern).unwrap() Regex::new(pattern).unwrap()
@ -21,12 +21,12 @@ fn mixed_whitespace(text: &str) -> bool {
pub struct Lexer<'a> { pub struct Lexer<'a> {
tokens: Vec<Token<'a>>, tokens: Vec<Token<'a>>,
text: &'a str, text: &'a str,
rest: &'a str, rest: &'a str,
index: usize, index: usize,
column: usize, column: usize,
line: usize, line: usize,
state: Vec<State<'a>>, state: Vec<State<'a>>,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@ -39,13 +39,13 @@ enum State<'a> {
impl<'a> Lexer<'a> { impl<'a> Lexer<'a> {
pub fn lex(text: &'a str) -> CompilationResult<Vec<Token<'a>>> { pub fn lex(text: &'a str) -> CompilationResult<Vec<Token<'a>>> {
let lexer = Lexer{ let lexer = Lexer {
tokens: vec![], tokens: vec![],
rest: text, rest: text,
index: 0, index: 0,
line: 0, line: 0,
column: 0, column: 0,
state: vec![State::Start], state: vec![State::Start],
text, text,
}; };
@ -54,21 +54,21 @@ impl<'a> Lexer<'a> {
fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> { fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> {
CompilationError { CompilationError {
text: self.text, text: self.text,
index: self.index, index: self.index,
line: self.line, line: self.line,
column: self.column, column: self.column,
width: None, width: None,
kind, kind,
} }
} }
fn token(&self, prefix: &'a str, lexeme: &'a str, kind: TokenKind) -> Token<'a> { fn token(&self, prefix: &'a str, lexeme: &'a str, kind: TokenKind) -> Token<'a> {
Token { Token {
index: self.index, index: self.index,
line: self.line, line: self.line,
column: self.column, column: self.column,
text: self.text, text: self.text,
prefix, prefix,
lexeme, lexeme,
kind, kind,
@ -80,19 +80,21 @@ impl<'a> Lexer<'a> {
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]"); static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]");
} }
let indentation = INDENT.captures(self.rest).map(|captures| captures.get(1).unwrap().as_str()); let indentation = INDENT
.captures(self.rest)
.map(|captures| captures.get(1).unwrap().as_str());
if self.column == 0 { if self.column == 0 {
if let Some(kind) = match (self.state.last().unwrap(), indentation) { if let Some(kind) = match (self.state.last().unwrap(), indentation) {
// ignore: was no indentation and there still isn't // ignore: was no indentation and there still isn't
// or current line is blank // or current line is blank
(&State::Start, Some("")) | (_, None) => { (&State::Start, Some("")) | (_, None) => None,
None
}
// indent: was no indentation, now there is // indent: was no indentation, now there is
(&State::Start, Some(current)) => { (&State::Start, Some(current)) => {
if mixed_whitespace(current) { if mixed_whitespace(current) {
return Err(self.error(MixedLeadingWhitespace{whitespace: current})); return Err(self.error(MixedLeadingWhitespace {
whitespace: current,
}));
} }
//indent = Some(current); //indent = Some(current);
self.state.push(State::Indent(current)); self.state.push(State::Indent(current));
@ -107,9 +109,9 @@ impl<'a> Lexer<'a> {
// was indentation and still is, check if the new indentation matches // was indentation and still is, check if the new indentation matches
(&State::Indent(previous), Some(current)) => { (&State::Indent(previous), Some(current)) => {
if !current.starts_with(previous) { if !current.starts_with(previous) {
return Err(self.error(InconsistentLeadingWhitespace{ return Err(self.error(InconsistentLeadingWhitespace {
expected: previous, expected: previous,
found: current found: current,
})); }));
} }
None None
@ -117,7 +119,7 @@ impl<'a> Lexer<'a> {
// at column 0 in some other state: this should never happen // at column 0 in some other state: this should never happen
(&State::Text, _) | (&State::Interpolation, _) => { (&State::Text, _) | (&State::Interpolation, _) => {
return Err(self.error(Internal { return Err(self.error(Internal {
message: "unexpected state at column 0".to_string() message: "unexpected state at column 0".to_string(),
})); }));
} }
} { } {
@ -129,27 +131,27 @@ impl<'a> Lexer<'a> {
pub fn inner(mut self) -> CompilationResult<'a, Vec<Token<'a>>> { pub fn inner(mut self) -> CompilationResult<'a, Vec<Token<'a>>> {
lazy_static! { lazy_static! {
static ref AT: Regex = token(r"@" ); static ref AT: Regex = token(r"@");
static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" ); static ref BACKTICK: Regex = token(r"`[^`\n\r]*`");
static ref COLON: Regex = token(r":" ); static ref COLON: Regex = token(r":");
static ref COMMA: Regex = token(r"," ); static ref COMMA: Regex = token(r",");
static ref COMMENT: Regex = token(r"#([^!\n\r][^\n\r]*)?\r?$" ); static ref COMMENT: Regex = token(r"#([^!\n\r][^\n\r]*)?\r?$");
static ref EOF: Regex = token(r"\z" ); static ref EOF: Regex = token(r"\z");
static ref EOL: Regex = token(r"\n|\r\n" ); static ref EOL: Regex = token(r"\n|\r\n");
static ref EQUALS: Regex = token(r"=" ); static ref EQUALS: Regex = token(r"=");
static ref INTERPOLATION_END: Regex = token(r"[}][}]" ); static ref INTERPOLATION_END: Regex = token(r"[}][}]");
static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]" ); static ref INTERPOLATION_START_TOKEN: Regex = token(r"[{][{]");
static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)" ); static ref NAME: Regex = token(r"([a-zA-Z_][a-zA-Z0-9_-]*)");
static ref PAREN_L: Regex = token(r"[(]" ); static ref PAREN_L: Regex = token(r"[(]");
static ref PAREN_R: Regex = token(r"[)]" ); static ref PAREN_R: Regex = token(r"[)]");
static ref PLUS: Regex = token(r"[+]" ); static ref PLUS: Regex = token(r"[+]");
static ref RAW_STRING: Regex = token(r#"'[^']*'"# ); static ref RAW_STRING: Regex = token(r#"'[^']*'"#);
static ref STRING: Regex = token(r#"["]"# ); static ref STRING: Regex = token(r#"["]"#);
static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# ); static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"#);
static ref INTERPOLATION_START: Regex = re(r"^[{][{]" ); static ref INTERPOLATION_START: Regex = re(r"^[{][{]");
static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]" ); static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]");
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$"); static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
static ref TEXT: Regex = re(r"^(?m)(.+)" ); static ref TEXT: Regex = re(r"^(?m)(.+)");
} }
loop { loop {
@ -163,17 +165,25 @@ impl<'a> Lexer<'a> {
self.tokens.push(token); self.tokens.push(token);
} }
let (prefix, lexeme, kind) = let (prefix, lexeme, kind) = if let (0, &State::Indent(indent), Some(captures)) = (
if let (0, &State::Indent(indent), Some(captures)) = self.column,
(self.column, self.state.last().unwrap(), LINE.captures(self.rest)) { self.state.last().unwrap(),
LINE.captures(self.rest),
) {
let line = captures.get(0).unwrap().as_str(); let line = captures.get(0).unwrap().as_str();
if !line.starts_with(indent) { if !line.starts_with(indent) {
return Err(self.error(Internal{message: "unexpected indent".to_string()})); return Err(self.error(Internal {
message: "unexpected indent".to_string(),
}));
} }
self.state.push(State::Text); self.state.push(State::Text);
(&line[0..indent.len()], "", Line) (&line[0..indent.len()], "", Line)
} else if let Some(captures) = EOF.captures(self.rest) { } else if let Some(captures) = EOF.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eof) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Eof,
)
} else if let State::Text = *self.state.last().unwrap() { } else if let State::Text = *self.state.last().unwrap() {
if let Some(captures) = INTERPOLATION_START.captures(self.rest) { if let Some(captures) = INTERPOLATION_START.captures(self.rest) {
self.state.push(State::Interpolation); self.state.push(State::Interpolation);
@ -184,51 +194,111 @@ impl<'a> Lexer<'a> {
("", captures.get(1).unwrap().as_str(), Text) ("", captures.get(1).unwrap().as_str(), Text)
} else if let Some(captures) = EOL.captures(self.rest) { } else if let Some(captures) = EOL.captures(self.rest) {
self.state.pop(); self.state.pop();
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Eol,
)
} else { } else {
return Err(self.error(Internal { return Err(self.error(Internal {
message: format!("Could not match token in text state: \"{}\"", self.rest) message: format!("Could not match token in text state: \"{}\"", self.rest),
})); }));
} }
} else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(self.rest) { } else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationStart) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
InterpolationStart,
)
} else if let Some(captures) = INTERPOLATION_END.captures(self.rest) { } else if let Some(captures) = INTERPOLATION_END.captures(self.rest) {
if self.state.last().unwrap() == &State::Interpolation { if self.state.last().unwrap() == &State::Interpolation {
self.state.pop(); self.state.pop();
} }
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationEnd) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
InterpolationEnd,
)
} else if let Some(captures) = NAME.captures(self.rest) { } else if let Some(captures) = NAME.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Name) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Name,
)
} else if let Some(captures) = EOL.captures(self.rest) { } else if let Some(captures) = EOL.captures(self.rest) {
if self.state.last().unwrap() == &State::Interpolation { if self.state.last().unwrap() == &State::Interpolation {
return Err(self.error(UnterminatedInterpolation)); return Err(self.error(UnterminatedInterpolation));
} }
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Eol,
)
} else if let Some(captures) = BACKTICK.captures(self.rest) { } else if let Some(captures) = BACKTICK.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Backtick) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Backtick,
)
} else if let Some(captures) = COLON.captures(self.rest) { } else if let Some(captures) = COLON.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Colon,
)
} else if let Some(captures) = AT.captures(self.rest) { } else if let Some(captures) = AT.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
At,
)
} else if let Some(captures) = COMMA.captures(self.rest) { } else if let Some(captures) = COMMA.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comma) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Comma,
)
} else if let Some(captures) = PAREN_L.captures(self.rest) { } else if let Some(captures) = PAREN_L.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenL) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
ParenL,
)
} else if let Some(captures) = PAREN_R.captures(self.rest) { } else if let Some(captures) = PAREN_R.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), ParenR) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
ParenR,
)
} else if let Some(captures) = PLUS.captures(self.rest) { } else if let Some(captures) = PLUS.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Plus,
)
} else if let Some(captures) = EQUALS.captures(self.rest) { } else if let Some(captures) = EQUALS.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Equals) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Equals,
)
} else if let Some(captures) = COMMENT.captures(self.rest) { } else if let Some(captures) = COMMENT.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comment) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
Comment,
)
} else if let Some(captures) = RAW_STRING.captures(self.rest) { } else if let Some(captures) = RAW_STRING.captures(self.rest) {
(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), RawString) (
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
RawString,
)
} else if UNTERMINATED_RAW_STRING.is_match(self.rest) { } else if UNTERMINATED_RAW_STRING.is_match(self.rest) {
return Err(self.error(UnterminatedString)); return Err(self.error(UnterminatedString));
} else if let Some(captures) = STRING.captures(self.rest) { } else if let Some(captures) = STRING.captures(self.rest) {
let prefix = captures.get(1).unwrap().as_str(); let prefix = captures.get(1).unwrap().as_str();
let contents = &self.rest[prefix.len()+1..]; let contents = &self.rest[prefix.len() + 1..];
if contents.is_empty() { if contents.is_empty() {
return Err(self.error(UnterminatedString)); return Err(self.error(UnterminatedString));
} }
@ -266,10 +336,12 @@ impl<'a> Lexer<'a> {
if len == 0 { if len == 0 {
let last = self.tokens.last().unwrap(); let last = self.tokens.last().unwrap();
match last.kind { match last.kind {
Eof => {}, Eof => {}
_ => return Err(last.error(Internal { _ => {
message: format!("zero length token: {:?}", last) return Err(last.error(Internal {
})), message: format!("zero length token: {:?}", last),
}))
}
} }
} }
@ -314,46 +386,55 @@ mod test {
let input = $input; let input = $input;
let expected = $expected; let expected = $expected;
let tokens = ::Lexer::lex(input).unwrap(); let tokens = ::Lexer::lex(input).unwrap();
let roundtrip = tokens.iter().map(|t| { let roundtrip = tokens
let mut s = String::new(); .iter()
s += t.prefix; .map(|t| {
s += t.lexeme; let mut s = String::new();
s s += t.prefix;
}).collect::<Vec<_>>().join(""); s += t.lexeme;
s
})
.collect::<Vec<_>>()
.join("");
let actual = token_summary(&tokens); let actual = token_summary(&tokens);
if actual != expected { if actual != expected {
panic!("token summary mismatch:\nexpected: {}\ngot: {}\n", expected, actual); panic!(
"token summary mismatch:\nexpected: {}\ngot: {}\n",
expected, actual
);
} }
assert_eq!(input, roundtrip); assert_eq!(input, roundtrip);
} }
} };
} }
fn token_summary(tokens: &[Token]) -> String { fn token_summary(tokens: &[Token]) -> String {
tokens.iter().map(|t| { tokens
match t.kind { .iter()
At => "@", .map(|t| match t.kind {
Backtick => "`", At => "@",
Colon => ":", Backtick => "`",
Comma => ",", Colon => ":",
Comment{..} => "#", Comma => ",",
Dedent => "<", Comment { .. } => "#",
Eof => ".", Dedent => "<",
Eol => "$", Eof => ".",
Equals => "=", Eol => "$",
Indent{..} => ">", Equals => "=",
InterpolationEnd => "}", Indent { .. } => ">",
InterpolationEnd => "}",
InterpolationStart => "{", InterpolationStart => "{",
Line{..} => "^", Line { .. } => "^",
Name => "N", Name => "N",
ParenL => "(", ParenL => "(",
ParenR => ")", ParenR => ")",
Plus => "+", Plus => "+",
RawString => "'", RawString => "'",
StringToken => "\"", StringToken => "\"",
Text => "_", Text => "_",
} })
}).collect::<Vec<_>>().join("") .collect::<Vec<_>>()
.join("")
} }
macro_rules! error_test { macro_rules! error_test {
@ -371,26 +452,26 @@ mod test {
let input = $input; let input = $input;
let expected = CompilationError { let expected = CompilationError {
text: input, text: input,
index: $index, index: $index,
line: $line, line: $line,
column: $column, column: $column,
width: $width, width: $width,
kind: $kind, kind: $kind,
}; };
if let Err(error) = Lexer::lex(input) { if let Err(error) = Lexer::lex(input) {
assert_eq!(error.text, expected.text); assert_eq!(error.text, expected.text);
assert_eq!(error.index, expected.index); assert_eq!(error.index, expected.index);
assert_eq!(error.line, expected.line); assert_eq!(error.line, expected.line);
assert_eq!(error.column, expected.column); assert_eq!(error.column, expected.column);
assert_eq!(error.kind, expected.kind); assert_eq!(error.kind, expected.kind);
assert_eq!(error, expected); assert_eq!(error, expected);
} else { } else {
panic!("tokenize succeeded but expected: {}\n{}", expected, input); panic!("tokenize succeeded but expected: {}\n{}", expected, input);
} }
} }
} };
} }
summary_test! { summary_test! {

View File

@ -6,12 +6,14 @@ pub fn load_dotenv() -> RunResult<'static, Map<String, String>> {
match dotenv::dotenv_iter() { match dotenv::dotenv_iter() {
Ok(iter) => { Ok(iter) => {
let result: dotenv::Result<Map<String, String>> = iter.collect(); let result: dotenv::Result<Map<String, String>> = iter.collect();
result.map_err(|dotenv_error| RuntimeError::Dotenv{dotenv_error}) result.map_err(|dotenv_error| RuntimeError::Dotenv { dotenv_error })
} }
Err(dotenv_error) => if dotenv_error.not_found() { Err(dotenv_error) => {
Ok(Map::new()) if dotenv_error.not_found() {
} else { Ok(Map::new())
Err(RuntimeError::Dotenv{dotenv_error}) } else {
Err(RuntimeError::Dotenv { dotenv_error })
}
} }
} }
} }

View File

@ -3,7 +3,14 @@ use common::*;
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
pub fn show_whitespace(text: &str) -> String { pub fn show_whitespace(text: &str) -> String {
text.chars().map(|c| match c { '\t' => '␉', ' ' => '␠', _ => c }).collect() text
.chars()
.map(|c| match c {
'\t' => '␉',
' ' => '␠',
_ => c,
})
.collect()
} }
pub fn default<T: Default>() -> T { pub fn default<T: Default>() -> T {
@ -27,15 +34,16 @@ pub fn maybe_s(n: usize) -> &'static str {
} }
pub fn conjoin<T: Display>( pub fn conjoin<T: Display>(
f: &mut fmt::Formatter, f: &mut fmt::Formatter,
values: &[T], values: &[T],
conjunction: &str, conjunction: &str,
) -> Result<(), fmt::Error> { ) -> Result<(), fmt::Error> {
match values.len() { match values.len() {
0 => {}, 0 => {}
1 => write!(f, "{}", values[0])?, 1 => write!(f, "{}", values[0])?,
2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?, 2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?,
_ => for (i, item) in values.iter().enumerate() { _ => {
for (i, item) in values.iter().enumerate() {
write!(f, "{}", item)?; write!(f, "{}", item)?;
if i == values.len() - 1 { if i == values.len() - 1 {
} else if i == values.len() - 2 { } else if i == values.len() - 2 {
@ -43,18 +51,19 @@ pub fn conjoin<T: Display>(
} else { } else {
write!(f, ", ")? write!(f, ", ")?
} }
}, }
} }
Ok(()) }
Ok(())
} }
pub fn write_error_context( pub fn write_error_context(
f: &mut fmt::Formatter, f: &mut fmt::Formatter,
text: &str, text: &str,
index: usize, index: usize,
line: usize, line: usize,
column: usize, column: usize,
width: Option<usize>, width: Option<usize>,
) -> Result<(), fmt::Error> { ) -> Result<(), fmt::Error> {
let line_number = line + 1; let line_number = line + 1;
let red = Color::fmt(f).error(); let red = Color::fmt(f).error();
@ -62,8 +71,8 @@ pub fn write_error_context(
Some(line) => { Some(line) => {
let mut i = 0; let mut i = 0;
let mut space_column = 0; let mut space_column = 0;
let mut space_line = String::new(); let mut space_line = String::new();
let mut space_width = 0; let mut space_width = 0;
for c in line.chars() { for c in line.chars() {
if c == '\t' { if c == '\t' {
space_line.push_str(" "); space_line.push_str(" ");
@ -76,7 +85,6 @@ pub fn write_error_context(
} else { } else {
if i < column { if i < column {
space_column += UnicodeWidthChar::width(c).unwrap_or(0); space_column += UnicodeWidthChar::width(c).unwrap_or(0);
} }
if i >= column && i < column + width.unwrap_or(1) { if i >= column && i < column + width.unwrap_or(1) {
space_width += UnicodeWidthChar::width(c).unwrap_or(0); space_width += UnicodeWidthChar::width(c).unwrap_or(0);
@ -90,15 +98,36 @@ pub fn write_error_context(
writeln!(f, "{} | {}", line_number, space_line)?; writeln!(f, "{} | {}", line_number, space_line)?;
write!(f, "{0:1$} |", "", line_number_width)?; write!(f, "{0:1$} |", "", line_number_width)?;
if width == None { if width == None {
write!(f, " {0:1$}{2}^{3}", "", space_column, red.prefix(), red.suffix())?; write!(
f,
" {0:1$}{2}^{3}",
"",
space_column,
red.prefix(),
red.suffix()
)?;
} else { } else {
write!(f, " {0:1$}{2}{3:^<4$}{5}", "", space_column, write!(
red.prefix(), "", space_width, red.suffix())?; f,
" {0:1$}{2}{3:^<4$}{5}",
"",
space_column,
red.prefix(),
"",
space_width,
red.suffix()
)?;
} }
}, }
None => if index != text.len() { None => {
write!(f, "internal error: Error has invalid line number: {}", line_number)? if index != text.len() {
}, write!(
f,
"internal error: Error has invalid line number: {}",
line_number
)?
}
}
} }
Ok(()) Ok(())
} }
@ -111,7 +140,7 @@ impl<'a, T: Display> Display for And<'a, T> {
} }
} }
pub struct Or <'a, T: 'a + Display>(pub &'a [T]); pub struct Or<'a, T: 'a + Display>(pub &'a [T]);
impl<'a, T: Display> Display for Or<'a, T> { impl<'a, T: Display> Display for Or<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
@ -133,17 +162,17 @@ mod test {
#[test] #[test]
fn conjoin_or() { fn conjoin_or() {
assert_eq!("1", Or(&[1 ]).to_string()); assert_eq!("1", Or(&[1]).to_string());
assert_eq!("1 or 2", Or(&[1,2 ]).to_string()); assert_eq!("1 or 2", Or(&[1, 2]).to_string());
assert_eq!("1, 2, or 3", Or(&[1,2,3 ]).to_string()); assert_eq!("1, 2, or 3", Or(&[1, 2, 3]).to_string());
assert_eq!("1, 2, 3, or 4", Or(&[1,2,3,4]).to_string()); assert_eq!("1, 2, 3, or 4", Or(&[1, 2, 3, 4]).to_string());
} }
#[test] #[test]
fn conjoin_and() { fn conjoin_and() {
assert_eq!("1", And(&[1 ]).to_string()); assert_eq!("1", And(&[1]).to_string());
assert_eq!("1 and 2", And(&[1,2 ]).to_string()); assert_eq!("1 and 2", And(&[1, 2]).to_string());
assert_eq!("1, 2, and 3", And(&[1,2,3 ]).to_string()); assert_eq!("1, 2, and 3", And(&[1, 2, 3]).to_string());
assert_eq!("1, 2, 3, and 4", And(&[1,2,3,4]).to_string()); assert_eq!("1, 2, 3, and 4", And(&[1, 2, 3, 4]).to_string());
} }
} }

View File

@ -2,9 +2,9 @@ use common::*;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Parameter<'a> { pub struct Parameter<'a> {
pub default: Option<String>, pub default: Option<String>,
pub name: &'a str, pub name: &'a str,
pub token: Token<'a>, pub token: Token<'a>,
pub variadic: bool, pub variadic: bool,
} }
@ -16,7 +16,10 @@ impl<'a> Display for Parameter<'a> {
} }
write!(f, "{}", color.parameter().paint(self.name))?; write!(f, "{}", color.parameter().paint(self.name))?;
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
let escaped = default.chars().flat_map(char::escape_default).collect::<String>();; let escaped = default
.chars()
.flat_map(char::escape_default)
.collect::<String>();;
write!(f, r#"='{}'"#, color.string().paint(&escaped))?; write!(f, r#"='{}'"#, color.string().paint(&escaped))?;
} }
Ok(()) Ok(())

View File

@ -1,16 +1,16 @@
use common::*; use common::*;
use itertools; use itertools;
use TokenKind::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
use TokenKind::*;
pub struct Parser<'a> { pub struct Parser<'a> {
text: &'a str, text: &'a str,
tokens: itertools::PutBackN<vec::IntoIter<Token<'a>>>, tokens: itertools::PutBackN<vec::IntoIter<Token<'a>>>,
recipes: Map<&'a str, Recipe<'a>>, recipes: Map<&'a str, Recipe<'a>>,
assignments: Map<&'a str, Expression<'a>>, assignments: Map<&'a str, Expression<'a>>,
assignment_tokens: Map<&'a str, Token<'a>>, assignment_tokens: Map<&'a str, Token<'a>>,
exports: Set<&'a str>, exports: Set<&'a str>,
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
@ -22,11 +22,11 @@ impl<'a> Parser<'a> {
pub fn new(text: &'a str, tokens: Vec<Token<'a>>) -> Parser<'a> { pub fn new(text: &'a str, tokens: Vec<Token<'a>>) -> Parser<'a> {
Parser { Parser {
tokens: itertools::put_back_n(tokens), tokens: itertools::put_back_n(tokens),
recipes: empty(), recipes: empty(),
assignments: empty(), assignments: empty(),
assignment_tokens: empty(), assignment_tokens: empty(),
exports: empty(), exports: empty(),
text, text,
} }
} }
@ -83,20 +83,20 @@ impl<'a> Parser<'a> {
fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> { fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> {
found.error(UnexpectedToken { found.error(UnexpectedToken {
expected: expected.to_vec(), expected: expected.to_vec(),
found: found.kind, found: found.kind,
}) })
} }
fn recipe( fn recipe(
&mut self, &mut self,
name: &Token<'a>, name: &Token<'a>,
doc: Option<Token<'a>>, doc: Option<Token<'a>>,
quiet: bool, quiet: bool,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'a, ()> {
if let Some(recipe) = self.recipes.get(name.lexeme) { if let Some(recipe) = self.recipes.get(name.lexeme) {
return Err(name.error(DuplicateRecipe { return Err(name.error(DuplicateRecipe {
recipe: recipe.name, recipe: recipe.name,
first: recipe.line_number first: recipe.line_number,
})); }));
} }
@ -108,11 +108,13 @@ impl<'a> Parser<'a> {
let parameter = match self.accept(Name) { let parameter = match self.accept(Name) {
Some(parameter) => parameter, Some(parameter) => parameter,
None => if let Some(plus) = plus { None => {
return Err(self.unexpected_token(&plus, &[Name])); if let Some(plus) = plus {
} else { return Err(self.unexpected_token(&plus, &[Name]));
break } else {
}, break;
}
}
}; };
let variadic = plus.is_some(); let variadic = plus.is_some();
@ -125,7 +127,8 @@ impl<'a> Parser<'a> {
if parameters.iter().any(|p| p.name == parameter.lexeme) { if parameters.iter().any(|p| p.name == parameter.lexeme) {
return Err(parameter.error(DuplicateParameter { return Err(parameter.error(DuplicateParameter {
recipe: name.lexeme, parameter: parameter.lexeme recipe: name.lexeme,
parameter: parameter.lexeme,
})); }));
} }
@ -142,7 +145,7 @@ impl<'a> Parser<'a> {
} }
if parsed_parameter_with_default && default.is_none() { if parsed_parameter_with_default && default.is_none() {
return Err(parameter.error(RequiredParameterFollowsDefaultParameter{ return Err(parameter.error(RequiredParameterFollowsDefaultParameter {
parameter: parameter.lexeme, parameter: parameter.lexeme,
})); }));
} }
@ -151,8 +154,8 @@ impl<'a> Parser<'a> {
parsed_variadic_parameter = variadic; parsed_variadic_parameter = variadic;
parameters.push(Parameter { parameters.push(Parameter {
name: parameter.lexeme, name: parameter.lexeme,
token: parameter, token: parameter,
default, default,
variadic, variadic,
}); });
@ -173,8 +176,8 @@ impl<'a> Parser<'a> {
while let Some(dependency) = self.accept(Name) { while let Some(dependency) = self.accept(Name) {
if dependencies.contains(&dependency.lexeme) { if dependencies.contains(&dependency.lexeme) {
return Err(dependency.error(DuplicateDependency { return Err(dependency.error(DuplicateDependency {
recipe: name.lexeme, recipe: name.lexeme,
dependency: dependency.lexeme dependency: dependency.lexeme,
})); }));
} }
dependencies.push(dependency.lexeme); dependencies.push(dependency.lexeme);
@ -195,9 +198,9 @@ impl<'a> Parser<'a> {
continue; continue;
} }
if let Some(token) = self.expect(Line) { if let Some(token) = self.expect(Line) {
return Err(token.error(Internal{ return Err(token.error(Internal {
message: format!("Expected a line but got {}", token.kind) message: format!("Expected a line but got {}", token.kind),
})) }));
} }
let mut fragments = vec![]; let mut fragments = vec![];
@ -209,18 +212,22 @@ impl<'a> Parser<'a> {
shebang = true; shebang = true;
} }
} else if !shebang } else if !shebang
&& !lines.last().and_then(|line| line.last()) && !lines
.map(Fragment::continuation).unwrap_or(false) .last()
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) { .and_then(|line| line.last())
.map(Fragment::continuation)
.unwrap_or(false)
&& (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t'))
{
return Err(token.error(ExtraLeadingWhitespace)); return Err(token.error(ExtraLeadingWhitespace));
} }
} }
fragments.push(Fragment::Text{text: token}); fragments.push(Fragment::Text { text: token });
} else if let Some(token) = self.expect(InterpolationStart) { } else if let Some(token) = self.expect(InterpolationStart) {
return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol])); return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol]));
} else { } else {
fragments.push(Fragment::Expression{ fragments.push(Fragment::Expression {
expression: self.expression()? expression: self.expression()?,
}); });
if let Some(token) = self.expect(InterpolationEnd) { if let Some(token) = self.expect(InterpolationEnd) {
@ -233,18 +240,21 @@ impl<'a> Parser<'a> {
} }
} }
self.recipes.insert(name.lexeme, Recipe { self.recipes.insert(
line_number: name.line, name.lexeme,
name: name.lexeme, Recipe {
doc: doc.map(|t| t.lexeme[1..].trim()), line_number: name.line,
private: &name.lexeme[0..1] == "_", name: name.lexeme,
dependencies, doc: doc.map(|t| t.lexeme[1..].trim()),
dependency_tokens, private: &name.lexeme[0..1] == "_",
lines, dependencies,
parameters, dependency_tokens,
quiet, lines,
shebang, parameters,
}); quiet,
shebang,
},
);
Ok(()) Ok(())
} }
@ -261,24 +271,34 @@ impl<'a> Parser<'a> {
if let Some(token) = self.expect(ParenR) { if let Some(token) = self.expect(ParenR) {
return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR])); return Err(self.unexpected_token(&token, &[Name, StringToken, ParenR]));
} }
Expression::Call {name: first.lexeme, token: first, arguments} Expression::Call {
name: first.lexeme,
token: first,
arguments,
}
} else { } else {
Expression::Variable {name: first.lexeme, token: first} Expression::Variable {
name: first.lexeme,
token: first,
}
} }
} }
Backtick => Expression::Backtick { Backtick => Expression::Backtick {
raw: &first.lexeme[1..first.lexeme.len()-1], raw: &first.lexeme[1..first.lexeme.len() - 1],
token: first token: first,
},
RawString | StringToken => Expression::String {
cooked_string: CookedString::new(&first)?,
}, },
RawString | StringToken => {
Expression::String{cooked_string: CookedString::new(&first)?}
}
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])), _ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
}; };
if self.accepted(Plus) { if self.accepted(Plus) {
let rhs = self.expression()?; let rhs = self.expression()?;
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)}) Ok(Expression::Concatination {
lhs: Box::new(lhs),
rhs: Box::new(rhs),
})
} else { } else {
Ok(lhs) Ok(lhs)
} }
@ -304,7 +324,9 @@ impl<'a> Parser<'a> {
fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> { fn assignment(&mut self, name: Token<'a>, export: bool) -> CompilationResult<'a, ()> {
if self.assignments.contains_key(name.lexeme) { if self.assignments.contains_key(name.lexeme) {
return Err(name.error(DuplicateVariable {variable: name.lexeme})); return Err(name.error(DuplicateVariable {
variable: name.lexeme,
}));
} }
if export { if export {
self.exports.insert(name.lexeme); self.exports.insert(name.lexeme);
@ -338,49 +360,58 @@ impl<'a> Parser<'a> {
} }
doc = Some(token); doc = Some(token);
} }
At => if let Some(name) = self.accept(Name) { At => {
self.recipe(&name, doc, true)?; if let Some(name) = self.accept(Name) {
doc = None; self.recipe(&name, doc, true)?;
} else { doc = None;
let unexpected = &self.tokens.next().unwrap(); } else {
return Err(self.unexpected_token(unexpected, &[Name])); let unexpected = &self.tokens.next().unwrap();
}, return Err(self.unexpected_token(unexpected, &[Name]));
Name => if token.lexeme == "export" { }
let next = self.tokens.next().unwrap(); }
if next.kind == Name && self.accepted(Equals) { Name => {
self.assignment(next, true)?; if token.lexeme == "export" {
let next = self.tokens.next().unwrap();
if next.kind == Name && self.accepted(Equals) {
self.assignment(next, true)?;
doc = None;
} else {
self.tokens.put_back(next);
self.recipe(&token, doc, false)?;
doc = None;
}
} else if self.accepted(Equals) {
self.assignment(token, false)?;
doc = None; doc = None;
} else { } else {
self.tokens.put_back(next);
self.recipe(&token, doc, false)?; self.recipe(&token, doc, false)?;
doc = None; doc = None;
} }
} else if self.accepted(Equals) { }
self.assignment(token, false)?;
doc = None;
} else {
self.recipe(&token, doc, false)?;
doc = None;
},
_ => return Err(self.unexpected_token(&token, &[Name, At])), _ => return Err(self.unexpected_token(&token, &[Name, At])),
}, },
None => return Err(CompilationError { None => {
text: self.text, return Err(CompilationError {
index: 0, text: self.text,
line: 0, index: 0,
column: 0, line: 0,
width: None, column: 0,
kind: Internal { width: None,
message: "unexpected end of token stream".to_string() kind: Internal {
} message: "unexpected end of token stream".to_string(),
}), },
})
}
} }
} }
if let Some(token) = self.tokens.next() { if let Some(token) = self.tokens.next() {
return Err(token.error(Internal { return Err(token.error(Internal {
message: format!("unexpected token remaining after parsing completed: {:?}", token.kind) message: format!(
})) "unexpected token remaining after parsing completed: {:?}",
token.kind
),
}));
} }
RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?; RecipeResolver::resolve_recipes(&self.recipes, &self.assignments, self.text)?;
@ -389,7 +420,7 @@ impl<'a> Parser<'a> {
for parameter in &recipe.parameters { for parameter in &recipe.parameters {
if self.assignments.contains_key(parameter.token.lexeme) { if self.assignments.contains_key(parameter.token.lexeme) {
return Err(parameter.token.error(ParameterShadowsVariable { return Err(parameter.token.error(ParameterShadowsVariable {
parameter: parameter.token.lexeme parameter: parameter.token.lexeme,
})); }));
} }
} }
@ -407,9 +438,9 @@ impl<'a> Parser<'a> {
AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?; AssignmentResolver::resolve_assignments(&self.assignments, &self.assignment_tokens)?;
Ok(Justfile { Ok(Justfile {
recipes: self.recipes, recipes: self.recipes,
assignments: self.assignments, assignments: self.assignments,
exports: self.exports, exports: self.exports,
}) })
} }
} }
@ -434,7 +465,7 @@ mod test {
assert_eq!(actual, expected); assert_eq!(actual, expected);
} }
} }
} };
} }
summary_test! { summary_test! {
@ -502,7 +533,7 @@ export a = "hello"
} }
summary_test! { summary_test! {
parse_complex, parse_complex,
" "
x: x:
y: y:
@ -540,7 +571,7 @@ z:"
} }
summary_test! { summary_test! {
parse_shebang, parse_shebang,
" "
practicum = 'hello' practicum = 'hello'
install: install:
@ -565,7 +596,7 @@ install:
} }
summary_test! { summary_test! {
parse_assignments, parse_assignments,
r#"a = "0" r#"a = "0"
c = a + b + a + b c = a + b + a + b
b = "1" b = "1"
@ -578,7 +609,7 @@ c = a + b + a + b"#,
} }
summary_test! { summary_test! {
parse_assignment_backticks, parse_assignment_backticks,
"a = `echo hello` "a = `echo hello`
c = a + b + a + b c = a + b + a + b
b = `echo goodbye`", b = `echo goodbye`",
@ -590,7 +621,7 @@ c = a + b + a + b",
} }
summary_test! { summary_test! {
parse_interpolation_backticks, parse_interpolation_backticks,
r#"a: r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
r#"a: r#"a:
@ -616,7 +647,7 @@ c = a + b + a + b",
} }
summary_test! { summary_test! {
parameters, parameters,
"a b c: "a b c:
{{b}} {{c}}", {{b}} {{c}}",
"a b c: "a b c:
@ -624,7 +655,7 @@ c = a + b + a + b",
} }
summary_test! { summary_test! {
unary_functions, unary_functions,
" "
x = arch() x = arch()
@ -637,7 +668,7 @@ a:
} }
summary_test! { summary_test! {
env_functions, env_functions,
r#" r#"
x = env_var('foo',) x = env_var('foo',)

View File

@ -7,8 +7,11 @@ pub struct Platform;
pub trait PlatformInterface { pub trait PlatformInterface {
/// Construct a command equivelant to running the script at `path` with the /// Construct a command equivelant to running the script at `path` with the
/// shebang line `shebang` /// shebang line `shebang`
fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) fn make_shebang_command(
-> Result<Command, brev::OutputError>; path: &Path,
command: &str,
argument: Option<&str>,
) -> Result<Command, brev::OutputError>;
/// Set the execute permission on the file pointed to by `path` /// Set the execute permission on the file pointed to by `path`
fn set_execute_permission(path: &Path) -> Result<(), io::Error>; fn set_execute_permission(path: &Path) -> Result<(), io::Error>;
@ -20,12 +23,13 @@ pub trait PlatformInterface {
fn to_shell_path(path: &Path) -> Result<String, String>; fn to_shell_path(path: &Path) -> Result<String, String>;
} }
#[cfg(unix)] #[cfg(unix)]
impl PlatformInterface for Platform { impl PlatformInterface for Platform {
fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>) fn make_shebang_command(
-> Result<Command, brev::OutputError> path: &Path,
{ _command: &str,
_argument: Option<&str>,
) -> Result<Command, brev::OutputError> {
// shebang scripts can be executed directly on unix // shebang scripts can be executed directly on unix
Ok(Command::new(path)) Ok(Command::new(path))
} }
@ -50,17 +54,20 @@ impl PlatformInterface for Platform {
} }
fn to_shell_path(path: &Path) -> Result<String, String> { fn to_shell_path(path: &Path) -> Result<String, String> {
path.to_str().map(str::to_string) path
.ok_or_else(|| String::from( .to_str()
"Error getting current directory: unicode decode error")) .map(str::to_string)
.ok_or_else(|| String::from("Error getting current directory: unicode decode error"))
} }
} }
#[cfg(windows)] #[cfg(windows)]
impl PlatformInterface for Platform { impl PlatformInterface for Platform {
fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) fn make_shebang_command(
-> Result<Command, brev::OutputError> path: &Path,
{ command: &str,
argument: Option<&str>,
) -> Result<Command, brev::OutputError> {
// Translate path to the interpreter from unix style to windows style // Translate path to the interpreter from unix style to windows style
let mut cygpath = Command::new("cygpath"); let mut cygpath = Command::new("cygpath");
cygpath.arg("--windows"); cygpath.arg("--windows");

View File

@ -4,8 +4,11 @@ pub trait RangeExt<T> {
fn range_contains(&self, i: T) -> bool; fn range_contains(&self, i: T) -> bool;
} }
impl<T> RangeExt<T> for Range<T> where T: PartialOrd + Copy { impl<T> RangeExt<T> for Range<T>
fn range_contains(&self, i: T) -> bool { where
T: PartialOrd + Copy,
{
fn range_contains(&self, i: T) -> bool {
i >= self.start && i < self.end i >= self.start && i < self.end
} }
} }
@ -16,10 +19,10 @@ mod test {
#[test] #[test]
fn range() { fn range() {
assert!( ( 0.. 1).range_contains( 0)); assert!((0..1).range_contains(0));
assert!( (10..20).range_contains(15)); assert!((10..20).range_contains(15));
assert!(!( 0.. 0).range_contains( 0)); assert!(!(0..0).range_contains(0));
assert!(!( 1..10).range_contains( 0)); assert!(!(1..10).range_contains(0));
assert!(!( 1..10).range_contains(10)); assert!(!(1..10).range_contains(10));
} }
} }

View File

@ -1,40 +1,47 @@
use common::*; use common::*;
use std::process::{ExitStatus, Command, Stdio}; use std::process::{Command, ExitStatus, Stdio};
use platform::{Platform, PlatformInterface}; use platform::{Platform, PlatformInterface};
/// Return a `RuntimeError::Signal` if the process was terminated by a signal, /// Return a `RuntimeError::Signal` if the process was terminated by a signal,
/// otherwise return an `RuntimeError::UnknownFailure` /// otherwise return an `RuntimeError::UnknownFailure`
fn error_from_signal( fn error_from_signal(
recipe: &str, recipe: &str,
line_number: Option<usize>, line_number: Option<usize>,
exit_status: ExitStatus exit_status: ExitStatus,
) -> RuntimeError { ) -> RuntimeError {
match Platform::signal_from_exit_status(exit_status) { match Platform::signal_from_exit_status(exit_status) {
Some(signal) => RuntimeError::Signal{recipe, line_number, signal}, Some(signal) => RuntimeError::Signal {
None => RuntimeError::Unknown{recipe, line_number}, recipe,
line_number,
signal,
},
None => RuntimeError::Unknown {
recipe,
line_number,
},
} }
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub struct Recipe<'a> { pub struct Recipe<'a> {
pub dependencies: Vec<&'a str>, pub dependencies: Vec<&'a str>,
pub dependency_tokens: Vec<Token<'a>>, pub dependency_tokens: Vec<Token<'a>>,
pub doc: Option<&'a str>, pub doc: Option<&'a str>,
pub line_number: usize, pub line_number: usize,
pub lines: Vec<Vec<Fragment<'a>>>, pub lines: Vec<Vec<Fragment<'a>>>,
pub name: &'a str, pub name: &'a str,
pub parameters: Vec<Parameter<'a>>, pub parameters: Vec<Parameter<'a>>,
pub private: bool, pub private: bool,
pub quiet: bool, pub quiet: bool,
pub shebang: bool, pub shebang: bool,
} }
pub struct RecipeContext<'a> { pub struct RecipeContext<'a> {
pub invocation_directory: &'a Result<PathBuf, String>, pub invocation_directory: &'a Result<PathBuf, String>,
pub configuration: &'a Configuration<'a>, pub configuration: &'a Configuration<'a>,
pub scope: Map<&'a str, String>, pub scope: Map<&'a str, String>,
} }
impl<'a> Recipe<'a> { impl<'a> Recipe<'a> {
@ -43,7 +50,11 @@ impl<'a> Recipe<'a> {
} }
pub fn min_arguments(&self) -> usize { pub fn min_arguments(&self) -> usize {
self.parameters.iter().filter(|p| p.default.is_none()).count() self
.parameters
.iter()
.filter(|p| p.default.is_none())
.count()
} }
pub fn max_arguments(&self) -> usize { pub fn max_arguments(&self) -> usize {
@ -56,16 +67,21 @@ impl<'a> Recipe<'a> {
pub fn run( pub fn run(
&self, &self,
context: &RecipeContext<'a>, context: &RecipeContext<'a>,
arguments: &[&'a str], arguments: &[&'a str],
dotenv: &Map<String, String>, dotenv: &Map<String, String>,
exports: &Set<&'a str>, exports: &Set<&'a str>,
) -> RunResult<'a, ()> { ) -> RunResult<'a, ()> {
let configuration = &context.configuration; let configuration = &context.configuration;
if configuration.verbosity.loquacious() { if configuration.verbosity.loquacious() {
let color = configuration.color.stderr().banner(); let color = configuration.color.stderr().banner();
eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix()); eprintln!(
"{}===> Running recipe `{}`...{}",
color.prefix(),
self.name,
color.suffix()
);
} }
let mut argument_map = Map::new(); let mut argument_map = Map::new();
@ -75,9 +91,11 @@ impl<'a> Recipe<'a> {
let value = if rest.is_empty() { let value = if rest.is_empty() {
match parameter.default { match parameter.default {
Some(ref default) => Cow::Borrowed(default.as_str()), Some(ref default) => Cow::Borrowed(default.as_str()),
None => return Err(RuntimeError::Internal{ None => {
message: "missing parameter without default".to_string() return Err(RuntimeError::Internal {
}), message: "missing parameter without default".to_string(),
})
}
} }
} else if parameter.variadic { } else if parameter.variadic {
let value = Cow::Owned(rest.to_vec().join(" ")); let value = Cow::Owned(rest.to_vec().join(" "));
@ -92,14 +110,14 @@ impl<'a> Recipe<'a> {
} }
let mut evaluator = AssignmentEvaluator { let mut evaluator = AssignmentEvaluator {
assignments: &empty(), assignments: &empty(),
dry_run: configuration.dry_run, dry_run: configuration.dry_run,
evaluated: empty(), evaluated: empty(),
invocation_directory: context.invocation_directory, invocation_directory: context.invocation_directory,
overrides: &empty(), overrides: &empty(),
quiet: configuration.quiet, quiet: configuration.quiet,
scope: &context.scope, scope: &context.scope,
shell: configuration.shell, shell: configuration.shell,
dotenv, dotenv,
exports, exports,
}; };
@ -120,13 +138,17 @@ impl<'a> Recipe<'a> {
return Ok(()); return Ok(());
} }
let tmp = TempDir::new("just") let tmp = TempDir::new("just").map_err(|error| RuntimeError::TmpdirIoError {
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; recipe: self.name,
io_error: error,
})?;
let mut path = tmp.path().to_path_buf(); let mut path = tmp.path().to_path_buf();
path.push(self.name); path.push(self.name);
{ {
let mut f = fs::File::create(&path) let mut f = fs::File::create(&path).map_err(|error| RuntimeError::TmpdirIoError {
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; recipe: self.name,
io_error: error,
})?;
let mut text = String::new(); let mut text = String::new();
// add the shebang // add the shebang
text += &evaluated_lines[0]; text += &evaluated_lines[0];
@ -147,44 +169,65 @@ impl<'a> Recipe<'a> {
} }
f.write_all(text.as_bytes()) f.write_all(text.as_bytes())
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; .map_err(|error| RuntimeError::TmpdirIoError {
recipe: self.name,
io_error: error,
})?;
} }
// make the script executable // make the script executable
Platform::set_execute_permission(&path) Platform::set_execute_permission(&path).map_err(|error| RuntimeError::TmpdirIoError {
.map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; recipe: self.name,
io_error: error,
})?;
let shebang_line = evaluated_lines.first() let shebang_line = evaluated_lines
.first()
.ok_or_else(|| RuntimeError::Internal { .ok_or_else(|| RuntimeError::Internal {
message: "evaluated_lines was empty".to_string() message: "evaluated_lines was empty".to_string(),
})?; })?;
let Shebang{interpreter, argument} = Shebang::new(shebang_line) let Shebang {
.ok_or_else(|| RuntimeError::Internal { interpreter,
message: format!("bad shebang line: {}", shebang_line) argument,
})?; } = Shebang::new(shebang_line).ok_or_else(|| RuntimeError::Internal {
message: format!("bad shebang line: {}", shebang_line),
})?;
// create a command to run the script // create a command to run the script
let mut command = Platform::make_shebang_command(&path, interpreter, argument) let mut command =
.map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error})?; Platform::make_shebang_command(&path, interpreter, argument).map_err(|output_error| {
RuntimeError::Cygpath {
recipe: self.name,
output_error,
}
})?;
command.export_environment_variables(&context.scope, dotenv, exports)?; command.export_environment_variables(&context.scope, dotenv, exports)?;
// run it! // run it!
match InterruptHandler::guard(|| command.status()) { match InterruptHandler::guard(|| command.status()) {
Ok(exit_status) => if let Some(code) = exit_status.code() { Ok(exit_status) => {
if code != 0 { if let Some(code) = exit_status.code() {
return Err(RuntimeError::Code{recipe: self.name, line_number: None, code}) if code != 0 {
return Err(RuntimeError::Code {
recipe: self.name,
line_number: None,
code,
});
}
} else {
return Err(error_from_signal(self.name, None, exit_status));
} }
} else { }
return Err(error_from_signal(self.name, None, exit_status)) Err(io_error) => {
}, return Err(RuntimeError::Shebang {
Err(io_error) => return Err(RuntimeError::Shebang { recipe: self.name,
recipe: self.name, command: interpreter.to_string(),
command: interpreter.to_string(), argument: argument.map(String::from),
argument: argument.map(String::from), io_error,
io_error, })
}) }
}; };
} else { } else {
let mut lines = self.lines.iter().peekable(); let mut lines = self.lines.iter().peekable();
@ -219,8 +262,7 @@ impl<'a> Recipe<'a> {
if configuration.dry_run if configuration.dry_run
|| configuration.verbosity.loquacious() || configuration.verbosity.loquacious()
|| !((quiet_command ^ self.quiet) || !((quiet_command ^ self.quiet) || configuration.quiet)
|| configuration.quiet)
{ {
let color = if configuration.highlight { let color = if configuration.highlight {
configuration.color.command() configuration.color.command()
@ -246,19 +288,25 @@ impl<'a> Recipe<'a> {
cmd.export_environment_variables(&context.scope, dotenv, exports)?; cmd.export_environment_variables(&context.scope, dotenv, exports)?;
match InterruptHandler::guard(|| cmd.status()) { match InterruptHandler::guard(|| cmd.status()) {
Ok(exit_status) => if let Some(code) = exit_status.code() { Ok(exit_status) => {
if code != 0 { if let Some(code) = exit_status.code() {
return Err(RuntimeError::Code{ if code != 0 {
recipe: self.name, line_number: Some(line_number), code, return Err(RuntimeError::Code {
}); recipe: self.name,
line_number: Some(line_number),
code,
});
}
} else {
return Err(error_from_signal(self.name, Some(line_number), exit_status));
} }
} else { }
return Err(error_from_signal(self.name, Some(line_number), exit_status)); Err(io_error) => {
}, return Err(RuntimeError::IoError {
Err(io_error) => return Err(RuntimeError::IoError{ recipe: self.name,
recipe: self.name, io_error,
io_error, })
}), }
}; };
} }
} }
@ -289,9 +337,8 @@ impl<'a> Display for Recipe<'a> {
write!(f, " ")?; write!(f, " ")?;
} }
match *piece { match *piece {
Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?, Fragment::Text { ref text } => write!(f, "{}", text.lexeme)?,
Fragment::Expression{ref expression, ..} => Fragment::Expression { ref expression, .. } => write!(f, "{{{{{}}}}}", expression)?,
write!(f, "{{{{{}}}}}", expression)?,
} }
} }
if i + 1 < self.lines.len() { if i + 1 < self.lines.len() {

View File

@ -3,21 +3,21 @@ use common::*;
use CompilationErrorKind::*; use CompilationErrorKind::*;
pub struct RecipeResolver<'a: 'b, 'b> { pub struct RecipeResolver<'a: 'b, 'b> {
stack: Vec<&'a str>, stack: Vec<&'a str>,
seen: Set<&'a str>, seen: Set<&'a str>,
resolved: Set<&'a str>, resolved: Set<&'a str>,
recipes: &'b Map<&'a str, Recipe<'a>>, recipes: &'b Map<&'a str, Recipe<'a>>,
} }
impl<'a, 'b> RecipeResolver<'a, 'b> { impl<'a, 'b> RecipeResolver<'a, 'b> {
pub fn resolve_recipes( pub fn resolve_recipes(
recipes: &Map<&'a str, Recipe<'a>>, recipes: &Map<&'a str, Recipe<'a>>,
assignments: &Map<&'a str, Expression<'a>>, assignments: &Map<&'a str, Expression<'a>>,
text: &'a str, text: &'a str,
) -> CompilationResult<'a, ()> { ) -> CompilationResult<'a, ()> {
let mut resolver = RecipeResolver { let mut resolver = RecipeResolver {
seen: empty(), seen: empty(),
stack: empty(), stack: empty(),
resolved: empty(), resolved: empty(),
recipes, recipes,
}; };
@ -40,15 +40,15 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
for recipe in recipes.values() { for recipe in recipes.values() {
for line in &recipe.lines { for line in &recipe.lines {
for fragment in line { for fragment in line {
if let Fragment::Expression{ref expression, ..} = *fragment { if let Fragment::Expression { ref expression, .. } = *fragment {
for (function, argc) in expression.functions() { for (function, argc) in expression.functions() {
if let Err(error) = resolve_function(function, argc) { if let Err(error) = resolve_function(function, argc) {
return Err(CompilationError { return Err(CompilationError {
index: error.index, index: error.index,
line: error.line, line: error.line,
column: error.column, column: error.column,
width: error.width, width: error.width,
kind: UnknownFunction { kind: UnknownFunction {
function: &text[error.index..error.index + error.width.unwrap()], function: &text[error.index..error.index + error.width.unwrap()],
}, },
text, text,
@ -60,13 +60,13 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
let undefined = !assignments.contains_key(name) let undefined = !assignments.contains_key(name)
&& !recipe.parameters.iter().any(|p| p.name == name); && !recipe.parameters.iter().any(|p| p.name == name);
if undefined { if undefined {
let error = variable.error(UndefinedVariable{variable: name}); let error = variable.error(UndefinedVariable { variable: name });
return Err(CompilationError { return Err(CompilationError {
index: error.index, index: error.index,
line: error.line, line: error.line,
column: error.column, column: error.column,
width: error.width, width: error.width,
kind: UndefinedVariable { kind: UndefinedVariable {
variable: &text[error.index..error.index + error.width.unwrap()], variable: &text[error.index..error.index + error.width.unwrap()],
}, },
text, text,
@ -83,29 +83,38 @@ impl<'a, 'b> RecipeResolver<'a, 'b> {
fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> { fn resolve_recipe(&mut self, recipe: &Recipe<'a>) -> CompilationResult<'a, ()> {
if self.resolved.contains(recipe.name) { if self.resolved.contains(recipe.name) {
return Ok(()) return Ok(());
} }
self.stack.push(recipe.name); self.stack.push(recipe.name);
self.seen.insert(recipe.name); self.seen.insert(recipe.name);
for dependency_token in &recipe.dependency_tokens { for dependency_token in &recipe.dependency_tokens {
match self.recipes.get(dependency_token.lexeme) { match self.recipes.get(dependency_token.lexeme) {
Some(dependency) => if !self.resolved.contains(dependency.name) { Some(dependency) => {
if self.seen.contains(dependency.name) { if !self.resolved.contains(dependency.name) {
let first = self.stack[0]; if self.seen.contains(dependency.name) {
self.stack.push(first); let first = self.stack[0];
return Err(dependency_token.error(CircularRecipeDependency { self.stack.push(first);
recipe: recipe.name, return Err(
circle: self.stack.iter() dependency_token.error(CircularRecipeDependency {
.skip_while(|name| **name != dependency.name) recipe: recipe.name,
.cloned().collect() circle: self
})); .stack
.iter()
.skip_while(|name| **name != dependency.name)
.cloned()
.collect(),
}),
);
}
self.resolve_recipe(dependency)?;
} }
self.resolve_recipe(dependency)?; }
}, None => {
None => return Err(dependency_token.error(UnknownDependency { return Err(dependency_token.error(UnknownDependency {
recipe: recipe.name, recipe: recipe.name,
unknown: dependency_token.lexeme unknown: dependency_token.lexeme,
})), }))
}
} }
} }
self.resolved.insert(recipe.name); self.resolved.insert(recipe.name);

View File

@ -1,23 +1,21 @@
use common::*; use common::*;
use std::{convert, ffi}; use clap::{App, AppSettings, Arg, ArgGroup};
use clap::{App, Arg, ArgGroup, AppSettings};
use configuration::DEFAULT_SHELL; use configuration::DEFAULT_SHELL;
use misc::maybe_s;
use unicode_width::UnicodeWidthStr;
use env_logger; use env_logger;
use interrupt_handler::InterruptHandler; use interrupt_handler::InterruptHandler;
use misc::maybe_s;
use std::{convert, ffi};
use unicode_width::UnicodeWidthStr;
#[cfg(windows)] #[cfg(windows)]
use ansi_term::enable_ansi_support; use ansi_term::enable_ansi_support;
fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! { fn edit<P: convert::AsRef<ffi::OsStr>>(path: P) -> ! {
let editor = env::var_os("EDITOR") let editor =
.unwrap_or_else(|| die!("Error getting EDITOR environment variable")); env::var_os("EDITOR").unwrap_or_else(|| die!("Error getting EDITOR environment variable"));
let error = Command::new(editor) let error = Command::new(editor).arg(path).status();
.arg(path)
.status();
match error { match error {
Ok(status) => process::exit(status.code().unwrap_or(EXIT_FAILURE)), Ok(status) => process::exit(status.code().unwrap_or(EXIT_FAILURE)),
@ -42,102 +40,151 @@ pub fn run() {
enable_ansi_support().ok(); enable_ansi_support().ok();
env_logger::Builder::from_env( env_logger::Builder::from_env(
env_logger::Env::new().filter("JUST_LOG").write_style("JUST_LOG_STYLE") env_logger::Env::new()
).init(); .filter("JUST_LOG")
.write_style("JUST_LOG_STYLE"),
)
.init();
let invocation_directory = env::current_dir() let invocation_directory =
.map_err(|e| format!("Error getting current directory: {}", e)); env::current_dir().map_err(|e| format!("Error getting current directory: {}", e));
let matches = App::new(env!("CARGO_PKG_NAME")) let matches = App::new(env!("CARGO_PKG_NAME"))
.version(concat!("v", env!("CARGO_PKG_VERSION"))) .version(concat!("v", env!("CARGO_PKG_VERSION")))
.author(env!("CARGO_PKG_AUTHORS")) .author(env!("CARGO_PKG_AUTHORS"))
.about(concat!(env!("CARGO_PKG_DESCRIPTION"), " - ", env!("CARGO_PKG_HOMEPAGE"))) .about(concat!(
env!("CARGO_PKG_DESCRIPTION"),
" - ",
env!("CARGO_PKG_HOMEPAGE")
))
.help_message("Print help information") .help_message("Print help information")
.version_message("Print version information") .version_message("Print version information")
.setting(AppSettings::ColoredHelp) .setting(AppSettings::ColoredHelp)
.setting(AppSettings::TrailingVarArg) .setting(AppSettings::TrailingVarArg)
.arg(Arg::with_name("ARGUMENTS") .arg(
.multiple(true) Arg::with_name("ARGUMENTS")
.help("The recipe(s) to run, defaults to the first recipe in the justfile")) .multiple(true)
.arg(Arg::with_name("COLOR") .help("The recipe(s) to run, defaults to the first recipe in the justfile"),
.long("color") )
.takes_value(true) .arg(
.possible_values(&["auto", "always", "never"]) Arg::with_name("COLOR")
.default_value("auto") .long("color")
.help("Print colorful output")) .takes_value(true)
.arg(Arg::with_name("DRY-RUN") .possible_values(&["auto", "always", "never"])
.long("dry-run") .default_value("auto")
.help("Print what just would do without doing it") .help("Print colorful output"),
.conflicts_with("QUIET")) )
.arg(Arg::with_name("DUMP") .arg(
.long("dump") Arg::with_name("DRY-RUN")
.help("Print entire justfile")) .long("dry-run")
.arg(Arg::with_name("EDIT") .help("Print what just would do without doing it")
.short("e") .conflicts_with("QUIET"),
.long("edit") )
.help("Open justfile with $EDITOR")) .arg(
.arg(Arg::with_name("EVALUATE") Arg::with_name("DUMP")
.long("evaluate") .long("dump")
.help("Print evaluated variables")) .help("Print entire justfile"),
.arg(Arg::with_name("HIGHLIGHT") )
.long("highlight") .arg(
.help("Highlight echoed recipe lines in bold")) Arg::with_name("EDIT")
.arg(Arg::with_name("JUSTFILE") .short("e")
.short("f") .long("edit")
.long("justfile") .help("Open justfile with $EDITOR"),
.takes_value(true) )
.help("Use <JUSTFILE> as justfile. --working-directory must also be set") .arg(
.requires("WORKING-DIRECTORY")) Arg::with_name("EVALUATE")
.arg(Arg::with_name("LIST") .long("evaluate")
.short("l") .help("Print evaluated variables"),
.long("list") )
.help("List available recipes and their arguments")) .arg(
.arg(Arg::with_name("QUIET") Arg::with_name("HIGHLIGHT")
.short("q") .long("highlight")
.long("quiet") .help("Highlight echoed recipe lines in bold"),
.help("Suppress all output") )
.conflicts_with("DRY-RUN")) .arg(
.arg(Arg::with_name("SET") Arg::with_name("JUSTFILE")
.long("set") .short("f")
.takes_value(true) .long("justfile")
.number_of_values(2) .takes_value(true)
.value_names(&["VARIABLE", "VALUE"]) .help("Use <JUSTFILE> as justfile. --working-directory must also be set")
.multiple(true) .requires("WORKING-DIRECTORY"),
.help("Set <VARIABLE> to <VALUE>")) )
.arg(Arg::with_name("SHELL") .arg(
.long("shell") Arg::with_name("LIST")
.takes_value(true) .short("l")
.default_value(DEFAULT_SHELL) .long("list")
.help("Invoke <SHELL> to run recipes")) .help("List available recipes and their arguments"),
.arg(Arg::with_name("SHOW") )
.short("s") .arg(
.long("show") Arg::with_name("QUIET")
.takes_value(true) .short("q")
.value_name("RECIPE") .long("quiet")
.help("Show information about <RECIPE>")) .help("Suppress all output")
.arg(Arg::with_name("SUMMARY") .conflicts_with("DRY-RUN"),
.long("summary") )
.help("List names of available recipes")) .arg(
.arg(Arg::with_name("VERBOSE") Arg::with_name("SET")
.short("v") .long("set")
.long("verbose") .takes_value(true)
.multiple(true) .number_of_values(2)
.help("Use verbose output")) .value_names(&["VARIABLE", "VALUE"])
.arg(Arg::with_name("WORKING-DIRECTORY") .multiple(true)
.help("Set <VARIABLE> to <VALUE>"),
)
.arg(
Arg::with_name("SHELL")
.long("shell")
.takes_value(true)
.default_value(DEFAULT_SHELL)
.help("Invoke <SHELL> to run recipes"),
)
.arg(
Arg::with_name("SHOW")
.short("s")
.long("show")
.takes_value(true)
.value_name("RECIPE")
.help("Show information about <RECIPE>"),
)
.arg(
Arg::with_name("SUMMARY")
.long("summary")
.help("List names of available recipes"),
)
.arg(
Arg::with_name("VERBOSE")
.short("v")
.long("verbose")
.multiple(true)
.help("Use verbose output"),
)
.arg(
Arg::with_name("WORKING-DIRECTORY")
.short("d") .short("d")
.long("working-directory") .long("working-directory")
.takes_value(true) .takes_value(true)
.help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set") .help("Use <WORKING-DIRECTORY> as working directory. --justfile must also be set")
.requires("JUSTFILE")) .requires("JUSTFILE"),
.group(ArgGroup::with_name("EARLY-EXIT") )
.args(&["DUMP", "EDIT", "LIST", "SHOW", "SUMMARY", "ARGUMENTS", "EVALUATE"])) .group(ArgGroup::with_name("EARLY-EXIT").args(&[
"DUMP",
"EDIT",
"LIST",
"SHOW",
"SUMMARY",
"ARGUMENTS",
"EVALUATE",
]))
.get_matches(); .get_matches();
let color = match matches.value_of("COLOR").expect("`--color` had no value") { let color = match matches.value_of("COLOR").expect("`--color` had no value") {
"auto" => Color::auto(), "auto" => Color::auto(),
"always" => Color::always(), "always" => Color::always(),
"never" => Color::never(), "never" => Color::never(),
other => die!("Invalid argument `{}` to --color. This is a bug in just.", other), other => die!(
"Invalid argument `{}` to --color. This is a bug in just.",
other
),
}; };
let set_count = matches.occurrences_of("SET"); let set_count = matches.occurrences_of("SET");
@ -151,15 +198,25 @@ pub fn run() {
let override_re = Regex::new("^([^=]+)=(.*)$").unwrap(); let override_re = Regex::new("^([^=]+)=(.*)$").unwrap();
let raw_arguments = matches.values_of("ARGUMENTS").map(|values| values.collect::<Vec<_>>()) let raw_arguments = matches
.values_of("ARGUMENTS")
.map(|values| values.collect::<Vec<_>>())
.unwrap_or_default(); .unwrap_or_default();
for argument in raw_arguments.iter().take_while(|arg| override_re.is_match(arg)) { for argument in raw_arguments
.iter()
.take_while(|arg| override_re.is_match(arg))
{
let captures = override_re.captures(argument).unwrap(); let captures = override_re.captures(argument).unwrap();
overrides.insert(captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str()); overrides.insert(
captures.get(1).unwrap().as_str(),
captures.get(2).unwrap().as_str(),
);
} }
let rest = raw_arguments.iter().skip_while(|arg| override_re.is_match(arg)) let rest = raw_arguments
.iter()
.skip_while(|arg| override_re.is_match(arg))
.enumerate() .enumerate()
.flat_map(|(i, argument)| { .flat_map(|(i, argument)| {
if i == 0 { if i == 0 {
@ -208,10 +265,12 @@ pub fn run() {
'outer: loop { 'outer: loop {
for candidate in &["justfile", "Justfile"] { for candidate in &["justfile", "Justfile"] {
match fs::metadata(candidate) { match fs::metadata(candidate) {
Ok(metadata) => if metadata.is_file() { Ok(metadata) => {
name = *candidate; if metadata.is_file() {
break 'outer; name = *candidate;
}, break 'outer;
}
}
Err(error) => { Err(error) => {
if error.kind() != io::ErrorKind::NotFound { if error.kind() != io::ErrorKind::NotFound {
die!("Error fetching justfile metadata: {}", error) die!("Error fetching justfile metadata: {}", error)
@ -221,7 +280,11 @@ pub fn run() {
} }
match env::current_dir() { match env::current_dir() {
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); }, Ok(pathbuf) => {
if pathbuf.as_os_str() == "/" {
die!("No justfile found.");
}
}
Err(error) => die!("Error getting current dir: {}", error), Err(error) => die!("Error getting current dir: {}", error),
} }
@ -240,19 +303,21 @@ pub fn run() {
.unwrap_or_else(|error| die!("Error reading justfile: {}", error)); .unwrap_or_else(|error| die!("Error reading justfile: {}", error));
} }
let justfile = Parser::parse(&text).unwrap_or_else(|error| let justfile = Parser::parse(&text).unwrap_or_else(|error| {
if color.stderr().active() { if color.stderr().active() {
die!("{:#}", error); die!("{:#}", error);
} else { } else {
die!("{}", error); die!("{}", error);
} }
); });
if matches.is_present("SUMMARY") { if matches.is_present("SUMMARY") {
if justfile.count() == 0 { if justfile.count() == 0 {
eprintln!("Justfile contains no recipes."); eprintln!("Justfile contains no recipes.");
} else { } else {
let summary = justfile.recipes.iter() let summary = justfile
.recipes
.iter()
.filter(|&(_, recipe)| !recipe.private) .filter(|&(_, recipe)| !recipe.private)
.map(|(name, _)| name) .map(|(name, _)| name)
.cloned() .cloned()
@ -305,10 +370,12 @@ pub fn run() {
} }
if let Some(doc) = recipe.doc { if let Some(doc) = recipe.doc {
print!( print!(
" {:padding$}{} {}", "", doc_color.paint("#"), doc_color.paint(doc), " {:padding$}{} {}",
padding = max_line_width.saturating_sub( "",
line_widths.get(name).cloned().unwrap_or(max_line_width) doc_color.paint("#"),
) doc_color.paint(doc),
padding =
max_line_width.saturating_sub(line_widths.get(name).cloned().unwrap_or(max_line_width))
); );
} }
println!(); println!();
@ -337,8 +404,12 @@ pub fn run() {
} else if let Some(recipe) = justfile.first() { } else if let Some(recipe) = justfile.first() {
let min_arguments = recipe.min_arguments(); let min_arguments = recipe.min_arguments();
if min_arguments > 0 { if min_arguments > 0 {
die!("Recipe `{}` cannot be used as default recipe since it requires at least {} argument{}.", die!(
recipe.name, min_arguments, maybe_s(min_arguments)); "Recipe `{}` cannot be used as default recipe since it requires at least {} argument{}.",
recipe.name,
min_arguments,
maybe_s(min_arguments)
);
} }
vec![recipe.name] vec![recipe.name]
} else { } else {
@ -348,11 +419,11 @@ pub fn run() {
let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of("VERBOSE")); let verbosity = Verbosity::from_flag_occurrences(matches.occurrences_of("VERBOSE"));
let configuration = Configuration { let configuration = Configuration {
dry_run: matches.is_present("DRY-RUN"), dry_run: matches.is_present("DRY-RUN"),
evaluate: matches.is_present("EVALUATE"), evaluate: matches.is_present("EVALUATE"),
highlight: matches.is_present("HIGHLIGHT"), highlight: matches.is_present("HIGHLIGHT"),
quiet: matches.is_present("QUIET"), quiet: matches.is_present("QUIET"),
shell: matches.value_of("SHELL").unwrap(), shell: matches.value_of("SHELL").unwrap(),
verbosity, verbosity,
color, color,
overrides, overrides,
@ -362,11 +433,7 @@ pub fn run() {
warn!("Failed to set CTRL-C handler: {}", error) warn!("Failed to set CTRL-C handler: {}", error)
} }
if let Err(run_error) = justfile.run( if let Err(run_error) = justfile.run(&invocation_directory, &arguments, &configuration) {
&invocation_directory,
&arguments,
&configuration)
{
if !configuration.quiet { if !configuration.quiet {
if color.stderr().active() { if color.stderr().active() {
eprintln!("{:#}", run_error); eprintln!("{:#}", run_error);

View File

@ -4,7 +4,7 @@ use dotenv;
use brev::OutputError; use brev::OutputError;
use misc::{And, Or, maybe_s, Tick, ticks, write_error_context}; use misc::{maybe_s, ticks, write_error_context, And, Or, Tick};
use self::RuntimeError::*; use self::RuntimeError::*;
@ -17,38 +17,82 @@ fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<()
token.index, token.index,
token.line, token.line,
token.column + token.prefix.len(), token.column + token.prefix.len(),
Some(token.lexeme.len()) Some(token.lexeme.len()),
) )
} }
#[derive(Debug)] #[derive(Debug)]
pub enum RuntimeError<'a> { pub enum RuntimeError<'a> {
ArgumentCountMismatch{ ArgumentCountMismatch {
recipe: &'a str, recipe: &'a str,
parameters: Vec<&'a Parameter<'a>>, parameters: Vec<&'a Parameter<'a>>,
found: usize, found: usize,
min: usize, min: usize,
max: usize, max: usize,
}, },
Backtick{token: Token<'a>, output_error: OutputError}, Backtick {
Code{recipe: &'a str, line_number: Option<usize>, code: i32}, token: Token<'a>,
Cygpath{recipe: &'a str, output_error: OutputError}, output_error: OutputError,
Dotenv{dotenv_error: dotenv::Error}, },
FunctionCall{token: Token<'a>, message: String}, Code {
Internal{message: String}, recipe: &'a str,
IoError{recipe: &'a str, io_error: io::Error}, line_number: Option<usize>,
Shebang{recipe: &'a str, command: String, argument: Option<String>, io_error: io::Error}, code: i32,
Signal{recipe: &'a str, line_number: Option<usize>, signal: i32}, },
TmpdirIoError{recipe: &'a str, io_error: io::Error}, Cygpath {
UnknownOverrides{overrides: Vec<&'a str>}, recipe: &'a str,
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, output_error: OutputError,
Unknown{recipe: &'a str, line_number: Option<usize>}, },
Dotenv {
dotenv_error: dotenv::Error,
},
FunctionCall {
token: Token<'a>,
message: String,
},
Internal {
message: String,
},
IoError {
recipe: &'a str,
io_error: io::Error,
},
Shebang {
recipe: &'a str,
command: String,
argument: Option<String>,
io_error: io::Error,
},
Signal {
recipe: &'a str,
line_number: Option<usize>,
signal: i32,
},
TmpdirIoError {
recipe: &'a str,
io_error: io::Error,
},
UnknownOverrides {
overrides: Vec<&'a str>,
},
UnknownRecipes {
recipes: Vec<&'a str>,
suggestion: Option<&'a str>,
},
Unknown {
recipe: &'a str,
line_number: Option<usize>,
},
} }
impl<'a> RuntimeError<'a> { impl<'a> RuntimeError<'a> {
pub fn code(&self) -> Option<i32> { pub fn code(&self) -> Option<i32> {
match *self { match *self {
Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code), Code { code, .. }
| Backtick {
output_error: OutputError::Code(code),
..
} => Some(code),
_ => None, _ => None,
} }
} }
@ -57,7 +101,11 @@ impl<'a> RuntimeError<'a> {
impl<'a> Display for RuntimeError<'a> { impl<'a> Display for RuntimeError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use RuntimeError::*; use RuntimeError::*;
let color = if f.alternate() { Color::always() } else { Color::never() }; let color = if f.alternate() {
Color::always()
} else {
Color::never()
};
let error = color.error(); let error = color.error();
let message = color.message(); let message = color.message();
write!(f, "{} {}", error.paint("error:"), message.prefix())?; write!(f, "{} {}", error.paint("error:"), message.prefix())?;
@ -65,30 +113,64 @@ impl<'a> Display for RuntimeError<'a> {
let mut error_token = None; let mut error_token = None;
match *self { match *self {
UnknownRecipes{ref recipes, ref suggestion} => { UnknownRecipes {
write!(f, "Justfile does not contain recipe{} {}.", ref recipes,
maybe_s(recipes.len()), Or(&ticks(recipes)))?; ref suggestion,
} => {
write!(
f,
"Justfile does not contain recipe{} {}.",
maybe_s(recipes.len()),
Or(&ticks(recipes))
)?;
if let Some(suggestion) = *suggestion { if let Some(suggestion) = *suggestion {
write!(f, "\nDid you mean `{}`?", suggestion)?; write!(f, "\nDid you mean `{}`?", suggestion)?;
} }
}, }
UnknownOverrides{ref overrides} => { UnknownOverrides { ref overrides } => {
write!(f, "Variable{} {} overridden on the command line but not present in justfile", write!(
maybe_s(overrides.len()), f,
And(&overrides.iter().map(Tick).collect::<Vec<_>>()))?; "Variable{} {} overridden on the command line but not present in justfile",
}, maybe_s(overrides.len()),
ArgumentCountMismatch{recipe, ref parameters, found, min, max} => { And(&overrides.iter().map(Tick).collect::<Vec<_>>())
)?;
}
ArgumentCountMismatch {
recipe,
ref parameters,
found,
min,
max,
} => {
if min == max { if min == max {
let expected = min; let expected = min;
write!(f, "Recipe `{}` got {} argument{} but {}takes {}", write!(
recipe, found, maybe_s(found), f,
if expected < found { "only " } else { "" }, expected)?; "Recipe `{}` got {} argument{} but {}takes {}",
recipe,
found,
maybe_s(found),
if expected < found { "only " } else { "" },
expected
)?;
} else if found < min { } else if found < min {
write!(f, "Recipe `{}` got {} argument{} but takes at least {}", write!(
recipe, found, maybe_s(found), min)?; f,
"Recipe `{}` got {} argument{} but takes at least {}",
recipe,
found,
maybe_s(found),
min
)?;
} else if found > max { } else if found > max {
write!(f, "Recipe `{}` got {} argument{} but takes at most {}", write!(
recipe, found, maybe_s(found), max)?; f,
"Recipe `{}` got {} argument{} but takes at most {}",
recipe,
found,
maybe_s(found),
max
)?;
} }
write!(f, "\nusage:\n just {}", recipe)?; write!(f, "\nusage:\n just {}", recipe)?;
for param in parameters { for param in parameters {
@ -98,89 +180,170 @@ impl<'a> Display for RuntimeError<'a> {
write!(f, " {}", param)?; write!(f, " {}", param)?;
} }
} }
}, }
Code{recipe, line_number, code} => { Code {
recipe,
line_number,
code,
} => {
if let Some(n) = line_number { if let Some(n) = line_number {
write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?; write!(
f,
"Recipe `{}` failed on line {} with exit code {}",
recipe, n, code
)?;
} else { } else {
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
} }
}, }
Cygpath{recipe, ref output_error} => match *output_error { Cygpath {
recipe,
ref output_error,
} => match *output_error {
OutputError::Code(code) => { OutputError::Code(code) => {
write!(f, "Cygpath failed with exit code {} while translating recipe `{}` \ write!(
shebang interpreter path", code, recipe)?; f,
"Cygpath failed with exit code {} while translating recipe `{}` \
shebang interpreter path",
code, recipe
)?;
} }
OutputError::Signal(signal) => { OutputError::Signal(signal) => {
write!(f, "Cygpath terminated by signal {} while translating recipe `{}` \ write!(
shebang interpreter path", signal, recipe)?; f,
"Cygpath terminated by signal {} while translating recipe `{}` \
shebang interpreter path",
signal, recipe
)?;
} }
OutputError::Unknown => { OutputError::Unknown => {
write!(f, "Cygpath experienced an unknown failure while translating recipe `{}` \ write!(
shebang interpreter path", recipe)?; f,
"Cygpath experienced an unknown failure while translating recipe `{}` \
shebang interpreter path",
recipe
)?;
} }
OutputError::Io(ref io_error) => { OutputError::Io(ref io_error) => {
match io_error.kind() { match io_error.kind() {
io::ErrorKind::NotFound => write!( io::ErrorKind::NotFound => write!(
f, "Could not find `cygpath` executable to translate recipe `{}` \ f,
shebang interpreter path:\n{}", recipe, io_error), "Could not find `cygpath` executable to translate recipe `{}` \
shebang interpreter path:\n{}",
recipe, io_error
),
io::ErrorKind::PermissionDenied => write!( io::ErrorKind::PermissionDenied => write!(
f, "Could not run `cygpath` executable to translate recipe `{}` \ f,
shebang interpreter path:\n{}", recipe, io_error), "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), _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error),
}?; }?;
} }
OutputError::Utf8(ref utf8_error) => { OutputError::Utf8(ref utf8_error) => {
write!(f, "Cygpath successfully translated recipe `{}` shebang interpreter path, \ write!(
but output was not utf8: {}", recipe, utf8_error)?; f,
"Cygpath successfully translated recipe `{}` shebang interpreter path, \
but output was not utf8: {}",
recipe, utf8_error
)?;
} }
}, },
Dotenv{ref dotenv_error} => { Dotenv { ref dotenv_error } => {
writeln!(f, "Failed to load .env: {}", dotenv_error)?; writeln!(f, "Failed to load .env: {}", dotenv_error)?;
} }
FunctionCall{ref token, ref message} => { FunctionCall {
ref token,
ref message,
} => {
writeln!(f, "Call to function `{}` failed: {}", token.lexeme, message)?; writeln!(f, "Call to function `{}` failed: {}", token.lexeme, message)?;
error_token = Some(token); error_token = Some(token);
} }
Shebang{recipe, ref command, ref argument, ref io_error} => { Shebang {
recipe,
ref command,
ref argument,
ref io_error,
} => {
if let Some(ref argument) = *argument { if let Some(ref argument) = *argument {
write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}", write!(
recipe, command, argument, io_error)?; f,
"Recipe `{}` with shebang `#!{} {}` execution error: {}",
recipe, command, argument, io_error
)?;
} else { } else {
write!(f, "Recipe `{}` with shebang `#!{}` execution error: {}", write!(
recipe, command, io_error)?; f,
"Recipe `{}` with shebang `#!{}` execution error: {}",
recipe, command, io_error
)?;
} }
} }
Signal{recipe, line_number, signal} => { Signal {
recipe,
line_number,
signal,
} => {
if let Some(n) = line_number { if let Some(n) = line_number {
write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?; write!(
f,
"Recipe `{}` was terminated on line {} by signal {}",
recipe, n, signal
)?;
} else { } else {
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
} }
} }
Unknown{recipe, line_number} => { Unknown {
recipe,
line_number,
} => {
if let Some(n) = line_number { if let Some(n) = line_number {
write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?; write!(
f,
"Recipe `{}` failed on line {} for an unknown reason",
recipe, n
)?;
} else { } else {
} }
}, }
IoError{recipe, ref io_error} => { IoError {
recipe,
ref io_error,
} => {
match io_error.kind() { match io_error.kind() {
io::ErrorKind::NotFound => writeln!(f, io::ErrorKind::NotFound => writeln!(
f,
"Recipe `{}` could not be run because just could not find `sh`:{}", "Recipe `{}` could not be run because just could not find `sh`:{}",
recipe, io_error), recipe, io_error
),
io::ErrorKind::PermissionDenied => writeln!( io::ErrorKind::PermissionDenied => writeln!(
f, "Recipe `{}` could not be run because just could not run `sh`:{}", f,
recipe, io_error), "Recipe `{}` could not be run because just could not run `sh`:{}",
_ => writeln!(f, "Recipe `{}` could not be run because of an IO error while \ recipe, io_error
launching `sh`:{}", recipe, io_error), ),
_ => writeln!(
f,
"Recipe `{}` could not be run because of an IO error while \
launching `sh`:{}",
recipe, io_error
),
}?; }?;
}, }
TmpdirIoError{recipe, ref io_error} => TmpdirIoError {
writeln!(f, "Recipe `{}` could not be run because of an IO error while trying \ recipe,
to create a temporary directory or write a file to that directory`:{}", ref io_error,
recipe, io_error)?, } => writeln!(
Backtick{ref token, ref output_error} => match *output_error { 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 {
ref token,
ref output_error,
} => match *output_error {
OutputError::Code(code) => { OutputError::Code(code) => {
writeln!(f, "Backtick failed with exit code {}", code)?; writeln!(f, "Backtick failed with exit code {}", code)?;
error_token = Some(token); error_token = Some(token);
@ -196,24 +359,40 @@ impl<'a> Display for RuntimeError<'a> {
OutputError::Io(ref io_error) => { OutputError::Io(ref io_error) => {
match io_error.kind() { match io_error.kind() {
io::ErrorKind::NotFound => write!( io::ErrorKind::NotFound => write!(
f, "Backtick could not be run because just could not find `sh`:\n{}", f,
io_error), "Backtick could not be run because just could not find `sh`:\n{}",
io_error
),
io::ErrorKind::PermissionDenied => write!( io::ErrorKind::PermissionDenied => write!(
f, "Backtick could not be run because just could not run `sh`:\n{}", io_error), f,
_ => write!(f, "Backtick could not be run because of an IO \ "Backtick could not be run because just could not run `sh`:\n{}",
error while launching `sh`:\n{}", io_error), io_error
),
_ => write!(
f,
"Backtick could not be run because of an IO \
error while launching `sh`:\n{}",
io_error
),
}?; }?;
error_token = Some(token); error_token = Some(token);
} }
OutputError::Utf8(ref utf8_error) => { OutputError::Utf8(ref utf8_error) => {
writeln!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?; writeln!(
f,
"Backtick succeeded but stdout was not utf8: {}",
utf8_error
)?;
error_token = Some(token); error_token = Some(token);
} }
}, },
Internal{ref message} => { Internal { ref message } => {
write!(f, "Internal error, this may indicate a bug in just: {} \ write!(
consider filing an issue: https://github.com/casey/just/issues/new", f,
message)?; "Internal error, this may indicate a bug in just: {} \
consider filing an issue: https://github.com/casey/just/issues/new",
message
)?;
} }
} }

View File

@ -1,6 +1,6 @@
pub struct Shebang<'a> { pub struct Shebang<'a> {
pub interpreter: &'a str, pub interpreter: &'a str,
pub argument: Option<&'a str>, pub argument: Option<&'a str>,
} }
impl<'a> Shebang<'a> { impl<'a> Shebang<'a> {
@ -17,13 +17,16 @@ impl<'a> Shebang<'a> {
.splitn(2, |c| c == ' ' || c == '\t'); .splitn(2, |c| c == ' ' || c == '\t');
let interpreter = pieces.next().unwrap_or(""); let interpreter = pieces.next().unwrap_or("");
let argument = pieces.next(); let argument = pieces.next();
if interpreter == "" { if interpreter == "" {
return None; return None;
} }
Some(Shebang{interpreter, argument}) Some(Shebang {
interpreter,
argument,
})
} }
} }
@ -35,26 +38,59 @@ mod test {
fn split_shebang() { fn split_shebang() {
fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) { fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) {
let shebang = Shebang::new(text); let shebang = Shebang::new(text);
assert_eq!(shebang.map(|shebang| (shebang.interpreter, shebang.argument)), expected_split); assert_eq!(
shebang.map(|shebang| (shebang.interpreter, shebang.argument)),
expected_split
);
} }
check("#! ", None ); check("#! ", None);
check("#!", None ); check("#!", None);
check("#!/bin/bash", Some(("/bin/bash", None ))); check("#!/bin/bash", Some(("/bin/bash", None)));
check("#!/bin/bash ", Some(("/bin/bash", None ))); check("#!/bin/bash ", Some(("/bin/bash", None)));
check("#!/usr/bin/env python", Some(("/usr/bin/env", Some("python" )))); check(
check("#!/usr/bin/env python ", Some(("/usr/bin/env", Some("python" )))); "#!/usr/bin/env python",
check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x" )))); Some(("/usr/bin/env", Some("python"))),
check("#!/usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x")))); );
check("#!/usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x")))); check(
check("#/usr/bin/env python \t-x\t", None ); "#!/usr/bin/env python ",
check("#! /bin/bash", Some(("/bin/bash", None ))); Some(("/usr/bin/env", Some("python"))),
check("#!\t\t/bin/bash ", Some(("/bin/bash", None ))); );
check("#! \t\t/usr/bin/env python", Some(("/usr/bin/env", Some("python" )))); check(
check("#! /usr/bin/env python ", Some(("/usr/bin/env", Some("python" )))); "#!/usr/bin/env python -x",
check("#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x" )))); Some(("/usr/bin/env", Some("python -x"))),
check("#! /usr/bin/env python -x", Some(("/usr/bin/env", Some("python -x")))); );
check("#! /usr/bin/env python \t-x\t", Some(("/usr/bin/env", Some("python \t-x")))); check(
check("# /usr/bin/env python \t-x\t", None ); "#!/usr/bin/env python -x",
Some(("/usr/bin/env", Some("python -x"))),
);
check(
"#!/usr/bin/env python \t-x\t",
Some(("/usr/bin/env", Some("python \t-x"))),
);
check("#/usr/bin/env python \t-x\t", None);
check("#! /bin/bash", Some(("/bin/bash", None)));
check("#!\t\t/bin/bash ", Some(("/bin/bash", None)));
check(
"#! \t\t/usr/bin/env python",
Some(("/usr/bin/env", Some("python"))),
);
check(
"#! /usr/bin/env python ",
Some(("/usr/bin/env", Some("python"))),
);
check(
"#! /usr/bin/env python -x",
Some(("/usr/bin/env", Some("python -x"))),
);
check(
"#! /usr/bin/env python -x",
Some(("/usr/bin/env", Some("python -x"))),
);
check(
"#! /usr/bin/env python \t-x\t",
Some(("/usr/bin/env", Some("python \t-x"))),
);
check("# /usr/bin/env python \t-x\t", None);
} }
} }

View File

@ -22,28 +22,28 @@ macro_rules! compilation_error_test {
let input = $input; let input = $input;
let expected = ::CompilationError { let expected = ::CompilationError {
text: input, text: input,
index: $index, index: $index,
line: $line, line: $line,
column: $column, column: $column,
width: $width, width: $width,
kind: $kind, kind: $kind,
}; };
let tokens = ::Lexer::lex(input).unwrap(); let tokens = ::Lexer::lex(input).unwrap();
let parser = ::Parser::new(input, tokens); let parser = ::Parser::new(input, tokens);
if let Err(error) = parser.justfile() { if let Err(error) = parser.justfile() {
assert_eq!(error.text, expected.text); assert_eq!(error.text, expected.text);
assert_eq!(error.index, expected.index); assert_eq!(error.index, expected.index);
assert_eq!(error.line, expected.line); assert_eq!(error.line, expected.line);
assert_eq!(error.column, expected.column); assert_eq!(error.column, expected.column);
assert_eq!(error.width, expected.width); assert_eq!(error.width, expected.width);
assert_eq!(error.kind, expected.kind); assert_eq!(error.kind, expected.kind);
assert_eq!(error, expected); assert_eq!(error, expected);
} else { } else {
panic!("parse succeeded but expected: {}\n{}", expected, input); panic!("parse succeeded but expected: {}\n{}", expected, input);
} }
} }
} };
} }

View File

@ -2,23 +2,23 @@ use common::*;
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct Token<'a> { pub struct Token<'a> {
pub index: usize, pub index: usize,
pub line: usize, pub line: usize,
pub column: usize, pub column: usize,
pub text: &'a str, pub text: &'a str,
pub prefix: &'a str, pub prefix: &'a str,
pub lexeme: &'a str, pub lexeme: &'a str,
pub kind: TokenKind, pub kind: TokenKind,
} }
impl<'a> Token<'a> { impl<'a> Token<'a> {
pub fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> { pub fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> {
CompilationError { CompilationError {
column: self.column + self.prefix.len(), column: self.column + self.prefix.len(),
index: self.index + self.prefix.len(), index: self.index + self.prefix.len(),
line: self.line, line: self.line,
text: self.text, text: self.text,
width: Some(self.lexeme.len()), width: Some(self.lexeme.len()),
kind, kind,
} }
} }
@ -51,27 +51,31 @@ pub enum TokenKind {
impl Display for TokenKind { impl Display for TokenKind {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
use TokenKind::*; use TokenKind::*;
write!(f, "{}", match *self { write!(
Backtick => "backtick", f,
Colon => "':'", "{}",
Comma => "','", match *self {
Comment => "comment", Backtick => "backtick",
Dedent => "dedent", Colon => "':'",
Eof => "end of file", Comma => "','",
Eol => "end of line", Comment => "comment",
Equals => "'='", Dedent => "dedent",
Indent => "indent", Eof => "end of file",
InterpolationEnd => "'}}'", Eol => "end of line",
InterpolationStart => "'{{'", Equals => "'='",
Line => "command", Indent => "indent",
Name => "name", InterpolationEnd => "'}}'",
Plus => "'+'", InterpolationStart => "'{{'",
At => "'@'", Line => "command",
ParenL => "'('", Name => "name",
ParenR => "')'", Plus => "'+'",
StringToken => "string", At => "'@'",
RawString => "raw string", ParenL => "'('",
Text => "command text", ParenR => "')'",
}) StringToken => "string",
RawString => "raw string",
Text => "command text",
}
)
} }
} }

View File

@ -41,16 +41,19 @@ macro_rules! integration_test {
} }
fn integration_test( fn integration_test(
shell: &str, shell: &str,
justfile: &str, justfile: &str,
args: &[&str], args: &[&str],
expected_stdout: &str, expected_stdout: &str,
expected_stderr: &str, expected_stderr: &str,
expected_status: i32, expected_status: i32,
) { ) {
let tmp = TempDir::new("just-integration") let tmp = TempDir::new("just-integration").unwrap_or_else(|err| {
.unwrap_or_else( panic!(
|err| panic!("integration test: failed to create temporary directory: {}", err)); "integration test: failed to create temporary directory: {}",
err
)
});
let mut justfile_path = tmp.path().to_path_buf(); let mut justfile_path = tmp.path().to_path_buf();
justfile_path.push("justfile"); justfile_path.push("justfile");
@ -77,13 +80,19 @@ fn integration_test(
let stdout = str::from_utf8(&output.stdout).unwrap(); let stdout = str::from_utf8(&output.stdout).unwrap();
if stdout != expected_stdout { if stdout != expected_stdout {
println!("bad stdout:\ngot:\n{}\n\nexpected:\n{}", stdout, expected_stdout); println!(
"bad stdout:\ngot:\n{}\n\nexpected:\n{}",
stdout, expected_stdout
);
failure = true; failure = true;
} }
let stderr = str::from_utf8(&output.stderr).unwrap(); let stderr = str::from_utf8(&output.stderr).unwrap();
if stderr != expected_stderr { if stderr != expected_stderr {
println!("bad stderr:\ngot:\n{}\n\nexpected:\n{}", stderr, expected_stderr); println!(
"bad stderr:\ngot:\n{}\n\nexpected:\n{}",
stderr, expected_stderr
);
failure = true; failure = true;
} }
@ -591,7 +600,6 @@ wut:
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }
integration_test! { integration_test! {
name: export_shebang, name: export_shebang,
justfile: r#" justfile: r#"
@ -929,7 +937,6 @@ integration_test! {
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }
integration_test! { integration_test! {
name: required_after_default, name: required_after_default,
justfile: "bar:\nhello baz arg='foo' bar:", justfile: "bar:\nhello baz arg='foo' bar:",
@ -969,7 +976,6 @@ hello baz arg="XYZ\t\" ":
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
integration_test! { integration_test! {
name: use_raw_string_default, name: use_raw_string_default,
justfile: r#" justfile: r#"
@ -1228,7 +1234,6 @@ foo:
status: EXIT_SUCCESS, status: EXIT_SUCCESS,
} }
integration_test! { integration_test! {
name: env_var_failure, name: env_var_failure,
justfile: "a:\n echo {{env_var('ZADDY')}}", justfile: "a:\n echo {{env_var('ZADDY')}}",
@ -1500,7 +1505,6 @@ a:"#,
status: EXIT_FAILURE, status: EXIT_FAILURE,
} }
integration_test! { integration_test! {
name: multiline_raw_string, name: multiline_raw_string,
justfile: " justfile: "

View File

@ -4,8 +4,12 @@ extern crate libc;
extern crate tempdir; extern crate tempdir;
use executable_path::executable_path; use executable_path::executable_path;
use std::{
process::Command,
thread,
time::{Duration, Instant},
};
use tempdir::TempDir; use tempdir::TempDir;
use std::{process::Command, time::{Duration, Instant}, thread};
#[cfg(unix)] #[cfg(unix)]
fn kill(process_id: u32) { fn kill(process_id: u32) {
@ -16,9 +20,12 @@ fn kill(process_id: u32) {
#[cfg(unix)] #[cfg(unix)]
fn interrupt_test(justfile: &str) { fn interrupt_test(justfile: &str) {
let tmp = TempDir::new("just-interrupts") let tmp = TempDir::new("just-interrupts").unwrap_or_else(|err| {
.unwrap_or_else( panic!(
|err| panic!("integration test: failed to create temporary directory: {}", err)); "integration test: failed to create temporary directory: {}",
err
)
});
let mut justfile_path = tmp.path().to_path_buf(); let mut justfile_path = tmp.path().to_path_buf();
justfile_path.push("justfile"); justfile_path.push("justfile");
@ -53,29 +60,35 @@ fn interrupt_test(justfile: &str) {
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn interrupt_shebang() { fn interrupt_shebang() {
interrupt_test(" interrupt_test(
"
default: default:
#!/usr/bin/env sh #!/usr/bin/env sh
sleep 2 sleep 2
"); ",
);
} }
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn interrupt_line() { fn interrupt_line() {
interrupt_test(" interrupt_test(
"
default: default:
@sleep 2 @sleep 2
"); ",
);
} }
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn interrupt_backtick() { fn interrupt_backtick() {
interrupt_test(" interrupt_test(
"
foo = `sleep 2` foo = `sleep 2`
default: default:
@echo hello @echo hello
"); ",
);
} }

View File

@ -4,16 +4,19 @@ extern crate target;
extern crate tempdir; extern crate tempdir;
use executable_path::executable_path; use executable_path::executable_path;
use std::path::Path;
use std::process; use std::process;
use std::str; use std::str;
use std::path::Path;
use tempdir::TempDir; use tempdir::TempDir;
#[cfg(unix)] #[cfg(unix)]
fn to_shell_path(path: &Path) -> String { fn to_shell_path(path: &Path) -> String {
use std::fs; use std::fs;
fs::canonicalize(path).expect("canonicalize failed") fs::canonicalize(path)
.to_str().map(str::to_string).expect("unicode decode failed") .expect("canonicalize failed")
.to_str()
.map(str::to_string)
.expect("unicode decode failed")
} }
#[cfg(windows)] #[cfg(windows)]
@ -27,13 +30,19 @@ fn to_shell_path(path: &Path) -> String {
#[test] #[test]
fn test_invocation_directory() { fn test_invocation_directory() {
let tmp = TempDir::new("just-integration") let tmp = TempDir::new("just-integration").unwrap_or_else(|err| {
.unwrap_or_else( panic!(
|err| panic!("integration test: failed to create temporary directory: {}", err)); "integration test: failed to create temporary directory: {}",
err
)
});
let mut justfile_path = tmp.path().to_path_buf(); let mut justfile_path = tmp.path().to_path_buf();
justfile_path.push("justfile"); justfile_path.push("justfile");
brev::dump(justfile_path, "default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}"); brev::dump(
justfile_path,
"default:\n @cd {{invocation_directory()}}\n @echo {{invocation_directory()}}",
);
let mut subdir = tmp.path().to_path_buf(); let mut subdir = tmp.path().to_path_buf();
subdir.push("subdir"); subdir.push("subdir");
@ -48,8 +57,7 @@ fn test_invocation_directory() {
let mut failure = false; let mut failure = false;
let expected_status = 0; let expected_status = 0;
let expected_stdout = let expected_stdout = to_shell_path(&subdir) + "\n";
to_shell_path(&subdir) + "\n";
let expected_stderr = ""; let expected_stderr = "";
let status = output.status.code().unwrap(); let status = output.status.code().unwrap();
@ -60,13 +68,19 @@ fn test_invocation_directory() {
let stdout = str::from_utf8(&output.stdout).unwrap(); let stdout = str::from_utf8(&output.stdout).unwrap();
if stdout != expected_stdout { if stdout != expected_stdout {
println!("bad stdout:\ngot:\n{:?}\n\nexpected:\n{:?}", stdout, expected_stdout); println!(
"bad stdout:\ngot:\n{:?}\n\nexpected:\n{:?}",
stdout, expected_stdout
);
failure = true; failure = true;
} }
let stderr = str::from_utf8(&output.stderr).unwrap(); let stderr = str::from_utf8(&output.stderr).unwrap();
if stderr != expected_stderr { if stderr != expected_stderr {
println!("bad stderr:\ngot:\n{:?}\n\nexpected:\n{:?}", stderr, expected_stderr); println!(
"bad stderr:\ngot:\n{:?}\n\nexpected:\n{:?}",
stderr, expected_stderr
);
failure = true; failure = true;
} }

View File

@ -2,9 +2,9 @@ extern crate brev;
extern crate executable_path; extern crate executable_path;
extern crate tempdir; extern crate tempdir;
use tempdir::TempDir;
use std::{path, str, fs, process};
use executable_path::executable_path; use executable_path::executable_path;
use std::{fs, path, process, str};
use tempdir::TempDir;
fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) { fn search_test<P: AsRef<path::Path>>(path: P, args: &[&str]) {
let binary = executable_path("just"); let binary = executable_path("just");