From 2b6b715528be5005ac3e4788cca67aaaa9853c68 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 16 Nov 2017 23:30:08 -0800 Subject: [PATCH] Refactor Everything (#250) --- Cargo.lock | 5 + Cargo.toml | 3 + justfile | 19 +- src/assignment_evaluator.rs | 179 ++ src/assignment_resolver.rs | 129 ++ src/color.rs | 3 +- src/command_ext.rs | 28 + src/compilation_error.rs | 141 ++ src/configuration.rs | 29 + src/cooked_string.rs | 48 + src/expression.rs | 49 + src/fragment.rs | 17 + src/justfile.rs | 345 ++++ src/lib.rs | 2163 --------------------- src/main.rs | 81 +- src/misc.rs | 149 ++ src/parameter.rs | 25 + src/parser.rs | 848 ++++++++ src/platform.rs | 16 +- src/range_ext.rs | 25 + src/recipe.rs | 281 +++ src/recipe_resolver.rs | 171 ++ src/{app.rs => run.rs} | 36 +- src/runtime_error.rs | 201 ++ src/shebang.rs | 60 + src/testing.rs | 25 + src/token.rs | 71 + src/tokenizer.rs | 585 ++++++ src/unit.rs | 1144 ----------- {src => tests}/integration.rs | 14 +- {src => tests}/search.rs | 11 +- utilities/Cargo.toml | 5 + src/test_utils.rs => utilities/src/lib.rs | 4 +- 33 files changed, 3566 insertions(+), 3344 deletions(-) create mode 100644 src/assignment_evaluator.rs create mode 100644 src/assignment_resolver.rs create mode 100644 src/command_ext.rs create mode 100644 src/compilation_error.rs create mode 100644 src/configuration.rs create mode 100644 src/cooked_string.rs create mode 100644 src/expression.rs create mode 100644 src/fragment.rs create mode 100644 src/justfile.rs delete mode 100644 src/lib.rs create mode 100644 src/misc.rs create mode 100644 src/parameter.rs create mode 100644 src/parser.rs create mode 100644 src/range_ext.rs create mode 100644 src/recipe.rs create mode 100644 src/recipe_resolver.rs rename src/{app.rs => run.rs} (94%) create mode 100644 src/runtime_error.rs create mode 100644 src/shebang.rs create mode 100644 src/testing.rs create mode 100644 src/token.rs create mode 100644 src/tokenizer.rs delete mode 100644 src/unit.rs rename {src => tests}/integration.rs (99%) rename {src => tests}/search.rs (96%) create mode 100644 utilities/Cargo.toml rename src/test_utils.rs => utilities/src/lib.rs (85%) diff --git a/Cargo.lock b/Cargo.lock index 0495733..f9093cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,6 +13,7 @@ dependencies = [ "regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "utilities 0.0.0", ] [[package]] @@ -247,6 +248,10 @@ name = "utf8-ranges" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "utilities" +version = "0.0.0" + [[package]] name = "vec_map" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 340f9f9..5d098ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ libc = "^0.2.21" regex = "^0.2.2" tempdir = "^0.3.5" unicode-width = "^0.1.3" + +[dev-dependencies.utilities] +path = "utilities" diff --git a/justfile b/justfile index 82030f3..ff540df 100644 --- a/justfile +++ b/justfile @@ -1,13 +1,24 @@ +bt='0' + +export RUST_BACKTRACE=bt + test: build - cargo test --lib + cargo test + +@spam: + { \ + figlet test; \ + cargo build --color always 2>&1; \ + cargo test --color always -- --color always 2>&1; \ + } | less # only run tests matching PATTERN filter PATTERN: build - cargo test --lib {{PATTERN}} + cargo test {{PATTERN}} # test with backtrace backtrace: - RUST_BACKTRACE=1 cargo test --lib + RUST_BACKTRACE=1 cargo test build: cargo build @@ -63,7 +74,7 @@ sloc: echo Checking for long lines... ! grep --color -En '.{101}' src/*.rs -rename FROM TO: +replace FROM TO: find src -name '*.rs' | xargs sed -i '' -E 's/{{FROM}}/{{TO}}/g' nop: diff --git a/src/assignment_evaluator.rs b/src/assignment_evaluator.rs new file mode 100644 index 0000000..5ccef09 --- /dev/null +++ b/src/assignment_evaluator.rs @@ -0,0 +1,179 @@ +use common::*; + +use brev; + +pub fn evaluate_assignments<'a>( + assignments: &Map<&'a str, Expression<'a>>, + overrides: &Map<&str, &str>, + quiet: bool, + shell: &'a str, +) -> Result, RuntimeError<'a>> { + let mut evaluator = AssignmentEvaluator { + assignments: assignments, + evaluated: empty(), + exports: &empty(), + overrides: overrides, + quiet: quiet, + scope: &empty(), + shell: shell, + }; + + for name in assignments.keys() { + evaluator.evaluate_assignment(name)?; + } + + Ok(evaluator.evaluated) +} + +fn run_backtick<'a, 'b>( + raw: &str, + token: &Token<'a>, + scope: &Map<&'a str, String>, + exports: &Set<&'a str>, + quiet: bool, + shell: &'b str, +) -> Result> { + let mut cmd = Command::new(shell); + + cmd.export_environment_variables(scope, exports)?; + + cmd.arg("-cu") + .arg(raw); + + cmd.stderr(if quiet { + process::Stdio::null() + } else { + process::Stdio::inherit() + }); + + brev::output(cmd).map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error}) +} + +pub struct AssignmentEvaluator<'a: 'b, 'b> { + pub assignments: &'b Map<&'a str, Expression<'a>>, + pub evaluated: Map<&'a str, String>, + pub exports: &'b Set<&'a str>, + pub overrides: &'b Map<&'b str, &'b str>, + pub quiet: bool, + pub scope: &'b Map<&'a str, String>, + pub shell: &'b str, +} + +impl<'a, 'b> AssignmentEvaluator<'a, 'b> { + pub fn evaluate_line( + &mut self, + line: &[Fragment<'a>], + arguments: &Map<&str, Cow> + ) -> Result> { + let mut evaluated = String::new(); + for fragment in line { + match *fragment { + Fragment::Text{ref text} => evaluated += text.lexeme, + Fragment::Expression{ref expression} => { + evaluated += &self.evaluate_expression(expression, arguments)?; + } + } + } + Ok(evaluated) + } + + fn evaluate_assignment(&mut self, name: &'a str) -> Result<(), RuntimeError<'a>> { + if self.evaluated.contains_key(name) { + return Ok(()); + } + + if let Some(expression) = self.assignments.get(name) { + if let Some(value) = self.overrides.get(name) { + self.evaluated.insert(name, value.to_string()); + } else { + let value = self.evaluate_expression(expression, &empty())?; + self.evaluated.insert(name, value); + } + } else { + return Err(RuntimeError::Internal { + message: format!("attempted to evaluated unknown assignment {}", name) + }); + } + + Ok(()) + } + + fn evaluate_expression( + &mut self, + expression: &Expression<'a>, + arguments: &Map<&str, Cow> + ) -> Result> { + Ok(match *expression { + Expression::Variable{name, ..} => { + if self.evaluated.contains_key(name) { + self.evaluated[name].clone() + } else if self.scope.contains_key(name) { + self.scope[name].clone() + } else if self.assignments.contains_key(name) { + self.evaluate_assignment(name)?; + self.evaluated[name].clone() + } else if arguments.contains_key(name) { + arguments[name].to_string() + } else { + return Err(RuntimeError::Internal { + message: format!("attempted to evaluate undefined variable `{}`", name) + }); + } + } + Expression::String{ref cooked_string} => cooked_string.cooked.clone(), + Expression::Backtick{raw, ref token} => { + run_backtick(raw, token, self.scope, self.exports, self.quiet, self.shell)? + } + Expression::Concatination{ref lhs, ref rhs} => { + self.evaluate_expression(lhs, arguments)? + + + &self.evaluate_expression(rhs, arguments)? + } + }) + } +} + + +#[cfg(test)] +mod test { + use super::*; + use brev::OutputError; + use testing::parse_success; + use Configuration; + +#[test] +fn backtick_code() { + match parse_success("a:\n echo {{`f() { return 100; }; f`}}") + .run(&["a"], &Default::default()).unwrap_err() { + RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => { + assert_eq!(code, 100); + assert_eq!(token.lexeme, "`f() { return 100; }; f`"); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn export_assignment_backtick() { + let text = r#" +export exported_variable = "A" +b = `echo $exported_variable` + +recipe: + echo {{b}} +"#; + + let options = Configuration { + quiet: true, + ..Default::default() + }; + + match parse_success(text).run(&["recipe"], &options).unwrap_err() { + RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => { + assert_eq!(token.lexeme, "`echo $exported_variable`"); + }, + other => panic!("expected a backtick code errror, but got: {}", other), + } +} + +} diff --git a/src/assignment_resolver.rs b/src/assignment_resolver.rs new file mode 100644 index 0000000..61ddbe0 --- /dev/null +++ b/src/assignment_resolver.rs @@ -0,0 +1,129 @@ +use common::*; + +pub fn resolve_assignments<'a>( + assignments: &Map<&'a str, Expression<'a>>, + assignment_tokens: &Map<&'a str, Token<'a>>, +) -> Result<(), CompilationError<'a>> { + + let mut resolver = AssignmentResolver { + assignments: assignments, + assignment_tokens: assignment_tokens, + stack: empty(), + seen: empty(), + evaluated: empty(), + }; + + for name in assignments.keys() { + resolver.resolve_assignment(name)?; + } + + Ok(()) +} + +struct AssignmentResolver<'a: 'b, 'b> { + assignments: &'b Map<&'a str, Expression<'a>>, + assignment_tokens: &'b Map<&'a str, Token<'a>>, + stack: Vec<&'a str>, + seen: Set<&'a str>, + evaluated: Set<&'a str>, +} + +impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> { + fn resolve_assignment(&mut self, name: &'a str) -> Result<(), CompilationError<'a>> { + if self.evaluated.contains(name) { + return Ok(()); + } + + self.seen.insert(name); + self.stack.push(name); + + if let Some(expression) = self.assignments.get(name) { + self.resolve_expression(expression)?; + self.evaluated.insert(name); + } else { + let message = format!("attempted to resolve unknown assignment `{}`", name); + return Err(CompilationError { + text: "", + index: 0, + line: 0, + column: 0, + width: None, + kind: CompilationErrorKind::Internal{message} + }); + } + Ok(()) + } + + fn resolve_expression(&mut self, expression: &Expression<'a>) -> Result<(), CompilationError<'a>> { + match *expression { + Expression::Variable{name, ref token} => { + if self.evaluated.contains(name) { + return Ok(()); + } else if self.seen.contains(name) { + let token = &self.assignment_tokens[name]; + self.stack.push(name); + return Err(token.error(CompilationErrorKind::CircularVariableDependency { + variable: name, + circle: self.stack.clone(), + })); + } else if self.assignments.contains_key(name) { + self.resolve_assignment(name)?; + } else { + return Err(token.error(CompilationErrorKind::UndefinedVariable{variable: name})); + } + } + Expression::Concatination{ref lhs, ref rhs} => { + self.resolve_expression(lhs)?; + self.resolve_expression(rhs)?; + } + Expression::String{..} | Expression::Backtick{..} => {} + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use testing::parse_error; + use super::*; + +#[test] +fn circular_variable_dependency() { + let text = "a = b\nb = a"; + parse_error(text, CompilationError { + text: text, + index: 0, + line: 0, + column: 0, + width: Some(1), + kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]} + }); +} + + +#[test] +fn self_variable_dependency() { + let text = "a = a"; + parse_error(text, CompilationError { + text: text, + index: 0, + line: 0, + column: 0, + width: Some(1), + kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "a"]} + }); +} +#[test] +fn unknown_expression_variable() { + let text = "x = yy"; + parse_error(text, CompilationError { + text: text, + index: 4, + line: 0, + column: 4, + width: Some(2), + kind: CompilationErrorKind::UndefinedVariable{variable: "yy"}, + }); +} + +} diff --git a/src/color.rs b/src/color.rs index 9b89144..053c5d6 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,7 +1,8 @@ extern crate ansi_term; extern crate atty; -use prelude::*; +use common::*; + use self::ansi_term::{Style, Prefix, Suffix, ANSIGenericString}; use self::ansi_term::Color::*; use self::atty::is as is_atty; diff --git a/src/command_ext.rs b/src/command_ext.rs new file mode 100644 index 0000000..1f38253 --- /dev/null +++ b/src/command_ext.rs @@ -0,0 +1,28 @@ +use common::*; + +pub trait CommandExt { + fn export_environment_variables<'a>( + &mut self, + scope: &Map<&'a str, String>, + exports: &Set<&'a str> + ) -> Result<(), RuntimeError<'a>>; +} + +impl CommandExt for Command { + fn export_environment_variables<'a>( + &mut self, + scope: &Map<&'a str, String>, + exports: &Set<&'a str> + ) -> Result<(), RuntimeError<'a>> { + for name in exports { + if let Some(value) = scope.get(name) { + self.env(name, value); + } else { + return Err(RuntimeError::Internal { + message: format!("scope does not contain exported variable `{}`", name), + }); + } + } + Ok(()) + } +} diff --git a/src/compilation_error.rs b/src/compilation_error.rs new file mode 100644 index 0000000..9801384 --- /dev/null +++ b/src/compilation_error.rs @@ -0,0 +1,141 @@ +use common::*; + +use misc::{Or, write_error_context, show_whitespace}; + +#[derive(Debug, PartialEq)] +pub struct CompilationError<'a> { + pub text: &'a str, + pub index: usize, + pub line: usize, + pub column: usize, + pub width: Option, + pub kind: CompilationErrorKind<'a>, +} + +#[derive(Debug, PartialEq)] +pub enum CompilationErrorKind<'a> { + CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>}, + CircularVariableDependency{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, + InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, + Internal{message: String}, + InvalidEscapeSequence{character: char}, + MixedLeadingWhitespace{whitespace: &'a str}, + OuterShebang, + ParameterShadowsVariable{parameter: &'a str}, + RequiredParameterFollowsDefaultParameter{parameter: &'a str}, + ParameterFollowsVariadicParameter{parameter: &'a str}, + UndefinedVariable{variable: &'a str}, + UnexpectedToken{expected: Vec, found: TokenKind}, + UnknownDependency{recipe: &'a str, unknown: &'a str}, + UnknownStartOfToken, + UnterminatedString, +} + +impl<'a> Display for CompilationError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use CompilationErrorKind::*; + let error = Color::fmt(f).error(); + let message = Color::fmt(f).message(); + + write!(f, "{} {}", error.paint("error:"), message.prefix())?; + + match self.kind { + CircularRecipeDependency{recipe, ref circle} => { + if circle.len() == 2 { + writeln!(f, "Recipe `{}` depends on itself", recipe)?; + } else { + writeln!(f, "Recipe `{}` has circular dependency `{}`", + recipe, circle.join(" -> "))?; + } + } + CircularVariableDependency{variable, ref circle} => { + if circle.len() == 2 { + writeln!(f, "Variable `{}` is defined in terms of itself", variable)?; + } else { + writeln!(f, "Variable `{}` depends on its own value: `{}`", + variable, circle.join(" -> "))?; + } + } + InvalidEscapeSequence{character} => { + writeln!(f, "`\\{}` is not a valid escape sequence", + character.escape_default().collect::())?; + } + DuplicateParameter{recipe, parameter} => { + writeln!(f, "Recipe `{}` has duplicate parameter `{}`", recipe, parameter)?; + } + DuplicateVariable{variable} => { + writeln!(f, "Variable `{}` has multiple definitions", variable)?; + } + UnexpectedToken{ref expected, found} => { + writeln!(f, "Expected {}, but found {}", Or(expected), found)?; + } + DuplicateDependency{recipe, dependency} => { + writeln!(f, "Recipe `{}` has duplicate dependency `{}`", recipe, dependency)?; + } + DuplicateRecipe{recipe, first} => { + writeln!(f, "Recipe `{}` first defined on line {} is redefined on line {}", + recipe, first + 1, self.line + 1)?; + } + DependencyHasParameters{recipe, dependency} => { + writeln!(f, "Recipe `{}` depends on `{}` which requires arguments. \ + Dependencies may not require arguments", recipe, dependency)?; + } + ParameterShadowsVariable{parameter} => { + writeln!(f, "Parameter `{}` shadows variable of the same name", parameter)?; + } + RequiredParameterFollowsDefaultParameter{parameter} => { + writeln!(f, "Non-default parameter `{}` follows default parameter", parameter)?; + } + ParameterFollowsVariadicParameter{parameter} => { + writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?; + } + MixedLeadingWhitespace{whitespace} => { + writeln!(f, + "Found a mix of tabs and spaces in leading whitespace: `{}`\n\ + Leading whitespace may consist of tabs or spaces, but not both", + show_whitespace(whitespace) + )?; + } + ExtraLeadingWhitespace => { + writeln!(f, "Recipe line has extra leading whitespace")?; + } + InconsistentLeadingWhitespace{expected, found} => { + writeln!(f, + "Recipe line has inconsistent leading whitespace. \ + Recipe started with `{}` but found line with `{}`", + show_whitespace(expected), show_whitespace(found) + )?; + } + OuterShebang => { + writeln!(f, "`#!` is reserved syntax outside of recipes")?; + } + UnknownDependency{recipe, unknown} => { + writeln!(f, "Recipe `{}` has unknown dependency `{}`", recipe, unknown)?; + } + UndefinedVariable{variable} => { + writeln!(f, "Variable `{}` not defined", variable)?; + } + UnknownStartOfToken => { + writeln!(f, "Unknown start of token:")?; + } + UnterminatedString => { + writeln!(f, "Unterminated string")?; + } + Internal{ref message} => { + writeln!(f, "Internal error, this may indicate a bug in just: {}\n\ + consider filing an issue: https://github.com/casey/just/issues/new", + message)?; + } + } + + write!(f, "{}", message.suffix())?; + + write_error_context(f, self.text, self.index, self.line, self.column, self.width) + } +} diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..a35fb98 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,29 @@ +use common::*; + +pub const DEFAULT_SHELL: &'static str = "sh"; + +pub struct Configuration<'a> { + pub dry_run: bool, + pub evaluate: bool, + pub highlight: bool, + pub overrides: Map<&'a str, &'a str>, + pub quiet: bool, + pub shell: &'a str, + pub color: Color, + pub verbose: bool, +} + +impl<'a> Default for Configuration<'a> { + fn default() -> Configuration<'static> { + Configuration { + dry_run: false, + evaluate: false, + highlight: false, + overrides: empty(), + quiet: false, + shell: DEFAULT_SHELL, + color: default(), + verbose: false, + } + } +} diff --git a/src/cooked_string.rs b/src/cooked_string.rs new file mode 100644 index 0000000..77c878d --- /dev/null +++ b/src/cooked_string.rs @@ -0,0 +1,48 @@ +use common::*; + +#[derive(PartialEq, Debug)] +pub struct CookedString<'a> { + pub raw: &'a str, + pub cooked: String, +} + +impl<'a> CookedString<'a> { + pub fn new(token: &Token<'a>) -> Result, CompilationError<'a>> { + let raw = &token.lexeme[1..token.lexeme.len()-1]; + + if let TokenKind::RawString = token.kind { + Ok(CookedString{raw: raw, cooked: raw.to_string()}) + } else if let TokenKind::StringToken = token.kind { + let mut cooked = String::new(); + let mut escape = false; + for c in raw.chars() { + if escape { + match c { + 'n' => cooked.push('\n'), + 'r' => cooked.push('\r'), + 't' => cooked.push('\t'), + '\\' => cooked.push('\\'), + '"' => cooked.push('"'), + other => return Err(token.error(CompilationErrorKind::InvalidEscapeSequence { + character: other, + })), + } + escape = false; + continue; + } + if c == '\\' { + escape = true; + continue; + } + cooked.push(c); + } + Ok(CookedString{raw: raw, cooked: cooked}) + } else { + Err(token.error(CompilationErrorKind::Internal { + message: "cook_string() called on non-string token".to_string() + })) + } + } +} + + diff --git a/src/expression.rs b/src/expression.rs new file mode 100644 index 0000000..1172eb0 --- /dev/null +++ b/src/expression.rs @@ -0,0 +1,49 @@ +use common::*; + +#[derive(PartialEq, Debug)] +pub enum Expression<'a> { + Variable{name: &'a str, token: Token<'a>}, + String{cooked_string: CookedString<'a>}, + Backtick{raw: &'a str, token: Token<'a>}, + Concatination{lhs: Box>, rhs: Box>}, +} + +impl<'a> Expression<'a> { + pub fn variables(&'a self) -> Variables<'a> { + Variables { + stack: vec![self], + } + } +} + +impl<'a> Display for Expression<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?, + Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, + Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?, + Expression::Variable {name, .. } => write!(f, "{}", name)?, + } + Ok(()) + } +} + +pub struct Variables<'a> { + stack: Vec<&'a Expression<'a>>, +} + +impl<'a> Iterator for Variables<'a> { + type Item = &'a Token<'a>; + + fn next(&mut self) -> Option<&'a Token<'a>> { + match self.stack.pop() { + None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None, + Some(&Expression::Variable{ref token,..}) => Some(token), + Some(&Expression::Concatination{ref lhs, ref rhs}) => { + self.stack.push(lhs); + self.stack.push(rhs); + self.next() + } + } + } +} diff --git a/src/fragment.rs b/src/fragment.rs new file mode 100644 index 0000000..323e5f0 --- /dev/null +++ b/src/fragment.rs @@ -0,0 +1,17 @@ +use common::*; + +#[derive(PartialEq, Debug)] +pub enum Fragment<'a> { + Text{text: Token<'a>}, + Expression{expression: Expression<'a>}, +} + +impl<'a> Fragment<'a> { + pub fn continuation(&self) -> bool { + match *self { + Fragment::Text{ref text} => text.lexeme.ends_with('\\'), + _ => false, + } + } +} + diff --git a/src/justfile.rs b/src/justfile.rs new file mode 100644 index 0000000..02645d8 --- /dev/null +++ b/src/justfile.rs @@ -0,0 +1,345 @@ +use common::*; + +use edit_distance::edit_distance; +use assignment_evaluator::evaluate_assignments; +use range_ext::RangeExt; + +pub struct Justfile<'a> { + pub recipes: Map<&'a str, Recipe<'a>>, + pub assignments: Map<&'a str, Expression<'a>>, + pub exports: Set<&'a str>, +} + +impl<'a, 'b> Justfile<'a> where 'a: 'b { + pub fn first(&self) -> Option<&Recipe> { + let mut first: Option<&Recipe> = None; + for recipe in self.recipes.values() { + if let Some(first_recipe) = first { + if recipe.line_number < first_recipe.line_number { + first = Some(recipe) + } + } else { + first = Some(recipe); + } + } + first + } + + pub fn count(&self) -> usize { + self.recipes.len() + } + + pub fn suggest(&self, name: &str) -> Option<&'a str> { + let mut suggestions = self.recipes.keys() + .map(|suggestion| (edit_distance(suggestion, name), suggestion)) + .collect::>(); + suggestions.sort(); + if let Some(&(distance, suggestion)) = suggestions.first() { + if distance < 3 { + return Some(suggestion) + } + } + None + } + + pub fn run( + &'a self, + arguments: &[&'a str], + options: &Configuration<'a>, + ) -> Result<(), RuntimeError<'a>> { + let unknown_overrides = options.overrides.keys().cloned() + .filter(|name| !self.assignments.contains_key(name)) + .collect::>(); + + if !unknown_overrides.is_empty() { + return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides}); + } + + let scope = evaluate_assignments( + &self.assignments, + &options.overrides, + options.quiet, + options.shell, + )?; + + if options.evaluate { + let mut width = 0; + for name in scope.keys() { + width = cmp::max(name.len(), width); + } + + for (name, value) in scope { + println!("{0:1$} = \"{2}\"", name, width, value); + } + return Ok(()); + } + + let mut missing = vec![]; + let mut grouped = vec![]; + let mut rest = arguments; + + while let Some((argument, mut tail)) = rest.split_first() { + if let Some(recipe) = self.recipes.get(argument) { + if recipe.parameters.is_empty() { + grouped.push((recipe, &tail[0..0])); + } else { + let argument_range = recipe.argument_range(); + let argument_count = cmp::min(tail.len(), recipe.max_arguments()); + if !argument_range.range_contains(argument_count) { + return Err(RuntimeError::ArgumentCountMismatch { + recipe: recipe.name, + found: tail.len(), + min: recipe.min_arguments(), + max: recipe.max_arguments(), + }); + } + grouped.push((recipe, &tail[0..argument_count])); + tail = &tail[argument_count..]; + } + } else { + missing.push(*argument); + } + rest = tail; + } + + if !missing.is_empty() { + let suggestion = if missing.len() == 1 { + self.suggest(missing.first().unwrap()) + } else { + None + }; + return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion: suggestion}); + } + + let mut ran = empty(); + for (recipe, arguments) in grouped { + self.run_recipe(recipe, arguments, &scope, &mut ran, options)? + } + + Ok(()) + } + + fn run_recipe<'c>( + &'c self, + recipe: &Recipe<'a>, + arguments: &[&'a str], + scope: &Map<&'c str, String>, + ran: &mut Set<&'a str>, + options: &Configuration<'a>, + ) -> Result<(), RuntimeError> { + for dependency_name in &recipe.dependencies { + if !ran.contains(dependency_name) { + self.run_recipe(&self.recipes[dependency_name], &[], scope, ran, options)?; + } + } + recipe.run(arguments, scope, &self.exports, options)?; + ran.insert(recipe.name); + Ok(()) + } +} + +impl<'a> Display for Justfile<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let mut items = self.recipes.len() + self.assignments.len(); + for (name, expression) in &self.assignments { + if self.exports.contains(name) { + write!(f, "export ")?; + } + write!(f, "{} = {}", name, expression)?; + items -= 1; + if items != 0 { + write!(f, "\n\n")?; + } + } + for recipe in self.recipes.values() { + write!(f, "{}", recipe)?; + items -= 1; + if items != 0 { + write!(f, "\n\n")?; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use testing::parse_success; + + #[test] + fn unknown_recipes() { + match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() { + RuntimeError::UnknownRecipes{recipes, suggestion} => { + assert_eq!(recipes, &["x", "y", "z"]); + assert_eq!(suggestion, None); + } + other => panic!("expected an unknown recipe error, but got: {}", other), + } + } + + +#[test] +fn run_shebang() { + // this test exists to make sure that shebang recipes + // run correctly. although this script is still + // executed by a shell its behavior depends on the value of a + // variable and continuing even though a command fails, + // whereas in plain recipes variables are not available + // in subsequent lines and execution stops when a line + // fails + let text = " +a: + #!/usr/bin/env sh + code=200 + x() { return $code; } + x + x +"; + + match parse_success(text).run(&["a"], &Default::default()).unwrap_err() { + RuntimeError::Code{recipe, line_number, code} => { + assert_eq!(recipe, "a"); + assert_eq!(code, 200); + assert_eq!(line_number, None); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} +#[test] +fn code_error() { + match parse_success("fail:\n @exit 100") + .run(&["fail"], &Default::default()).unwrap_err() { + RuntimeError::Code{recipe, line_number, code} => { + assert_eq!(recipe, "fail"); + assert_eq!(code, 100); + assert_eq!(line_number, Some(2)); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn run_args() { + let text = r#" +a return code: + @x() { {{return}} {{code + "0"}}; }; x"#; + + match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() { + RuntimeError::Code{recipe, line_number, code} => { + assert_eq!(recipe, "a"); + assert_eq!(code, 150); + assert_eq!(line_number, Some(3)); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn missing_some_arguments() { + match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() { + RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { + assert_eq!(recipe, "a"); + assert_eq!(found, 2); + assert_eq!(min, 3); + assert_eq!(max, 3); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn missing_some_arguments_variadic() { + match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() { + RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { + assert_eq!(recipe, "a"); + assert_eq!(found, 2); + assert_eq!(min, 3); + assert_eq!(max, usize::MAX - 1); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn missing_all_arguments() { + match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}") + .run(&["a"], &Default::default()).unwrap_err() { + RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { + assert_eq!(recipe, "a"); + assert_eq!(found, 0); + assert_eq!(min, 3); + assert_eq!(max, 3); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn missing_some_defaults() { + match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() { + RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { + assert_eq!(recipe, "a"); + assert_eq!(found, 1); + assert_eq!(min, 2); + assert_eq!(max, 3); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn missing_all_defaults() { + match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() { + RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { + assert_eq!(recipe, "a"); + assert_eq!(found, 0); + assert_eq!(min, 1); + assert_eq!(max, 3); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn unknown_overrides() { + let mut options: Configuration = Default::default(); + options.overrides.insert("foo", "bar"); + options.overrides.insert("baz", "bob"); + match parse_success("a:\n echo {{`f() { return 100; }; f`}}") + .run(&["a"], &options).unwrap_err() { + RuntimeError::UnknownOverrides{overrides} => { + assert_eq!(overrides, &["baz", "foo"]); + }, + other => panic!("expected a code run error, but got: {}", other), + } +} + +#[test] +fn export_failure() { + let text = r#" +export foo = "a" +baz = "c" +export bar = "b" +export abc = foo + bar + baz + +wut: + echo $foo $bar $baz +"#; + + let options = Configuration { + quiet: true, + ..Default::default() + }; + + match parse_success(text).run(&["wut"], &options).unwrap_err() { + RuntimeError::Code{code: _, line_number, recipe} => { + assert_eq!(recipe, "wut"); + assert_eq!(line_number, Some(8)); + }, + other => panic!("expected a recipe code errror, but got: {}", other), + } +} + + +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index bb3e3f8..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,2163 +0,0 @@ -#[macro_use] -extern crate lazy_static; -extern crate regex; -extern crate tempdir; -extern crate itertools; -extern crate ansi_term; -extern crate unicode_width; -extern crate edit_distance; -extern crate libc; -extern crate brev; - -#[cfg(test)] -mod test_utils; - -#[cfg(test)] -mod unit; - -#[cfg(test)] -mod integration; - -#[cfg(test)] -mod search; - -mod platform; - -mod app; - -mod color; - -mod prelude { - pub use libc::{EXIT_FAILURE, EXIT_SUCCESS}; - pub use regex::Regex; - pub use std::io::prelude::*; - pub use std::path::{Path, PathBuf}; - pub use std::{cmp, env, fs, fmt, io, iter, process}; - - pub fn default() -> T { - Default::default() - } -} - -use prelude::*; - -pub use app::app; - -use brev::{output, OutputError}; -use color::Color; -use platform::{Platform, PlatformInterface}; -use std::borrow::Cow; -use std::collections::{BTreeMap as Map, BTreeSet as Set}; -use std::fmt::Display; -use std::ops::Range; - -const DEFAULT_SHELL: &'static str = "sh"; - -trait Slurp { - fn slurp(&mut self) -> Result; -} - -impl Slurp for fs::File { - fn slurp(&mut self) -> Result { - let mut destination = String::new(); - self.read_to_string(&mut destination)?; - Ok(destination) - } -} - -/// Split a shebang line into a command and an optional argument -fn split_shebang(shebang: &str) -> Option<(&str, Option<&str>)> { - lazy_static! { - static ref EMPTY: Regex = re(r"^#!\s*$"); - static ref SIMPLE: Regex = re(r"^#!(\S+)\s*$"); - static ref ARGUMENT: Regex = re(r"^#!(\S+)\s+(\S.*?)?\s*$"); - } - - if EMPTY.is_match(shebang) { - Some(("", None)) - } else if let Some(captures) = SIMPLE.captures(shebang) { - Some((captures.get(1).unwrap().as_str(), None)) - } else if let Some(captures) = ARGUMENT.captures(shebang) { - Some((captures.get(1).unwrap().as_str(), Some(captures.get(2).unwrap().as_str()))) - } else { - None - } -} - -fn re(pattern: &str) -> Regex { - Regex::new(pattern).unwrap() -} - -fn empty>() -> C { - iter::empty().collect() -} - -fn contains(range: &Range, i: T) -> bool { - i >= range.start && i < range.end -} - -#[derive(PartialEq, Debug)] -struct Recipe<'a> { - dependencies: Vec<&'a str>, - dependency_tokens: Vec>, - doc: Option<&'a str>, - line_number: usize, - lines: Vec>>, - name: &'a str, - parameters: Vec>, - private: bool, - quiet: bool, - shebang: bool, -} - -#[derive(PartialEq, Debug)] -struct Parameter<'a> { - default: Option, - name: &'a str, - token: Token<'a>, - variadic: bool, -} - -impl<'a> Display for Parameter<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let color = Color::fmt(f); - if self.variadic { - write!(f, "{}", color.annotation().paint("+"))?; - } - write!(f, "{}", color.parameter().paint(self.name))?; - if let Some(ref default) = self.default { - let escaped = default.chars().flat_map(char::escape_default).collect::();; - write!(f, r#"='{}'"#, color.string().paint(&escaped))?; - } - Ok(()) - } -} - -#[derive(PartialEq, Debug)] -enum Fragment<'a> { - Text{text: Token<'a>}, - Expression{expression: Expression<'a>}, -} - -impl<'a> Fragment<'a> { - fn continuation(&self) -> bool { - match *self { - Fragment::Text{ref text} => text.lexeme.ends_with('\\'), - _ => false, - } - } -} - -#[derive(PartialEq, Debug)] -enum Expression<'a> { - Variable{name: &'a str, token: Token<'a>}, - String{cooked_string: CookedString<'a>}, - Backtick{raw: &'a str, token: Token<'a>}, - Concatination{lhs: Box>, rhs: Box>}, -} - -impl<'a> Expression<'a> { - fn variables(&'a self) -> Variables<'a> { - Variables { - stack: vec![self], - } - } -} - -struct Variables<'a> { - stack: Vec<&'a Expression<'a>>, -} - -impl<'a> Iterator for Variables<'a> { - type Item = &'a Token<'a>; - - fn next(&mut self) -> Option<&'a Token<'a>> { - match self.stack.pop() { - None | Some(&Expression::String{..}) | Some(&Expression::Backtick{..}) => None, - Some(&Expression::Variable{ref token,..}) => Some(token), - Some(&Expression::Concatination{ref lhs, ref rhs}) => { - self.stack.push(lhs); - self.stack.push(rhs); - self.next() - } - } - } -} - -impl<'a> Display for Expression<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - match *self { - Expression::Backtick {raw, .. } => write!(f, "`{}`", raw)?, - Expression::Concatination{ref lhs, ref rhs } => write!(f, "{} + {}", lhs, rhs)?, - Expression::String {ref cooked_string} => write!(f, "\"{}\"", cooked_string.raw)?, - Expression::Variable {name, .. } => write!(f, "{}", name)?, - } - Ok(()) - } -} - -/// Return a `RuntimeError::Signal` if the process was terminated by a signal, -/// otherwise return an `RuntimeError::UnknownFailure` -fn error_from_signal( - recipe: &str, - line_number: Option, - exit_status: process::ExitStatus -) -> RuntimeError { - match Platform::signal_from_exit_status(exit_status) { - Some(signal) => RuntimeError::Signal{recipe: recipe, line_number: line_number, signal: signal}, - None => RuntimeError::UnknownFailure{recipe: recipe, line_number: line_number}, - } -} - -fn export_env<'a>( - command: &mut process::Command, - scope: &Map<&'a str, String>, - exports: &Set<&'a str>, -) -> Result<(), RuntimeError<'a>> { - for name in exports { - if let Some(value) = scope.get(name) { - command.env(name, value); - } else { - return Err(RuntimeError::InternalError { - message: format!("scope does not contain exported variable `{}`", name), - }); - } - } - Ok(()) -} - -fn run_backtick<'a>( - raw: &str, - token: &Token<'a>, - scope: &Map<&'a str, String>, - exports: &Set<&'a str>, - quiet: bool, -) -> Result> { - let mut cmd = process::Command::new(DEFAULT_SHELL); - - export_env(&mut cmd, scope, exports)?; - - cmd.arg("-cu") - .arg(raw); - - cmd.stderr(if quiet { - process::Stdio::null() - } else { - process::Stdio::inherit() - }); - - output(cmd).map_err(|output_error| RuntimeError::Backtick{token: token.clone(), output_error}) -} - -impl<'a> Recipe<'a> { - fn argument_range(&self) -> Range { - self.min_arguments()..self.max_arguments() + 1 - } - - fn min_arguments(&self) -> usize { - self.parameters.iter().filter(|p| !p.default.is_some()).count() - } - - fn max_arguments(&self) -> usize { - if self.parameters.iter().any(|p| p.variadic) { - std::usize::MAX - 1 - } else { - self.parameters.len() - } - } - - fn run( - &self, - arguments: &[&'a str], - scope: &Map<&'a str, String>, - exports: &Set<&'a str>, - options: &RunOptions, - ) -> Result<(), RuntimeError<'a>> { - if options.verbose { - let color = options.color.stderr().banner(); - eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix()); - } - - let mut argument_map = Map::new(); - - let mut rest = arguments; - for parameter in &self.parameters { - let value = if rest.is_empty() { - match parameter.default { - Some(ref default) => Cow::Borrowed(default.as_str()), - None => return Err(RuntimeError::InternalError{ - message: "missing parameter without default".to_string() - }), - } - } else if parameter.variadic { - let value = Cow::Owned(rest.to_vec().join(" ")); - rest = &[]; - value - } else { - let value = Cow::Borrowed(rest[0]); - rest = &rest[1..]; - value - }; - argument_map.insert(parameter.name, value); - } - - let mut evaluator = Evaluator { - evaluated: empty(), - scope: scope, - exports: exports, - assignments: &empty(), - overrides: &empty(), - quiet: options.quiet, - }; - - if self.shebang { - let mut evaluated_lines = vec![]; - for line in &self.lines { - evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?); - } - - if options.dry_run || self.quiet { - for line in &evaluated_lines { - eprintln!("{}", line); - } - } - - if options.dry_run { - return Ok(()); - } - - let tmp = tempdir::TempDir::new("just") - .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; - let mut path = tmp.path().to_path_buf(); - path.push(self.name); - { - let mut f = fs::File::create(&path) - .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; - let mut text = String::new(); - // add the shebang - text += &evaluated_lines[0]; - text += "\n"; - // add blank lines so that lines in the generated script - // have the same line number as the corresponding lines - // in the justfile - for _ in 1..(self.line_number + 2) { - text += "\n" - } - for line in &evaluated_lines[1..] { - text += line; - text += "\n"; - } - f.write_all(text.as_bytes()) - .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; - } - - // make the script executable - Platform::set_execute_permission(&path) - .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; - - let shebang_line = evaluated_lines.first() - .ok_or_else(|| RuntimeError::InternalError { - message: "evaluated_lines was empty".to_string() - })?; - - let (shebang_command, shebang_argument) = split_shebang(shebang_line) - .ok_or_else(|| RuntimeError::InternalError { - message: format!("bad shebang line: {}", shebang_line) - })?; - - // create a command to run the script - let mut command = Platform::make_shebang_command(&path, shebang_command, shebang_argument) - .map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error: output_error})?; - - // export environment variables - export_env(&mut command, scope, exports)?; - - // run it! - match command.status() { - Ok(exit_status) => if let Some(code) = exit_status.code() { - if code != 0 { - return Err(RuntimeError::Code{recipe: self.name, line_number: None, code: code}) - } - } else { - return Err(error_from_signal(self.name, None, exit_status)) - }, - Err(io_error) => return Err(RuntimeError::Shebang { - recipe: self.name, - command: shebang_command.to_string(), - argument: shebang_argument.map(String::from), - io_error: io_error - }) - }; - } else { - let mut lines = self.lines.iter().peekable(); - let mut line_number = self.line_number + 1; - loop { - if lines.peek().is_none() { - break; - } - let mut evaluated = String::new(); - loop { - if lines.peek().is_none() { - break; - } - let line = lines.next().unwrap(); - line_number += 1; - evaluated += &evaluator.evaluate_line(line, &argument_map)?; - if line.last().map(Fragment::continuation).unwrap_or(false) { - evaluated.pop(); - } else { - break; - } - } - let mut command = evaluated.as_str(); - let quiet_command = command.starts_with('@'); - if quiet_command { - command = &command[1..]; - } - - if command == "" { - continue; - } - - if options.dry_run || options.verbose || !((quiet_command ^ self.quiet) || options.quiet) { - let color = if options.highlight { - options.color.command() - } else { - options.color - }; - eprintln!("{}", color.stderr().paint(command)); - } - - if options.dry_run { - continue; - } - - let mut cmd = process::Command::new(options.shell.unwrap_or(DEFAULT_SHELL)); - - cmd.arg("-cu").arg(command); - - if options.quiet { - cmd.stderr(process::Stdio::null()); - cmd.stdout(process::Stdio::null()); - } - - export_env(&mut cmd, scope, exports)?; - - match cmd.status() { - Ok(exit_status) => if let Some(code) = exit_status.code() { - if code != 0 { - return Err(RuntimeError::Code{ - recipe: self.name, line_number: Some(line_number), code: code - }); - } - } else { - return Err(error_from_signal(self.name, Some(line_number), exit_status)); - }, - Err(io_error) => return Err(RuntimeError::IoError{ - recipe: self.name, io_error: io_error}), - }; - } - } - Ok(()) - } -} - -impl<'a> Display for Recipe<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - if let Some(doc) = self.doc { - writeln!(f, "# {}", doc)?; - } - write!(f, "{}", self.name)?; - for parameter in &self.parameters { - write!(f, " {}", parameter)?; - } - write!(f, ":")?; - for dependency in &self.dependencies { - write!(f, " {}", dependency)?; - } - - for (i, pieces) in self.lines.iter().enumerate() { - if i == 0 { - writeln!(f, "")?; - } - for (j, piece) in pieces.iter().enumerate() { - if j == 0 { - write!(f, " ")?; - } - match *piece { - Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?, - Fragment::Expression{ref expression, ..} => - write!(f, "{}{}{}", "{{", expression, "}}")?, - } - } - if i + 1 < self.lines.len() { - write!(f, "\n")?; - } - } - Ok(()) - } -} - -fn resolve_recipes<'a>( - recipes: &Map<&'a str, Recipe<'a>>, - assignments: &Map<&'a str, Expression<'a>>, - text: &'a str, -) -> Result<(), CompilationError<'a>> { - let mut resolver = Resolver { - seen: empty(), - stack: empty(), - resolved: empty(), - recipes: recipes, - }; - - for recipe in recipes.values() { - resolver.resolve(recipe)?; - resolver.seen = empty(); - } - - for recipe in recipes.values() { - for line in &recipe.lines { - for fragment in line { - if let Fragment::Expression{ref expression, ..} = *fragment { - for variable in expression.variables() { - let name = variable.lexeme; - let undefined = !assignments.contains_key(name) - && !recipe.parameters.iter().any(|p| p.name == name); - if undefined { - // There's a borrow issue here that seems too difficult to solve. - // The error derived from the variable token has too short a lifetime, - // so we create a new error from its contents, which do live long - // enough. - // - // I suspect the solution here is to give recipes, pieces, and expressions - // two lifetime parameters instead of one, with one being the lifetime - // of the struct, and the second being the lifetime of the tokens - // that it contains - let error = variable.error(CompilationErrorKind::UndefinedVariable{variable: name}); - return Err(CompilationError { - text: text, - index: error.index, - line: error.line, - column: error.column, - width: error.width, - kind: CompilationErrorKind::UndefinedVariable { - variable: &text[error.index..error.index + error.width.unwrap()], - } - }); - } - } - } - } - } - } - - Ok(()) -} - -struct Resolver<'a: 'b, 'b> { - stack: Vec<&'a str>, - seen: Set<&'a str>, - resolved: Set<&'a str>, - recipes: &'b Map<&'a str, Recipe<'a>>, -} - -impl<'a, 'b> Resolver<'a, 'b> { - fn resolve(&mut self, recipe: &Recipe<'a>) -> Result<(), CompilationError<'a>> { - if self.resolved.contains(recipe.name) { - return Ok(()) - } - self.stack.push(recipe.name); - self.seen.insert(recipe.name); - for dependency_token in &recipe.dependency_tokens { - match self.recipes.get(dependency_token.lexeme) { - Some(dependency) => if !self.resolved.contains(dependency.name) { - if self.seen.contains(dependency.name) { - let first = self.stack[0]; - self.stack.push(first); - return Err(dependency_token.error(CompilationErrorKind::CircularRecipeDependency { - recipe: recipe.name, - circle: self.stack.iter() - .skip_while(|name| **name != dependency.name) - .cloned().collect() - })); - } - self.resolve(dependency)?; - }, - None => return Err(dependency_token.error(CompilationErrorKind::UnknownDependency { - recipe: recipe.name, - unknown: dependency_token.lexeme - })), - } - } - self.resolved.insert(recipe.name); - self.stack.pop(); - Ok(()) - } -} - -fn resolve_assignments<'a>( - assignments: &Map<&'a str, Expression<'a>>, - assignment_tokens: &Map<&'a str, Token<'a>>, -) -> Result<(), CompilationError<'a>> { - - let mut resolver = AssignmentResolver { - assignments: assignments, - assignment_tokens: assignment_tokens, - stack: empty(), - seen: empty(), - evaluated: empty(), - }; - - for name in assignments.keys() { - resolver.resolve_assignment(name)?; - } - - Ok(()) -} - -struct AssignmentResolver<'a: 'b, 'b> { - assignments: &'b Map<&'a str, Expression<'a>>, - assignment_tokens: &'b Map<&'a str, Token<'a>>, - stack: Vec<&'a str>, - seen: Set<&'a str>, - evaluated: Set<&'a str>, -} - -impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> { - fn resolve_assignment(&mut self, name: &'a str) -> Result<(), CompilationError<'a>> { - if self.evaluated.contains(name) { - return Ok(()); - } - - self.seen.insert(name); - self.stack.push(name); - - if let Some(expression) = self.assignments.get(name) { - self.resolve_expression(expression)?; - self.evaluated.insert(name); - } else { - return Err(internal_error(format!("attempted to resolve unknown assignment `{}`", name))); - } - Ok(()) - } - - fn resolve_expression(&mut self, expression: &Expression<'a>) -> Result<(), CompilationError<'a>> { - match *expression { - Expression::Variable{name, ref token} => { - if self.evaluated.contains(name) { - return Ok(()); - } else if self.seen.contains(name) { - let token = &self.assignment_tokens[name]; - self.stack.push(name); - return Err(token.error(CompilationErrorKind::CircularVariableDependency { - variable: name, - circle: self.stack.clone(), - })); - } else if self.assignments.contains_key(name) { - self.resolve_assignment(name)?; - } else { - return Err(token.error(CompilationErrorKind::UndefinedVariable{variable: name})); - } - } - Expression::Concatination{ref lhs, ref rhs} => { - self.resolve_expression(lhs)?; - self.resolve_expression(rhs)?; - } - Expression::String{..} | Expression::Backtick{..} => {} - } - Ok(()) - } -} - -fn evaluate_assignments<'a>( - assignments: &Map<&'a str, Expression<'a>>, - overrides: &Map<&str, &str>, - quiet: bool, -) -> Result, RuntimeError<'a>> { - let mut evaluator = Evaluator { - assignments: assignments, - evaluated: empty(), - exports: &empty(), - overrides: overrides, - quiet: quiet, - scope: &empty(), - }; - - for name in assignments.keys() { - evaluator.evaluate_assignment(name)?; - } - - Ok(evaluator.evaluated) -} - -struct Evaluator<'a: 'b, 'b> { - assignments: &'b Map<&'a str, Expression<'a>>, - evaluated: Map<&'a str, String>, - exports: &'b Set<&'a str>, - overrides: &'b Map<&'b str, &'b str>, - quiet: bool, - scope: &'b Map<&'a str, String>, -} - -impl<'a, 'b> Evaluator<'a, 'b> { - fn evaluate_line( - &mut self, - line: &[Fragment<'a>], - arguments: &Map<&str, Cow> - ) -> Result> { - let mut evaluated = String::new(); - for fragment in line { - match *fragment { - Fragment::Text{ref text} => evaluated += text.lexeme, - Fragment::Expression{ref expression} => { - evaluated += &self.evaluate_expression(expression, arguments)?; - } - } - } - Ok(evaluated) - } - - fn evaluate_assignment(&mut self, name: &'a str) -> Result<(), RuntimeError<'a>> { - if self.evaluated.contains_key(name) { - return Ok(()); - } - - if let Some(expression) = self.assignments.get(name) { - if let Some(value) = self.overrides.get(name) { - self.evaluated.insert(name, value.to_string()); - } else { - let value = self.evaluate_expression(expression, &empty())?; - self.evaluated.insert(name, value); - } - } else { - return Err(RuntimeError::InternalError { - message: format!("attempted to evaluated unknown assignment {}", name) - }); - } - - Ok(()) - } - - fn evaluate_expression( - &mut self, - expression: &Expression<'a>, - arguments: &Map<&str, Cow> - ) -> Result> { - Ok(match *expression { - Expression::Variable{name, ..} => { - if self.evaluated.contains_key(name) { - self.evaluated[name].clone() - } else if self.scope.contains_key(name) { - self.scope[name].clone() - } else if self.assignments.contains_key(name) { - self.evaluate_assignment(name)?; - self.evaluated[name].clone() - } else if arguments.contains_key(name) { - arguments[name].to_string() - } else { - return Err(RuntimeError::InternalError { - message: format!("attempted to evaluate undefined variable `{}`", name) - }); - } - } - Expression::String{ref cooked_string} => cooked_string.cooked.clone(), - Expression::Backtick{raw, ref token} => { - run_backtick(raw, token, self.scope, self.exports, self.quiet)? - } - Expression::Concatination{ref lhs, ref rhs} => { - self.evaluate_expression(lhs, arguments)? - + - &self.evaluate_expression(rhs, arguments)? - } - }) - } -} - -#[derive(Debug, PartialEq)] -struct CompilationError<'a> { - text: &'a str, - index: usize, - line: usize, - column: usize, - width: Option, - kind: CompilationErrorKind<'a>, -} - -#[derive(Debug, PartialEq)] -enum CompilationErrorKind<'a> { - CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>}, - CircularVariableDependency{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, - InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, - InternalError{message: String}, - InvalidEscapeSequence{character: char}, - MixedLeadingWhitespace{whitespace: &'a str}, - OuterShebang, - ParameterShadowsVariable{parameter: &'a str}, - RequiredParameterFollowsDefaultParameter{parameter: &'a str}, - ParameterFollowsVariadicParameter{parameter: &'a str}, - UndefinedVariable{variable: &'a str}, - UnexpectedToken{expected: Vec, found: TokenKind}, - UnknownDependency{recipe: &'a str, unknown: &'a str}, - UnknownStartOfToken, - UnterminatedString, -} - -fn internal_error(message: String) -> CompilationError<'static> { - CompilationError { - text: "", - index: 0, - line: 0, - column: 0, - width: None, - kind: CompilationErrorKind::InternalError { message: message } - } -} - -fn show_whitespace(text: &str) -> String { - text.chars().map(|c| match c { '\t' => '␉', ' ' => '␠', _ => c }).collect() -} - -fn mixed_whitespace(text: &str) -> bool { - !(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t')) -} - -fn maybe_s(n: usize) -> &'static str { - if n == 1 { - "" - } else { - "s" - } -} - -struct Tick<'a, T: 'a + Display>(&'a T); - -impl<'a, T: Display> Display for Tick<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, "`{}`", self.0) - } -} - -fn ticks(ts: &[T]) -> Vec> { - ts.iter().map(Tick).collect() -} - -#[derive(PartialEq, Debug)] -struct CookedString<'a> { - raw: &'a str, - cooked: String, -} - -fn cook_string<'a>(token: &Token<'a>) -> Result, CompilationError<'a>> { - let raw = &token.lexeme[1..token.lexeme.len()-1]; - - if let RawString = token.kind { - Ok(CookedString{raw: raw, cooked: raw.to_string()}) - } else if let StringToken = token.kind { - let mut cooked = String::new(); - let mut escape = false; - for c in raw.chars() { - if escape { - match c { - 'n' => cooked.push('\n'), - 'r' => cooked.push('\r'), - 't' => cooked.push('\t'), - '\\' => cooked.push('\\'), - '"' => cooked.push('"'), - other => return Err(token.error(CompilationErrorKind::InvalidEscapeSequence { - character: other, - })), - } - escape = false; - continue; - } - if c == '\\' { - escape = true; - continue; - } - cooked.push(c); - } - Ok(CookedString{raw: raw, cooked: cooked}) - } else { - Err(token.error(CompilationErrorKind::InternalError{ - message: "cook_string() called on non-string token".to_string() - })) - } -} - -struct And<'a, T: 'a + Display>(&'a [T]); -struct Or <'a, T: 'a + Display>(&'a [T]); - -impl<'a, T: Display> Display for And<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - conjoin(f, self.0, "and") - } -} - -impl<'a, T: Display> Display for Or<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - conjoin(f, self.0, "or") - } -} - -fn conjoin( - f: &mut fmt::Formatter, - values: &[T], - conjunction: &str, -) -> Result<(), fmt::Error> { - match values.len() { - 0 => {}, - 1 => write!(f, "{}", values[0])?, - 2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?, - _ => for (i, item) in values.iter().enumerate() { - write!(f, "{}", item)?; - if i == values.len() - 1 { - } else if i == values.len() - 2 { - write!(f, ", {} ", conjunction)?; - } else { - write!(f, ", ")? - } - }, - } - Ok(()) -} - -fn write_error_context( - f: &mut fmt::Formatter, - text: &str, - index: usize, - line: usize, - column: usize, - width: Option, -) -> Result<(), fmt::Error> { - let line_number = line + 1; - let red = Color::fmt(f).error(); - match text.lines().nth(line) { - Some(line) => { - let mut i = 0; - let mut space_column = 0; - let mut space_line = String::new(); - let mut space_width = 0; - for c in line.chars() { - if c == '\t' { - space_line.push_str(" "); - if i < column { - space_column += 4; - } - if i >= column && i < column + width.unwrap_or(1) { - space_width += 4; - } - } else { - if i < column { - space_column += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); - - } - if i >= column && i < column + width.unwrap_or(1) { - space_width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0); - } - space_line.push(c); - } - i += c.len_utf8(); - } - let line_number_width = line_number.to_string().len(); - write!(f, "{0:1$} |\n", "", line_number_width)?; - write!(f, "{} | {}\n", line_number, space_line)?; - write!(f, "{0:1$} |", "", line_number_width)?; - if width == None { - write!(f, " {0:1$}{2}^{3}", "", space_column, red.prefix(), red.suffix())?; - } else { - write!(f, " {0:1$}{2}{3:^<4$}{5}", "", space_column, - red.prefix(), "", space_width, red.suffix())?; - } - }, - None => if index != text.len() { - write!(f, "internal error: Error has invalid line number: {}", line_number)? - }, - } - Ok(()) -} - -fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<(), fmt::Error> { - write_error_context( - f, - token.text, - token.index, - token.line, - token.column + token.prefix.len(), - Some(token.lexeme.len()) - ) -} - -impl<'a> Display for CompilationError<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - use CompilationErrorKind::*; - let error = Color::fmt(f).error(); - let message = Color::fmt(f).message(); - - write!(f, "{} {}", error.paint("error:"), message.prefix())?; - - match self.kind { - CircularRecipeDependency{recipe, ref circle} => { - if circle.len() == 2 { - writeln!(f, "Recipe `{}` depends on itself", recipe)?; - } else { - writeln!(f, "Recipe `{}` has circular dependency `{}`", - recipe, circle.join(" -> "))?; - } - } - CircularVariableDependency{variable, ref circle} => { - if circle.len() == 2 { - writeln!(f, "Variable `{}` is defined in terms of itself", variable)?; - } else { - writeln!(f, "Variable `{}` depends on its own value: `{}`", - variable, circle.join(" -> "))?; - } - } - InvalidEscapeSequence{character} => { - writeln!(f, "`\\{}` is not a valid escape sequence", - character.escape_default().collect::())?; - } - DuplicateParameter{recipe, parameter} => { - writeln!(f, "Recipe `{}` has duplicate parameter `{}`", recipe, parameter)?; - } - DuplicateVariable{variable} => { - writeln!(f, "Variable `{}` has multiple definitions", variable)?; - } - UnexpectedToken{ref expected, found} => { - writeln!(f, "Expected {}, but found {}", Or(expected), found)?; - } - DuplicateDependency{recipe, dependency} => { - writeln!(f, "Recipe `{}` has duplicate dependency `{}`", recipe, dependency)?; - } - DuplicateRecipe{recipe, first} => { - writeln!(f, "Recipe `{}` first defined on line {} is redefined on line {}", - recipe, first + 1, self.line + 1)?; - } - DependencyHasParameters{recipe, dependency} => { - writeln!(f, "Recipe `{}` depends on `{}` which requires arguments. \ - Dependencies may not require arguments", recipe, dependency)?; - } - ParameterShadowsVariable{parameter} => { - writeln!(f, "Parameter `{}` shadows variable of the same name", parameter)?; - } - RequiredParameterFollowsDefaultParameter{parameter} => { - writeln!(f, "Non-default parameter `{}` follows default parameter", parameter)?; - } - ParameterFollowsVariadicParameter{parameter} => { - writeln!(f, "Parameter `{}` follows variadic parameter", parameter)?; - } - MixedLeadingWhitespace{whitespace} => { - writeln!(f, - "Found a mix of tabs and spaces in leading whitespace: `{}`\n\ - Leading whitespace may consist of tabs or spaces, but not both", - show_whitespace(whitespace) - )?; - } - ExtraLeadingWhitespace => { - writeln!(f, "Recipe line has extra leading whitespace")?; - } - InconsistentLeadingWhitespace{expected, found} => { - writeln!(f, - "Recipe line has inconsistent leading whitespace. \ - Recipe started with `{}` but found line with `{}`", - show_whitespace(expected), show_whitespace(found) - )?; - } - OuterShebang => { - writeln!(f, "`#!` is reserved syntax outside of recipes")?; - } - UnknownDependency{recipe, unknown} => { - writeln!(f, "Recipe `{}` has unknown dependency `{}`", recipe, unknown)?; - } - UndefinedVariable{variable} => { - writeln!(f, "Variable `{}` not defined", variable)?; - } - UnknownStartOfToken => { - writeln!(f, "Unknown start of token:")?; - } - UnterminatedString => { - writeln!(f, "Unterminated string")?; - } - InternalError{ref message} => { - writeln!(f, "Internal error, this may indicate a bug in just: {}\n\ - consider filing an issue: https://github.com/casey/just/issues/new", - message)?; - } - } - - write!(f, "{}", message.suffix())?; - - write_error_context(f, self.text, self.index, self.line, self.column, self.width) - } -} - -struct Justfile<'a> { - recipes: Map<&'a str, Recipe<'a>>, - assignments: Map<&'a str, Expression<'a>>, - exports: Set<&'a str>, -} - -#[derive(Default)] -struct RunOptions<'a> { - dry_run: bool, - evaluate: bool, - highlight: bool, - overrides: Map<&'a str, &'a str>, - quiet: bool, - shell: Option<&'a str>, - color: Color, - verbose: bool, -} - -impl<'a, 'b> Justfile<'a> where 'a: 'b { - fn first(&self) -> Option<&Recipe> { - let mut first: Option<&Recipe> = None; - for recipe in self.recipes.values() { - if let Some(first_recipe) = first { - if recipe.line_number < first_recipe.line_number { - first = Some(recipe) - } - } else { - first = Some(recipe); - } - } - first - } - - fn count(&self) -> usize { - self.recipes.len() - } - - fn suggest(&self, name: &str) -> Option<&'a str> { - let mut suggestions = self.recipes.keys() - .map(|suggestion| (edit_distance::edit_distance(suggestion, name), suggestion)) - .collect::>(); - suggestions.sort(); - if let Some(&(distance, suggestion)) = suggestions.first() { - if distance < 3 { - return Some(suggestion) - } - } - None - } - - fn run( - &'a self, - arguments: &[&'a str], - options: &RunOptions<'a>, - ) -> Result<(), RuntimeError<'a>> { - let unknown_overrides = options.overrides.keys().cloned() - .filter(|name| !self.assignments.contains_key(name)) - .collect::>(); - - if !unknown_overrides.is_empty() { - return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides}); - } - - let scope = evaluate_assignments(&self.assignments, &options.overrides, options.quiet)?; - if options.evaluate { - let mut width = 0; - for name in scope.keys() { - width = cmp::max(name.len(), width); - } - - for (name, value) in scope { - println!("{0:1$} = \"{2}\"", name, width, value); - } - return Ok(()); - } - - let mut missing = vec![]; - let mut grouped = vec![]; - let mut rest = arguments; - - while let Some((argument, mut tail)) = rest.split_first() { - if let Some(recipe) = self.recipes.get(argument) { - if recipe.parameters.is_empty() { - grouped.push((recipe, &tail[0..0])); - } else { - let argument_range = recipe.argument_range(); - let argument_count = cmp::min(tail.len(), recipe.max_arguments()); - if !contains(&argument_range, argument_count) { - return Err(RuntimeError::ArgumentCountMismatch { - recipe: recipe.name, - found: tail.len(), - min: recipe.min_arguments(), - max: recipe.max_arguments(), - }); - } - grouped.push((recipe, &tail[0..argument_count])); - tail = &tail[argument_count..]; - } - } else { - missing.push(*argument); - } - rest = tail; - } - - if !missing.is_empty() { - let suggestion = if missing.len() == 1 { - self.suggest(missing.first().unwrap()) - } else { - None - }; - return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion: suggestion}); - } - - let mut ran = empty(); - for (recipe, arguments) in grouped { - self.run_recipe(recipe, arguments, &scope, &mut ran, options)? - } - - Ok(()) - } - - fn run_recipe<'c>( - &'c self, - recipe: &Recipe<'a>, - arguments: &[&'a str], - scope: &Map<&'c str, String>, - ran: &mut Set<&'a str>, - options: &RunOptions<'a>, - ) -> Result<(), RuntimeError> { - for dependency_name in &recipe.dependencies { - if !ran.contains(dependency_name) { - self.run_recipe(&self.recipes[dependency_name], &[], scope, ran, options)?; - } - } - recipe.run(arguments, scope, &self.exports, options)?; - ran.insert(recipe.name); - Ok(()) - } -} - -impl<'a> Display for Justfile<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let mut items = self.recipes.len() + self.assignments.len(); - for (name, expression) in &self.assignments { - if self.exports.contains(name) { - write!(f, "export ")?; - } - write!(f, "{} = {}", name, expression)?; - items -= 1; - if items != 0 { - write!(f, "\n\n")?; - } - } - for recipe in self.recipes.values() { - write!(f, "{}", recipe)?; - items -= 1; - if items != 0 { - write!(f, "\n\n")?; - } - } - Ok(()) - } -} - -#[derive(Debug)] -enum RuntimeError<'a> { - ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize}, - Backtick{token: Token<'a>, output_error: OutputError}, - Code{recipe: &'a str, line_number: Option, code: i32}, - Cygpath{recipe: &'a str, output_error: OutputError}, - InternalError{message: String}, - IoError{recipe: &'a str, io_error: io::Error}, - Shebang{recipe: &'a str, command: String, argument: Option, io_error: io::Error}, - Signal{recipe: &'a str, line_number: Option, signal: i32}, - TmpdirIoError{recipe: &'a str, io_error: io::Error}, - UnknownFailure{recipe: &'a str, line_number: Option}, - UnknownOverrides{overrides: Vec<&'a str>}, - UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, -} - -impl<'a> RuntimeError<'a> { - fn code(&self) -> Option { - use RuntimeError::*; - match *self { - Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code), - _ => None, - } - } -} - -impl<'a> Display for RuntimeError<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - use RuntimeError::*; - let color = if f.alternate() { Color::always() } else { Color::never() }; - let error = color.error(); - let message = color.message(); - write!(f, "{} {}", error.paint("error:"), message.prefix())?; - - let mut error_token = None; - - match *self { - UnknownRecipes{ref recipes, ref suggestion} => { - write!(f, "Justfile does not contain recipe{} {}.", - maybe_s(recipes.len()), Or(&ticks(recipes)))?; - if let Some(suggestion) = *suggestion { - write!(f, "\nDid you mean `{}`?", suggestion)?; - } - }, - UnknownOverrides{ref overrides} => { - write!(f, "Variable{} {} overridden on the command line but not present in justfile", - maybe_s(overrides.len()), - And(&overrides.iter().map(Tick).collect::>()))?; - }, - ArgumentCountMismatch{recipe, found, min, max} => { - if min == max { - let expected = min; - write!(f, "Recipe `{}` got {} argument{} but {}takes {}", - recipe, found, maybe_s(found), - if expected < found { "only " } else { "" }, expected)?; - } else if found < min { - write!(f, "Recipe `{}` got {} argument{} but takes at least {}", - recipe, found, maybe_s(found), min)?; - } else if found > max { - write!(f, "Recipe `{}` got {} argument{} but takes at most {}", - recipe, found, maybe_s(found), max)?; - } - }, - Code{recipe, line_number, code} => { - if let Some(n) = line_number { - write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?; - } else { - write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; - } - }, - Cygpath{recipe, ref output_error} => match *output_error { - OutputError::Code(code) => { - write!(f, "Cygpath failed with exit code {} while translating recipe `{}` \ - shebang interpreter path", code, recipe)?; - } - OutputError::Signal(signal) => { - write!(f, "Cygpath terminated by signal {} while translating recipe `{}` \ - shebang interpreter path", signal, recipe)?; - } - OutputError::Unknown => { - write!(f, "Cygpath experienced an unknown failure while translating recipe `{}` \ - shebang interpreter path", recipe)?; - } - OutputError::Io(ref io_error) => { - match io_error.kind() { - io::ErrorKind::NotFound => write!( - f, "Could not find `cygpath` executable to translate recipe `{}` \ - shebang interpreter path:\n{}", recipe, io_error), - io::ErrorKind::PermissionDenied => write!( - f, "Could not run `cygpath` executable to translate recipe `{}` \ - shebang interpreter path:\n{}", recipe, io_error), - _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error), - }?; - } - OutputError::Utf8(ref utf8_error) => { - write!(f, "Cygpath successfully translated recipe `{}` shebang interpreter path, \ - but output was not utf8: {}", recipe, utf8_error)?; - } - }, - Shebang{recipe, ref command, ref argument, ref io_error} => { - if let Some(ref argument) = *argument { - write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}", - recipe, command, argument, io_error)?; - } else { - write!(f, "Recipe `{}` with shebang `#!{}` execution error: {}", - recipe, command, io_error)?; - } - } - Signal{recipe, line_number, signal} => { - if let Some(n) = line_number { - write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?; - } else { - write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; - } - } - UnknownFailure{recipe, line_number} => { - if let Some(n) = line_number { - write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?; - } else { - } - }, - IoError{recipe, ref io_error} => { - match io_error.kind() { - io::ErrorKind::NotFound => write!(f, - "Recipe `{}` could not be run because just could not find `sh`:\n{}", - recipe, io_error), - io::ErrorKind::PermissionDenied => write!( - f, "Recipe `{}` could not be run because just could not run `sh`:\n{}", - recipe, io_error), - _ => write!(f, "Recipe `{}` could not be run because of an IO error while \ - launching `sh`:\n{}", recipe, io_error), - }?; - }, - TmpdirIoError{recipe, ref io_error} => - write!(f, "Recipe `{}` could not be run because of an IO error while trying \ - to create a temporary directory or write a file to that directory`:\n{}", - recipe, io_error)?, - Backtick{ref token, ref output_error} => match *output_error { - OutputError::Code(code) => { - write!(f, "Backtick failed with exit code {}\n", code)?; - error_token = Some(token); - } - OutputError::Signal(signal) => { - write!(f, "Backtick was terminated by signal {}", signal)?; - error_token = Some(token); - } - OutputError::Unknown => { - write!(f, "Backtick failed for an unknown reason")?; - error_token = Some(token); - } - OutputError::Io(ref io_error) => { - match io_error.kind() { - io::ErrorKind::NotFound => write!( - f, "Backtick could not be run because just could not find `sh`:\n{}", - io_error), - io::ErrorKind::PermissionDenied => write!( - f, "Backtick could not be run because just could not run `sh`:\n{}", io_error), - _ => write!(f, "Backtick could not be run because of an IO \ - error while launching `sh`:\n{}", io_error), - }?; - error_token = Some(token); - } - OutputError::Utf8(ref utf8_error) => { - write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?; - error_token = Some(token); - } - }, - InternalError{ref message} => { - write!(f, "Internal error, this may indicate a bug in just: {} \ - consider filing an issue: https://github.com/casey/just/issues/new", - message)?; - } - } - - write!(f, "{}", message.suffix())?; - - if let Some(token) = error_token { - write_token_error_context(f, token)?; - } - - Ok(()) - } -} - -#[derive(Debug, PartialEq, Clone)] -struct Token<'a> { - index: usize, - line: usize, - column: usize, - text: &'a str, - prefix: &'a str, - lexeme: &'a str, - kind: TokenKind, -} - -impl<'a> Token<'a> { - fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> { - CompilationError { - text: self.text, - index: self.index + self.prefix.len(), - line: self.line, - column: self.column + self.prefix.len(), - width: Some(self.lexeme.len()), - kind: kind, - } - } -} - -#[derive(Debug, PartialEq, Clone, Copy)] -enum TokenKind { - At, - Backtick, - Colon, - Comment, - Dedent, - Eof, - Eol, - Equals, - Indent, - InterpolationEnd, - InterpolationStart, - Line, - Name, - Plus, - RawString, - StringToken, - Text, -} - -impl Display for TokenKind { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, "{}", match *self { - Backtick => "backtick", - Colon => "':'", - Comment => "comment", - Dedent => "dedent", - Eof => "end of file", - Eol => "end of line", - Equals => "'='", - Indent => "indent", - InterpolationEnd => "'}}'", - InterpolationStart => "'{{'", - Line => "command", - Name => "name", - Plus => "'+'", - At => "'@'", - StringToken => "string", - RawString => "raw string", - Text => "command text", - }) - } -} - -use TokenKind::*; - -fn token(pattern: &str) -> Regex { - let mut s = String::new(); - s += r"^(?m)([ \t]*)("; - s += pattern; - s += ")"; - re(&s) -} - -fn tokenize(text: &str) -> Result, CompilationError> { - lazy_static! { - static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" ); - static ref COLON: Regex = token(r":" ); - static ref AT: Regex = token(r"@" ); - static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" ); - static ref EOF: Regex = token(r"(?-m)$" ); - static ref EOL: Regex = token(r"\n|\r\n" ); - static ref EQUALS: Regex = token(r"=" ); - static ref INTERPOLATION_END: 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 PLUS: Regex = token(r"[+]" ); - static ref STRING: Regex = token("\"" ); - static ref RAW_STRING: Regex = token(r#"'[^']*'"# ); - static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# ); - static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" ); - static ref INTERPOLATION_START: Regex = re(r"^[{][{]" ); - static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]" ); - static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$"); - static ref TEXT: Regex = re(r"^(?m)(.+)" ); - } - - #[derive(PartialEq)] - enum State<'a> { - Start, - Indent(&'a str), - Text, - Interpolation, - } - - fn indentation(text: &str) -> Option<&str> { - INDENT.captures(text).map(|captures| captures.get(1).unwrap().as_str()) - } - - let mut tokens = vec![]; - let mut rest = text; - let mut index = 0; - let mut line = 0; - let mut column = 0; - let mut state = vec![State::Start]; - - macro_rules! error { - ($kind:expr) => {{ - Err(CompilationError { - text: text, - index: index, - line: line, - column: column, - width: None, - kind: $kind, - }) - }}; - } - - loop { - if column == 0 { - if let Some(kind) = match (state.last().unwrap(), indentation(rest)) { - // ignore: was no indentation and there still isn't - // or current line is blank - (&State::Start, Some("")) | (_, None) => { - None - } - // indent: was no indentation, now there is - (&State::Start, Some(current)) => { - if mixed_whitespace(current) { - return error!(CompilationErrorKind::MixedLeadingWhitespace{whitespace: current}) - } - //indent = Some(current); - state.push(State::Indent(current)); - Some(Indent) - } - // dedent: there was indentation and now there isn't - (&State::Indent(_), Some("")) => { - // indent = None; - state.pop(); - Some(Dedent) - } - // was indentation and still is, check if the new indentation matches - (&State::Indent(previous), Some(current)) => { - if !current.starts_with(previous) { - return error!(CompilationErrorKind::InconsistentLeadingWhitespace{ - expected: previous, - found: current - }); - } - None - } - // at column 0 in some other state: this should never happen - (&State::Text, _) | (&State::Interpolation, _) => { - return error!(CompilationErrorKind::InternalError{ - message: "unexpected state at column 0".to_string() - }); - } - } { - tokens.push(Token { - index: index, - line: line, - column: column, - text: text, - prefix: "", - lexeme: "", - kind: kind, - }); - } - } - - // insert a dedent if we're indented and we hit the end of the file - if &State::Start != state.last().unwrap() && EOF.is_match(rest) { - tokens.push(Token { - index: index, - line: line, - column: column, - text: text, - prefix: "", - lexeme: "", - kind: Dedent, - }); - } - - let (prefix, lexeme, kind) = - if let (0, &State::Indent(indent), Some(captures)) = - (column, state.last().unwrap(), LINE.captures(rest)) { - let line = captures.get(0).unwrap().as_str(); - if !line.starts_with(indent) { - return error!(CompilationErrorKind::InternalError{message: "unexpected indent".to_string()}); - } - state.push(State::Text); - (&line[0..indent.len()], "", Line) - } else if let Some(captures) = EOF.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eof) - } else if let State::Text = *state.last().unwrap() { - if let Some(captures) = INTERPOLATION_START.captures(rest) { - state.push(State::Interpolation); - ("", captures.get(0).unwrap().as_str(), InterpolationStart) - } else if let Some(captures) = LEADING_TEXT.captures(rest) { - ("", captures.get(1).unwrap().as_str(), Text) - } else if let Some(captures) = TEXT.captures(rest) { - ("", captures.get(1).unwrap().as_str(), Text) - } else if let Some(captures) = EOL.captures(rest) { - state.pop(); - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) - } else { - return error!(CompilationErrorKind::InternalError{ - message: format!("Could not match token in text state: \"{}\"", rest) - }); - } - } else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationStart) - } else if let Some(captures) = INTERPOLATION_END.captures(rest) { - if state.last().unwrap() == &State::Interpolation { - state.pop(); - } - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationEnd) - } else if let Some(captures) = NAME.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Name) - } else if let Some(captures) = EOL.captures(rest) { - if state.last().unwrap() == &State::Interpolation { - return error!(CompilationErrorKind::InternalError { - message: "hit EOL while still in interpolation state".to_string() - }); - } - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) - } else if let Some(captures) = BACKTICK.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Backtick) - } else if let Some(captures) = COLON.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon) - } else if let Some(captures) = AT.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At) - } else if let Some(captures) = PLUS.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus) - } else if let Some(captures) = EQUALS.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Equals) - } else if let Some(captures) = COMMENT.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comment) - } else if let Some(captures) = RAW_STRING.captures(rest) { - (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), RawString) - } else if UNTERMINATED_RAW_STRING.is_match(rest) { - return error!(CompilationErrorKind::UnterminatedString); - } else if let Some(captures) = STRING.captures(rest) { - let prefix = captures.get(1).unwrap().as_str(); - let contents = &rest[prefix.len()+1..]; - if contents.is_empty() { - return error!(CompilationErrorKind::UnterminatedString); - } - let mut len = 0; - let mut escape = false; - for c in contents.chars() { - if c == '\n' || c == '\r' { - return error!(CompilationErrorKind::UnterminatedString); - } else if !escape && c == '"' { - break; - } else if !escape && c == '\\' { - escape = true; - } else if escape { - escape = false; - } - len += c.len_utf8(); - } - let start = prefix.len(); - let content_end = start + len + 1; - if escape || content_end >= rest.len() { - return error!(CompilationErrorKind::UnterminatedString); - } - (prefix, &rest[start..content_end + 1], StringToken) - } else if rest.starts_with("#!") { - return error!(CompilationErrorKind::OuterShebang) - } else { - return error!(CompilationErrorKind::UnknownStartOfToken) - }; - - tokens.push(Token { - index: index, - line: line, - column: column, - prefix: prefix, - text: text, - lexeme: lexeme, - kind: kind, - }); - - let len = prefix.len() + lexeme.len(); - - if len == 0 { - let last = tokens.last().unwrap(); - match last.kind { - Eof => {}, - _ => return Err(last.error(CompilationErrorKind::InternalError{ - message: format!("zero length token: {:?}", last) - })), - } - } - - match tokens.last().unwrap().kind { - Eol => { - line += 1; - column = 0; - } - Eof => { - break; - } - RawString => { - let lexeme_lines = lexeme.lines().count(); - line += lexeme_lines - 1; - if lexeme_lines == 1 { - column += len; - } else { - column = lexeme.lines().last().unwrap().len(); - } - } - _ => { - column += len; - } - } - - rest = &rest[len..]; - index += len; - } - - Ok(tokens) -} - -fn compile(text: &str) -> Result { - let tokens = tokenize(text)?; - let parser = Parser { - text: text, - tokens: itertools::put_back(tokens), - recipes: empty(), - assignments: empty(), - assignment_tokens: empty(), - exports: empty(), - }; - parser.justfile() -} - -struct Parser<'a> { - text: &'a str, - tokens: itertools::PutBack>>, - recipes: Map<&'a str, Recipe<'a>>, - assignments: Map<&'a str, Expression<'a>>, - assignment_tokens: Map<&'a str, Token<'a>>, - exports: Set<&'a str>, -} - -impl<'a> Parser<'a> { - fn peek(&mut self, kind: TokenKind) -> bool { - let next = self.tokens.next().unwrap(); - let result = next.kind == kind; - self.tokens.put_back(next); - result - } - - fn accept(&mut self, kind: TokenKind) -> Option> { - if self.peek(kind) { - self.tokens.next() - } else { - None - } - } - - fn accept_any(&mut self, kinds: &[TokenKind]) -> Option> { - for kind in kinds { - if self.peek(*kind) { - return self.tokens.next(); - } - } - None - } - - fn accepted(&mut self, kind: TokenKind) -> bool { - self.accept(kind).is_some() - } - - fn expect(&mut self, kind: TokenKind) -> Option> { - if self.peek(kind) { - self.tokens.next(); - None - } else { - self.tokens.next() - } - } - - fn expect_eol(&mut self) -> Option> { - self.accepted(Comment); - if self.peek(Eol) { - self.accept(Eol); - None - } else if self.peek(Eof) { - None - } else { - self.tokens.next() - } - } - - fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> { - found.error(CompilationErrorKind::UnexpectedToken { - expected: expected.to_vec(), - found: found.kind, - }) - } - - fn recipe( - &mut self, - name: Token<'a>, - doc: Option>, - quiet: bool, - ) -> Result<(), CompilationError<'a>> { - if let Some(recipe) = self.recipes.get(name.lexeme) { - return Err(name.error(CompilationErrorKind::DuplicateRecipe { - recipe: recipe.name, - first: recipe.line_number - })); - } - - let mut parsed_parameter_with_default = false; - let mut parsed_variadic_parameter = false; - let mut parameters: Vec = vec![]; - loop { - let plus = self.accept(Plus); - - let parameter = match self.accept(Name) { - Some(parameter) => parameter, - None => if let Some(plus) = plus { - return Err(self.unexpected_token(&plus, &[Name])); - } else { - break - }, - }; - - let variadic = plus.is_some(); - - if parsed_variadic_parameter { - return Err(parameter.error(CompilationErrorKind::ParameterFollowsVariadicParameter { - parameter: parameter.lexeme, - })); - } - - if parameters.iter().any(|p| p.name == parameter.lexeme) { - return Err(parameter.error(CompilationErrorKind::DuplicateParameter { - recipe: name.lexeme, parameter: parameter.lexeme - })); - } - - let default; - if self.accepted(Equals) { - if let Some(string) = self.accept_any(&[StringToken, RawString]) { - default = Some(cook_string(&string)?.cooked); - } else { - let unexpected = self.tokens.next().unwrap(); - return Err(self.unexpected_token(&unexpected, &[StringToken, RawString])); - } - } else { - default = None - } - - if parsed_parameter_with_default && default.is_none() { - return Err(parameter.error(CompilationErrorKind::RequiredParameterFollowsDefaultParameter{ - parameter: parameter.lexeme, - })); - } - - parsed_parameter_with_default |= default.is_some(); - parsed_variadic_parameter = variadic; - - parameters.push(Parameter { - default: default, - name: parameter.lexeme, - token: parameter, - variadic: variadic, - }); - } - - if let Some(token) = self.expect(Colon) { - // if we haven't accepted any parameters, an equals - // would have been fine as part of an assignment - if parameters.is_empty() { - return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals])); - } else { - return Err(self.unexpected_token(&token, &[Name, Plus, Colon])); - } - } - - let mut dependencies = vec![]; - let mut dependency_tokens = vec![]; - while let Some(dependency) = self.accept(Name) { - if dependencies.contains(&dependency.lexeme) { - return Err(dependency.error(CompilationErrorKind::DuplicateDependency { - recipe: name.lexeme, - dependency: dependency.lexeme - })); - } - dependencies.push(dependency.lexeme); - dependency_tokens.push(dependency); - } - - if let Some(token) = self.expect_eol() { - return Err(self.unexpected_token(&token, &[Name, Eol, Eof])); - } - - let mut lines: Vec> = vec![]; - let mut shebang = false; - - if self.accepted(Indent) { - while !self.accepted(Dedent) { - if self.accepted(Eol) { - lines.push(vec![]); - continue; - } - if let Some(token) = self.expect(Line) { - return Err(token.error(CompilationErrorKind::InternalError{ - message: format!("Expected a line but got {}", token.kind) - })) - } - let mut fragments = vec![]; - - while !(self.accepted(Eol) || self.peek(Dedent)) { - if let Some(token) = self.accept(Text) { - if fragments.is_empty() { - if lines.is_empty() { - if token.lexeme.starts_with("#!") { - shebang = true; - } - } else if !shebang - && !lines.last().and_then(|line| line.last()) - .map(Fragment::continuation).unwrap_or(false) - && (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) { - return Err(token.error(CompilationErrorKind::ExtraLeadingWhitespace)); - } - } - fragments.push(Fragment::Text{text: token}); - } else if let Some(token) = self.expect(InterpolationStart) { - return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol])); - } else { - fragments.push(Fragment::Expression{ - expression: self.expression(true)? - }); - if let Some(token) = self.expect(InterpolationEnd) { - return Err(self.unexpected_token(&token, &[InterpolationEnd])); - } - } - } - - lines.push(fragments); - } - } - - self.recipes.insert(name.lexeme, Recipe { - line_number: name.line, - name: name.lexeme, - doc: doc.map(|t| t.lexeme[1..].trim()), - dependencies: dependencies, - dependency_tokens: dependency_tokens, - parameters: parameters, - private: &name.lexeme[0..1] == "_", - lines: lines, - shebang: shebang, - quiet: quiet, - }); - - Ok(()) - } - - fn expression(&mut self, interpolation: bool) -> Result, CompilationError<'a>> { - let first = self.tokens.next().unwrap(); - let lhs = match first.kind { - Name => Expression::Variable {name: first.lexeme, token: first}, - Backtick => Expression::Backtick { - raw: &first.lexeme[1..first.lexeme.len()-1], - token: first - }, - RawString | StringToken => { - Expression::String{cooked_string: cook_string(&first)?} - } - _ => return Err(self.unexpected_token(&first, &[Name, StringToken])), - }; - - if self.accepted(Plus) { - let rhs = self.expression(interpolation)?; - Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)}) - } else if interpolation && self.peek(InterpolationEnd) { - Ok(lhs) - } else if let Some(token) = self.expect_eol() { - if interpolation { - return Err(self.unexpected_token(&token, &[Plus, Eol, InterpolationEnd])) - } else { - Err(self.unexpected_token(&token, &[Plus, Eol])) - } - } else { - Ok(lhs) - } - } - - fn assignment(&mut self, name: Token<'a>, export: bool) -> Result<(), CompilationError<'a>> { - if self.assignments.contains_key(name.lexeme) { - return Err(name.error(CompilationErrorKind::DuplicateVariable {variable: name.lexeme})); - } - if export { - self.exports.insert(name.lexeme); - } - let expression = self.expression(false)?; - self.assignments.insert(name.lexeme, expression); - self.assignment_tokens.insert(name.lexeme, name); - Ok(()) - } - - fn justfile(mut self) -> Result, CompilationError<'a>> { - let mut doc = None; - loop { - match self.tokens.next() { - Some(token) => match token.kind { - Eof => break, - Eol => { - doc = None; - continue; - } - Comment => { - if let Some(token) = self.expect_eol() { - return Err(token.error(CompilationErrorKind::InternalError { - message: format!("found comment followed by {}", token.kind), - })); - } - doc = Some(token); - } - At => if let Some(name) = self.accept(Name) { - self.recipe(name, doc, true)?; - doc = None; - } else { - 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) { - 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; - } else { - self.recipe(token, doc, false)?; - doc = None; - }, - _ => return Err(self.unexpected_token(&token, &[Name, At])), - }, - None => return Err(CompilationError { - text: self.text, - index: 0, - line: 0, - column: 0, - width: None, - kind: CompilationErrorKind::InternalError { - message: "unexpected end of token stream".to_string() - } - }), - } - } - - if let Some(token) = self.tokens.next() { - return Err(token.error(CompilationErrorKind::InternalError{ - message: format!("unexpected token remaining after parsing completed: {:?}", token.kind) - })) - } - - resolve_recipes(&self.recipes, &self.assignments, self.text)?; - - for recipe in self.recipes.values() { - for parameter in &recipe.parameters { - if self.assignments.contains_key(parameter.token.lexeme) { - return Err(parameter.token.error(CompilationErrorKind::ParameterShadowsVariable { - parameter: parameter.token.lexeme - })); - } - } - - for dependency in &recipe.dependency_tokens { - if !self.recipes[dependency.lexeme].parameters.is_empty() { - return Err(dependency.error(CompilationErrorKind::DependencyHasParameters { - recipe: recipe.name, - dependency: dependency.lexeme, - })); - } - } - } - - resolve_assignments(&self.assignments, &self.assignment_tokens)?; - - Ok(Justfile { - recipes: self.recipes, - assignments: self.assignments, - exports: self.exports, - }) - } -} diff --git a/src/main.rs b/src/main.rs index ae6b10f..992dcc7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,82 @@ -extern crate just; +#[macro_use] +extern crate lazy_static; +extern crate ansi_term; +extern crate brev; +extern crate clap; +extern crate edit_distance; +extern crate itertools; +extern crate libc; +extern crate regex; +extern crate tempdir; +extern crate unicode_width; + +mod platform; +mod run; +mod color; +mod compilation_error; +mod runtime_error; +mod misc; +mod justfile; +mod recipe; +mod token; +mod parser; +mod tokenizer; +mod cooked_string; +mod recipe_resolver; +mod assignment_resolver; +mod assignment_evaluator; +mod configuration; +mod parameter; +mod expression; +mod fragment; +mod shebang; +mod command_ext; +mod range_ext; + +#[cfg(test)] mod testing; + +use tokenizer::tokenize; + +mod common { + pub use std::borrow::Cow; + pub use std::collections::{BTreeMap as Map, BTreeSet as Set}; + pub use std::fmt::Display; + pub use std::io::prelude::*; + pub use std::ops::Range; + pub use std::path::{Path, PathBuf}; + pub use std::process::Command; + pub use std::{cmp, env, fs, fmt, io, iter, process, vec, usize}; + + pub use color::Color; + pub use libc::{EXIT_FAILURE, EXIT_SUCCESS}; + pub use regex::Regex; + pub use tempdir::TempDir; + + pub use assignment_evaluator::AssignmentEvaluator; + pub use command_ext::CommandExt; + pub use compilation_error::{CompilationError, CompilationErrorKind}; + pub use configuration::Configuration; + pub use cooked_string::CookedString; + pub use expression::Expression; + pub use fragment::Fragment; + pub use justfile::Justfile; + pub use misc::{default, empty}; + pub use parameter::Parameter; + pub use parser::Parser; + pub use recipe::Recipe; + pub use runtime_error::RuntimeError; + pub use shebang::Shebang; + pub use token::{Token, TokenKind}; +} + +use common::*; + +fn compile(text: &str) -> Result { + let tokens = tokenize(text)?; + let parser = Parser::new(text, tokens); + parser.justfile() +} fn main() { - just::app(); + run::run(); } diff --git a/src/misc.rs b/src/misc.rs new file mode 100644 index 0000000..19de143 --- /dev/null +++ b/src/misc.rs @@ -0,0 +1,149 @@ +use common::*; + +use unicode_width::UnicodeWidthChar; + +pub fn show_whitespace(text: &str) -> String { + text.chars().map(|c| match c { '\t' => '␉', ' ' => '␠', _ => c }).collect() +} + +pub fn default() -> T { + Default::default() +} + +pub fn empty>() -> C { + iter::empty().collect() +} + +pub fn ticks(ts: &[T]) -> Vec> { + ts.iter().map(Tick).collect() +} + +pub fn maybe_s(n: usize) -> &'static str { + if n == 1 { + "" + } else { + "s" + } +} + +pub fn conjoin( + f: &mut fmt::Formatter, + values: &[T], + conjunction: &str, +) -> Result<(), fmt::Error> { + match values.len() { + 0 => {}, + 1 => write!(f, "{}", values[0])?, + 2 => write!(f, "{} {} {}", values[0], conjunction, values[1])?, + _ => for (i, item) in values.iter().enumerate() { + write!(f, "{}", item)?; + if i == values.len() - 1 { + } else if i == values.len() - 2 { + write!(f, ", {} ", conjunction)?; + } else { + write!(f, ", ")? + } + }, + } + Ok(()) +} + +pub fn write_error_context( + f: &mut fmt::Formatter, + text: &str, + index: usize, + line: usize, + column: usize, + width: Option, +) -> Result<(), fmt::Error> { + let line_number = line + 1; + let red = Color::fmt(f).error(); + match text.lines().nth(line) { + Some(line) => { + let mut i = 0; + let mut space_column = 0; + let mut space_line = String::new(); + let mut space_width = 0; + for c in line.chars() { + if c == '\t' { + space_line.push_str(" "); + if i < column { + space_column += 4; + } + if i >= column && i < column + width.unwrap_or(1) { + space_width += 4; + } + } else { + if i < column { + space_column += UnicodeWidthChar::width(c).unwrap_or(0); + + } + if i >= column && i < column + width.unwrap_or(1) { + space_width += UnicodeWidthChar::width(c).unwrap_or(0); + } + space_line.push(c); + } + i += c.len_utf8(); + } + let line_number_width = line_number.to_string().len(); + write!(f, "{0:1$} |\n", "", line_number_width)?; + write!(f, "{} | {}\n", line_number, space_line)?; + write!(f, "{0:1$} |", "", line_number_width)?; + if width == None { + write!(f, " {0:1$}{2}^{3}", "", space_column, red.prefix(), red.suffix())?; + } else { + write!(f, " {0:1$}{2}{3:^<4$}{5}", "", space_column, + red.prefix(), "", space_width, red.suffix())?; + } + }, + None => if index != text.len() { + write!(f, "internal error: Error has invalid line number: {}", line_number)? + }, + } + Ok(()) +} + +pub struct And<'a, T: 'a + Display>(pub &'a [T]); + +impl<'a, T: Display> Display for And<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + conjoin(f, self.0, "and") + } +} + +pub struct Or <'a, T: 'a + Display>(pub &'a [T]); + +impl<'a, T: Display> Display for Or<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + conjoin(f, self.0, "or") + } +} + +pub struct Tick<'a, T: 'a + Display>(pub &'a T); + +impl<'a, T: Display> Display for Tick<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "`{}`", self.0) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn conjoin_or() { + assert_eq!("1", Or(&[1 ]).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, 3, or 4", Or(&[1,2,3,4]).to_string()); + } + + #[test] + fn conjoin_and() { + assert_eq!("1", And(&[1 ]).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, 3, and 4", And(&[1,2,3,4]).to_string()); + } +} diff --git a/src/parameter.rs b/src/parameter.rs new file mode 100644 index 0000000..aecf64b --- /dev/null +++ b/src/parameter.rs @@ -0,0 +1,25 @@ +use common::*; + +#[derive(PartialEq, Debug)] +pub struct Parameter<'a> { + pub default: Option, + pub name: &'a str, + pub token: Token<'a>, + pub variadic: bool, +} + +impl<'a> Display for Parameter<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let color = Color::fmt(f); + if self.variadic { + write!(f, "{}", color.annotation().paint("+"))?; + } + write!(f, "{}", color.parameter().paint(self.name))?; + if let Some(ref default) = self.default { + let escaped = default.chars().flat_map(char::escape_default).collect::();; + write!(f, r#"='{}'"#, color.string().paint(&escaped))?; + } + Ok(()) + } +} + diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..5887c93 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,848 @@ +use common::*; + +use itertools; +use token::TokenKind::*; +use recipe_resolver::resolve_recipes; +use assignment_resolver::resolve_assignments; + +pub struct Parser<'a> { + text: &'a str, + tokens: itertools::PutBack>>, + recipes: Map<&'a str, Recipe<'a>>, + assignments: Map<&'a str, Expression<'a>>, + assignment_tokens: Map<&'a str, Token<'a>>, + exports: Set<&'a str>, +} + +impl<'a> Parser<'a> { + pub fn new(text: &'a str, tokens: Vec>) -> Parser<'a> { + Parser { + text: text, + tokens: itertools::put_back(tokens), + recipes: empty(), + assignments: empty(), + assignment_tokens: empty(), + exports: empty(), + } + } + + fn peek(&mut self, kind: TokenKind) -> bool { + let next = self.tokens.next().unwrap(); + let result = next.kind == kind; + self.tokens.put_back(next); + result + } + + fn accept(&mut self, kind: TokenKind) -> Option> { + if self.peek(kind) { + self.tokens.next() + } else { + None + } + } + + fn accept_any(&mut self, kinds: &[TokenKind]) -> Option> { + for kind in kinds { + if self.peek(*kind) { + return self.tokens.next(); + } + } + None + } + + fn accepted(&mut self, kind: TokenKind) -> bool { + self.accept(kind).is_some() + } + + fn expect(&mut self, kind: TokenKind) -> Option> { + if self.peek(kind) { + self.tokens.next(); + None + } else { + self.tokens.next() + } + } + + fn expect_eol(&mut self) -> Option> { + self.accepted(Comment); + if self.peek(Eol) { + self.accept(Eol); + None + } else if self.peek(Eof) { + None + } else { + self.tokens.next() + } + } + + fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenKind]) -> CompilationError<'a> { + found.error(CompilationErrorKind::UnexpectedToken { + expected: expected.to_vec(), + found: found.kind, + }) + } + + fn recipe( + &mut self, + name: Token<'a>, + doc: Option>, + quiet: bool, + ) -> Result<(), CompilationError<'a>> { + if let Some(recipe) = self.recipes.get(name.lexeme) { + return Err(name.error(CompilationErrorKind::DuplicateRecipe { + recipe: recipe.name, + first: recipe.line_number + })); + } + + let mut parsed_parameter_with_default = false; + let mut parsed_variadic_parameter = false; + let mut parameters: Vec = vec![]; + loop { + let plus = self.accept(Plus); + + let parameter = match self.accept(Name) { + Some(parameter) => parameter, + None => if let Some(plus) = plus { + return Err(self.unexpected_token(&plus, &[Name])); + } else { + break + }, + }; + + let variadic = plus.is_some(); + + if parsed_variadic_parameter { + return Err(parameter.error(CompilationErrorKind::ParameterFollowsVariadicParameter { + parameter: parameter.lexeme, + })); + } + + if parameters.iter().any(|p| p.name == parameter.lexeme) { + return Err(parameter.error(CompilationErrorKind::DuplicateParameter { + recipe: name.lexeme, parameter: parameter.lexeme + })); + } + + let default; + if self.accepted(Equals) { + if let Some(string) = self.accept_any(&[StringToken, RawString]) { + default = Some(CookedString::new(&string)?.cooked); + } else { + let unexpected = self.tokens.next().unwrap(); + return Err(self.unexpected_token(&unexpected, &[StringToken, RawString])); + } + } else { + default = None + } + + if parsed_parameter_with_default && default.is_none() { + return Err(parameter.error(CompilationErrorKind::RequiredParameterFollowsDefaultParameter{ + parameter: parameter.lexeme, + })); + } + + parsed_parameter_with_default |= default.is_some(); + parsed_variadic_parameter = variadic; + + parameters.push(Parameter { + default: default, + name: parameter.lexeme, + token: parameter, + variadic: variadic, + }); + } + + if let Some(token) = self.expect(Colon) { + // if we haven't accepted any parameters, an equals + // would have been fine as part of an assignment + if parameters.is_empty() { + return Err(self.unexpected_token(&token, &[Name, Plus, Colon, Equals])); + } else { + return Err(self.unexpected_token(&token, &[Name, Plus, Colon])); + } + } + + let mut dependencies = vec![]; + let mut dependency_tokens = vec![]; + while let Some(dependency) = self.accept(Name) { + if dependencies.contains(&dependency.lexeme) { + return Err(dependency.error(CompilationErrorKind::DuplicateDependency { + recipe: name.lexeme, + dependency: dependency.lexeme + })); + } + dependencies.push(dependency.lexeme); + dependency_tokens.push(dependency); + } + + if let Some(token) = self.expect_eol() { + return Err(self.unexpected_token(&token, &[Name, Eol, Eof])); + } + + let mut lines: Vec> = vec![]; + let mut shebang = false; + + if self.accepted(Indent) { + while !self.accepted(Dedent) { + if self.accepted(Eol) { + lines.push(vec![]); + continue; + } + if let Some(token) = self.expect(Line) { + return Err(token.error(CompilationErrorKind::Internal{ + message: format!("Expected a line but got {}", token.kind) + })) + } + let mut fragments = vec![]; + + while !(self.accepted(Eol) || self.peek(Dedent)) { + if let Some(token) = self.accept(Text) { + if fragments.is_empty() { + if lines.is_empty() { + if token.lexeme.starts_with("#!") { + shebang = true; + } + } else if !shebang + && !lines.last().and_then(|line| line.last()) + .map(Fragment::continuation).unwrap_or(false) + && (token.lexeme.starts_with(' ') || token.lexeme.starts_with('\t')) { + return Err(token.error(CompilationErrorKind::ExtraLeadingWhitespace)); + } + } + fragments.push(Fragment::Text{text: token}); + } else if let Some(token) = self.expect(InterpolationStart) { + return Err(self.unexpected_token(&token, &[Text, InterpolationStart, Eol])); + } else { + fragments.push(Fragment::Expression{ + expression: self.expression(true)? + }); + if let Some(token) = self.expect(InterpolationEnd) { + return Err(self.unexpected_token(&token, &[InterpolationEnd])); + } + } + } + + lines.push(fragments); + } + } + + self.recipes.insert(name.lexeme, Recipe { + line_number: name.line, + name: name.lexeme, + doc: doc.map(|t| t.lexeme[1..].trim()), + dependencies: dependencies, + dependency_tokens: dependency_tokens, + parameters: parameters, + private: &name.lexeme[0..1] == "_", + lines: lines, + shebang: shebang, + quiet: quiet, + }); + + Ok(()) + } + + fn expression(&mut self, interpolation: bool) -> Result, CompilationError<'a>> { + let first = self.tokens.next().unwrap(); + let lhs = match first.kind { + Name => Expression::Variable {name: first.lexeme, token: first}, + Backtick => Expression::Backtick { + raw: &first.lexeme[1..first.lexeme.len()-1], + token: first + }, + RawString | StringToken => { + Expression::String{cooked_string: CookedString::new(&first)?} + } + _ => return Err(self.unexpected_token(&first, &[Name, StringToken])), + }; + + if self.accepted(Plus) { + let rhs = self.expression(interpolation)?; + Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)}) + } else if interpolation && self.peek(InterpolationEnd) { + Ok(lhs) + } else if let Some(token) = self.expect_eol() { + if interpolation { + return Err(self.unexpected_token(&token, &[Plus, Eol, InterpolationEnd])) + } else { + Err(self.unexpected_token(&token, &[Plus, Eol])) + } + } else { + Ok(lhs) + } + } + + fn assignment(&mut self, name: Token<'a>, export: bool) -> Result<(), CompilationError<'a>> { + if self.assignments.contains_key(name.lexeme) { + return Err(name.error(CompilationErrorKind::DuplicateVariable {variable: name.lexeme})); + } + if export { + self.exports.insert(name.lexeme); + } + let expression = self.expression(false)?; + self.assignments.insert(name.lexeme, expression); + self.assignment_tokens.insert(name.lexeme, name); + Ok(()) + } + + pub fn justfile(mut self) -> Result, CompilationError<'a>> { + let mut doc = None; + loop { + match self.tokens.next() { + Some(token) => match token.kind { + Eof => break, + Eol => { + doc = None; + continue; + } + Comment => { + if let Some(token) = self.expect_eol() { + return Err(token.error(CompilationErrorKind::Internal { + message: format!("found comment followed by {}", token.kind), + })); + } + doc = Some(token); + } + At => if let Some(name) = self.accept(Name) { + self.recipe(name, doc, true)?; + doc = None; + } else { + 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) { + 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; + } else { + self.recipe(token, doc, false)?; + doc = None; + }, + _ => return Err(self.unexpected_token(&token, &[Name, At])), + }, + None => return Err(CompilationError { + text: self.text, + index: 0, + line: 0, + column: 0, + width: None, + kind: CompilationErrorKind::Internal { + message: "unexpected end of token stream".to_string() + } + }), + } + } + + if let Some(token) = self.tokens.next() { + return Err(token.error(CompilationErrorKind::Internal { + message: format!("unexpected token remaining after parsing completed: {:?}", token.kind) + })) + } + + resolve_recipes(&self.recipes, &self.assignments, self.text)?; + + for recipe in self.recipes.values() { + for parameter in &recipe.parameters { + if self.assignments.contains_key(parameter.token.lexeme) { + return Err(parameter.token.error(CompilationErrorKind::ParameterShadowsVariable { + parameter: parameter.token.lexeme + })); + } + } + + for dependency in &recipe.dependency_tokens { + if !self.recipes[dependency.lexeme].parameters.is_empty() { + return Err(dependency.error(CompilationErrorKind::DependencyHasParameters { + recipe: recipe.name, + dependency: dependency.lexeme, + })); + } + } + } + + resolve_assignments(&self.assignments, &self.assignment_tokens)?; + + Ok(Justfile { + recipes: self.recipes, + assignments: self.assignments, + exports: self.exports, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use brev; + use testing::parse_success; + use testing::parse_error; + +fn parse_summary(input: &str, output: &str) { + let justfile = parse_success(input); + let s = format!("{:#}", justfile); + if s != output { + println!("got:\n\"{}\"\n", s); + println!("\texpected:\n\"{}\"", output); + assert_eq!(s, output); + } +} + +#[test] +fn parse_empty() { + parse_summary(" + +# hello + + + ", ""); +} + +#[test] +fn parse_string_default() { + parse_summary(r#" + +foo a="b\t": + + + "#, r#"foo a='b\t':"#); +} + +#[test] +fn parse_variadic() { + parse_summary(r#" + +foo +a: + + + "#, r#"foo +a:"#); +} + +#[test] +fn parse_variadic_string_default() { + parse_summary(r#" + +foo +a="Hello": + + + "#, r#"foo +a='Hello':"#); +} + +#[test] +fn parse_raw_string_default() { + parse_summary(r#" + +foo a='b\t': + + + "#, r#"foo a='b\\t':"#); +} + +#[test] +fn parse_export() { + parse_summary(r#" +export a = "hello" + + "#, r#"export a = "hello""#); +} + + +#[test] +fn parse_complex() { + parse_summary(" +x: +y: +z: +foo = \"xx\" +bar = foo +goodbye = \"y\" +hello a b c : x y z #hello + #! blah + #blarg + {{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz + 1 + 2 + 3 +", "bar = foo + +foo = \"xx\" + +goodbye = \"y\" + +hello a b c: x y z + #! blah + #blarg + {{foo + bar}}abc{{goodbye + \"x\"}}xyz + 1 + 2 + 3 + +x: + +y: + +z:"); +} + +#[test] +fn parse_shebang() { + parse_summary(" +practicum = 'hello' +install: +\t#!/bin/sh +\tif [[ -f {{practicum}} ]]; then +\t\treturn +\tfi +", "practicum = \"hello\" + +install: + #!/bin/sh + if [[ -f {{practicum}} ]]; then + \treturn + fi" + ); +} + +#[test] +fn parse_assignments() { + parse_summary( +r#"a = "0" +c = a + b + a + b +b = "1" +"#, + +r#"a = "0" + +b = "1" + +c = a + b + a + b"#); +} + +#[test] +fn parse_assignment_backticks() { + parse_summary( +"a = `echo hello` +c = a + b + a + b +b = `echo goodbye`", + +"a = `echo hello` + +b = `echo goodbye` + +c = a + b + a + b"); +} + +#[test] +fn parse_interpolation_backticks() { + parse_summary( +r#"a: + echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, +r#"a: + echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#, + ); +} + +#[test] +fn missing_colon() { + let text = "a b c\nd e f"; + parse_error(text, CompilationError { + text: text, + index: 5, + line: 0, + column: 5, + width: Some(1), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol}, + }); +} + +#[test] +fn missing_default_eol() { + let text = "hello arg=\n"; + parse_error(text, CompilationError { + text: text, + index: 10, + line: 0, + column: 10, + width: Some(1), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eol}, + }); +} + +#[test] +fn missing_default_eof() { + let text = "hello arg="; + parse_error(text, CompilationError { + text: text, + index: 10, + line: 0, + column: 10, + width: Some(0), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eof}, + }); +} + +#[test] +fn missing_default_colon() { + let text = "hello arg=:"; + parse_error(text, CompilationError { + text: text, + index: 10, + line: 0, + column: 10, + width: Some(1), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Colon}, + }); +} + +#[test] +fn missing_default_backtick() { + let text = "hello arg=`hello`"; + parse_error(text, CompilationError { + text: text, + index: 10, + line: 0, + column: 10, + width: Some(7), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick}, + }); +} + +#[test] +fn parameter_after_variadic() { + let text = "foo +a bbb:"; + parse_error(text, CompilationError { + text: text, + index: 7, + line: 0, + column: 7, + width: Some(3), + kind: CompilationErrorKind::ParameterFollowsVariadicParameter{parameter: "bbb"} + }); +} + +#[test] +fn required_after_default() { + let text = "hello arg='foo' bar:"; + parse_error(text, CompilationError { + text: text, + index: 16, + line: 0, + column: 16, + width: Some(3), + kind: CompilationErrorKind::RequiredParameterFollowsDefaultParameter{parameter: "bar"}, + }); +} + +#[test] +fn missing_eol() { + let text = "a b c: z ="; + parse_error(text, CompilationError { + text: text, + index: 9, + line: 0, + column: 9, + width: Some(1), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals}, + }); +} + +#[test] +fn eof_test() { + parse_summary("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:"); +} + +#[test] +fn duplicate_parameter() { + let text = "a b b:"; + parse_error(text, CompilationError { + text: text, + index: 4, + line: 0, + column: 4, + width: Some(1), + kind: CompilationErrorKind::DuplicateParameter{recipe: "a", parameter: "b"} + }); +} + +#[test] +fn parameter_shadows_varible() { + let text = "foo = \"h\"\na foo:"; + parse_error(text, CompilationError { + text: text, + index: 12, + line: 1, + column: 2, + width: Some(3), + kind: CompilationErrorKind::ParameterShadowsVariable{parameter: "foo"} + }); +} + +#[test] +fn dependency_has_parameters() { + let text = "foo arg:\nb: foo"; + parse_error(text, CompilationError { + text: text, + index: 12, + line: 1, + column: 3, + width: Some(3), + kind: CompilationErrorKind::DependencyHasParameters{recipe: "b", dependency: "foo"} + }); +} + + +#[test] +fn duplicate_dependency() { + let text = "a b c: b c z z"; + parse_error(text, CompilationError { + text: text, + index: 13, + line: 0, + column: 13, + width: Some(1), + kind: CompilationErrorKind::DuplicateDependency{recipe: "a", dependency: "z"} + }); +} + +#[test] +fn duplicate_recipe() { + let text = "a:\nb:\na:"; + parse_error(text, CompilationError { + text: text, + index: 6, + line: 2, + column: 0, + width: Some(1), + kind: CompilationErrorKind::DuplicateRecipe{recipe: "a", first: 0} + }); +} + +#[test] +fn duplicate_variable() { + let text = "a = \"0\"\na = \"0\""; + parse_error(text, CompilationError { + text: text, + index: 8, + line: 1, + column: 0, + width: Some(1), + kind: CompilationErrorKind::DuplicateVariable{variable: "a"} + }); +} + +#[test] +fn string_quote_escape() { + parse_summary( + r#"a = "hello\"""#, + r#"a = "hello\"""# + ); +} + +#[test] +fn string_escapes() { + parse_summary( + r#"a = "\n\t\r\"\\""#, + r#"a = "\n\t\r\"\\""# + ); +} + +#[test] +fn parameters() { + parse_summary( +"a b c: + {{b}} {{c}}", +"a b c: + {{b}} {{c}}", + ); +} + + + +#[test] +fn extra_whitespace() { + let text = "a:\n blah\n blarg"; + parse_error(text, CompilationError { + text: text, + index: 10, + line: 2, + column: 1, + width: Some(6), + kind: CompilationErrorKind::ExtraLeadingWhitespace + }); + + // extra leading whitespace is okay in a shebang recipe + parse_success("a:\n #!\n print(1)"); +} +#[test] +fn interpolation_outside_of_recipe() { + let text = "{{"; + parse_error(text, CompilationError { + text: text, + index: 0, + line: 0, + column: 0, + width: Some(2), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, At], found: InterpolationStart}, + }); +} +#[test] +fn unclosed_interpolation_delimiter() { + let text = "a:\n echo {{ foo"; + parse_error(text, CompilationError { + text: text, + index: 15, + line: 1, + column: 12, + width: Some(0), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent}, + }); +} + +#[test] +fn plus_following_parameter() { + let text = "a b c+:"; + parse_error(text, CompilationError { + text: text, + index: 5, + line: 0, + column: 5, + width: Some(1), + kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name], found: Plus}, + }); +} + +#[test] +fn readme_test() { + let mut justfiles = vec![]; + let mut current = None; + + for line in brev::slurp("README.asc").lines() { + if let Some(mut justfile) = current { + if line == "```" { + justfiles.push(justfile); + current = None; + } else { + justfile += line; + justfile += "\n"; + current = Some(justfile); + } + } else if line == "```make" { + current = Some(String::new()); + } + } + + for justfile in justfiles { + parse_success(&justfile); + } +} + +} diff --git a/src/platform.rs b/src/platform.rs index fc271d6..2c6b116 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -1,4 +1,6 @@ -use ::prelude::*; +use common::*; + +use brev; pub struct Platform; @@ -6,7 +8,7 @@ pub trait PlatformInterface { /// Construct a command equivelant to running the script at `path` with the /// shebang line `shebang` fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) - -> Result; + -> Result; /// Set the execute permission on the file pointed to by `path` fn set_execute_permission(path: &Path) -> Result<(), io::Error>; @@ -18,10 +20,10 @@ pub trait PlatformInterface { #[cfg(unix)] impl PlatformInterface for Platform { fn make_shebang_command(path: &Path, _command: &str, _argument: Option<&str>) - -> Result + -> Result { // shebang scripts can be executed directly on unix - Ok(process::Command::new(path)) + Ok(Command::new(path)) } fn set_execute_permission(path: &Path) -> Result<(), io::Error> { @@ -47,14 +49,14 @@ impl PlatformInterface for Platform { #[cfg(windows)] impl PlatformInterface for Platform { fn make_shebang_command(path: &Path, command: &str, argument: Option<&str>) - -> Result + -> Result { // Translate path to the interpreter from unix style to windows style - let mut cygpath = process::Command::new("cygpath"); + let mut cygpath = Command::new("cygpath"); cygpath.arg("--windows"); cygpath.arg(command); - let mut cmd = process::Command::new(super::output(cygpath)?); + let mut cmd = Command::new(brev::output(cygpath)?); if let Some(argument) = argument { cmd.arg(argument); } diff --git a/src/range_ext.rs b/src/range_ext.rs new file mode 100644 index 0000000..db70cec --- /dev/null +++ b/src/range_ext.rs @@ -0,0 +1,25 @@ +use common::*; + +pub trait RangeExt { + fn range_contains(&self, i: T) -> bool; +} + +impl RangeExt for Range where T: PartialOrd + Copy { + fn range_contains(&self, i: T) -> bool { + i >= self.start && i < self.end + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn range() { + assert!( ( 0.. 1).range_contains( 0)); + assert!( (10..20).range_contains(15)); + assert!(!( 0.. 0).range_contains( 0)); + assert!(!( 1..10).range_contains( 0)); + assert!(!( 1..10).range_contains(10)); + } +} diff --git a/src/recipe.rs b/src/recipe.rs new file mode 100644 index 0000000..4d1adb2 --- /dev/null +++ b/src/recipe.rs @@ -0,0 +1,281 @@ +use common::*; + +use std::process::{ExitStatus, Command, Stdio}; + +use platform::{Platform, PlatformInterface}; + +/// Return a `RuntimeError::Signal` if the process was terminated by a signal, +/// otherwise return an `RuntimeError::UnknownFailure` +fn error_from_signal( + recipe: &str, + line_number: Option, + exit_status: ExitStatus +) -> RuntimeError { + match Platform::signal_from_exit_status(exit_status) { + Some(signal) => RuntimeError::Signal{recipe: recipe, line_number: line_number, signal: signal}, + None => RuntimeError::Unknown{recipe: recipe, line_number: line_number}, + } +} + +#[derive(PartialEq, Debug)] +pub struct Recipe<'a> { + pub dependencies: Vec<&'a str>, + pub dependency_tokens: Vec>, + pub doc: Option<&'a str>, + pub line_number: usize, + pub lines: Vec>>, + pub name: &'a str, + pub parameters: Vec>, + pub private: bool, + pub quiet: bool, + pub shebang: bool, +} + +impl<'a> Recipe<'a> { + pub fn argument_range(&self) -> Range { + self.min_arguments()..self.max_arguments() + 1 + } + + pub fn min_arguments(&self) -> usize { + self.parameters.iter().filter(|p| !p.default.is_some()).count() + } + + pub fn max_arguments(&self) -> usize { + if self.parameters.iter().any(|p| p.variadic) { + usize::MAX - 1 + } else { + self.parameters.len() + } + } + + pub fn run( + &self, + arguments: &[&'a str], + scope: &Map<&'a str, String>, + exports: &Set<&'a str>, + options: &Configuration, + ) -> Result<(), RuntimeError<'a>> { + if options.verbose { + let color = options.color.stderr().banner(); + eprintln!("{}===> Running recipe `{}`...{}", color.prefix(), self.name, color.suffix()); + } + + let mut argument_map = Map::new(); + + let mut rest = arguments; + for parameter in &self.parameters { + let value = if rest.is_empty() { + match parameter.default { + Some(ref default) => Cow::Borrowed(default.as_str()), + None => return Err(RuntimeError::Internal{ + message: "missing parameter without default".to_string() + }), + } + } else if parameter.variadic { + let value = Cow::Owned(rest.to_vec().join(" ")); + rest = &[]; + value + } else { + let value = Cow::Borrowed(rest[0]); + rest = &rest[1..]; + value + }; + argument_map.insert(parameter.name, value); + } + + let mut evaluator = AssignmentEvaluator { + evaluated: empty(), + scope: scope, + exports: exports, + assignments: &empty(), + overrides: &empty(), + quiet: options.quiet, + shell: options.shell, + }; + + if self.shebang { + let mut evaluated_lines = vec![]; + for line in &self.lines { + evaluated_lines.push(evaluator.evaluate_line(line, &argument_map)?); + } + + if options.dry_run || self.quiet { + for line in &evaluated_lines { + eprintln!("{}", line); + } + } + + if options.dry_run { + return Ok(()); + } + + let tmp = TempDir::new("just") + .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; + let mut path = tmp.path().to_path_buf(); + path.push(self.name); + { + let mut f = fs::File::create(&path) + .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; + let mut text = String::new(); + // add the shebang + text += &evaluated_lines[0]; + text += "\n"; + // add blank lines so that lines in the generated script + // have the same line number as the corresponding lines + // in the justfile + for _ in 1..(self.line_number + 2) { + text += "\n" + } + for line in &evaluated_lines[1..] { + text += line; + text += "\n"; + } + f.write_all(text.as_bytes()) + .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; + } + + // make the script executable + Platform::set_execute_permission(&path) + .map_err(|error| RuntimeError::TmpdirIoError{recipe: self.name, io_error: error})?; + + let shebang_line = evaluated_lines.first() + .ok_or_else(|| RuntimeError::Internal { + message: "evaluated_lines was empty".to_string() + })?; + + let Shebang{interpreter, argument} = Shebang::new(shebang_line) + .ok_or_else(|| RuntimeError::Internal { + message: format!("bad shebang line: {}", shebang_line) + })?; + + // create a command to run the script + let mut command = Platform::make_shebang_command(&path, interpreter, argument) + .map_err(|output_error| RuntimeError::Cygpath{recipe: self.name, output_error: output_error})?; + + command.export_environment_variables(scope, exports)?; + + // run it! + match command.status() { + Ok(exit_status) => if let Some(code) = exit_status.code() { + if code != 0 { + return Err(RuntimeError::Code{recipe: self.name, line_number: None, code: code}) + } + } else { + return Err(error_from_signal(self.name, None, exit_status)) + }, + Err(io_error) => return Err(RuntimeError::Shebang { + recipe: self.name, + command: interpreter.to_string(), + argument: argument.map(String::from), + io_error: io_error + }) + }; + } else { + let mut lines = self.lines.iter().peekable(); + let mut line_number = self.line_number + 1; + loop { + if lines.peek().is_none() { + break; + } + let mut evaluated = String::new(); + loop { + if lines.peek().is_none() { + break; + } + let line = lines.next().unwrap(); + line_number += 1; + evaluated += &evaluator.evaluate_line(line, &argument_map)?; + if line.last().map(Fragment::continuation).unwrap_or(false) { + evaluated.pop(); + } else { + break; + } + } + let mut command = evaluated.as_str(); + let quiet_command = command.starts_with('@'); + if quiet_command { + command = &command[1..]; + } + + if command == "" { + continue; + } + + if options.dry_run || options.verbose || !((quiet_command ^ self.quiet) || options.quiet) { + let color = if options.highlight { + options.color.command() + } else { + options.color + }; + eprintln!("{}", color.stderr().paint(command)); + } + + if options.dry_run { + continue; + } + + let mut cmd = Command::new(options.shell); + + cmd.arg("-cu").arg(command); + + if options.quiet { + cmd.stderr(Stdio::null()); + cmd.stdout(Stdio::null()); + } + + cmd.export_environment_variables(scope, exports)?; + + match cmd.status() { + Ok(exit_status) => if let Some(code) = exit_status.code() { + if code != 0 { + return Err(RuntimeError::Code{ + recipe: self.name, line_number: Some(line_number), code: code + }); + } + } else { + return Err(error_from_signal(self.name, Some(line_number), exit_status)); + }, + Err(io_error) => return Err(RuntimeError::IoError{ + recipe: self.name, io_error: io_error}), + }; + } + } + Ok(()) + } +} + +impl<'a> Display for Recipe<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + if let Some(doc) = self.doc { + writeln!(f, "# {}", doc)?; + } + write!(f, "{}", self.name)?; + for parameter in &self.parameters { + write!(f, " {}", parameter)?; + } + write!(f, ":")?; + for dependency in &self.dependencies { + write!(f, " {}", dependency)?; + } + + for (i, pieces) in self.lines.iter().enumerate() { + if i == 0 { + writeln!(f, "")?; + } + for (j, piece) in pieces.iter().enumerate() { + if j == 0 { + write!(f, " ")?; + } + match *piece { + Fragment::Text{ref text} => write!(f, "{}", text.lexeme)?, + Fragment::Expression{ref expression, ..} => + write!(f, "{}{}{}", "{{", expression, "}}")?, + } + } + if i + 1 < self.lines.len() { + write!(f, "\n")?; + } + } + Ok(()) + } +} diff --git a/src/recipe_resolver.rs b/src/recipe_resolver.rs new file mode 100644 index 0000000..7c918f9 --- /dev/null +++ b/src/recipe_resolver.rs @@ -0,0 +1,171 @@ +use common::*; + +pub fn resolve_recipes<'a>( + recipes: &Map<&'a str, Recipe<'a>>, + assignments: &Map<&'a str, Expression<'a>>, + text: &'a str, +) -> Result<(), CompilationError<'a>> { + let mut resolver = RecipeResolver { + seen: empty(), + stack: empty(), + resolved: empty(), + recipes: recipes, + }; + + for recipe in recipes.values() { + resolver.resolve(recipe)?; + resolver.seen = empty(); + } + + for recipe in recipes.values() { + for line in &recipe.lines { + for fragment in line { + if let Fragment::Expression{ref expression, ..} = *fragment { + for variable in expression.variables() { + let name = variable.lexeme; + let undefined = !assignments.contains_key(name) + && !recipe.parameters.iter().any(|p| p.name == name); + if undefined { + // There's a borrow issue here that seems too difficult to solve. + // The error derived from the variable token has too short a lifetime, + // so we create a new error from its contents, which do live long + // enough. + // + // I suspect the solution here is to give recipes, pieces, and expressions + // two lifetime parameters instead of one, with one being the lifetime + // of the struct, and the second being the lifetime of the tokens + // that it contains + let error = variable.error(CompilationErrorKind::UndefinedVariable{variable: name}); + return Err(CompilationError { + text: text, + index: error.index, + line: error.line, + column: error.column, + width: error.width, + kind: CompilationErrorKind::UndefinedVariable { + variable: &text[error.index..error.index + error.width.unwrap()], + } + }); + } + } + } + } + } + } + + Ok(()) +} + +struct RecipeResolver<'a: 'b, 'b> { + stack: Vec<&'a str>, + seen: Set<&'a str>, + resolved: Set<&'a str>, + recipes: &'b Map<&'a str, Recipe<'a>>, +} + +impl<'a, 'b> RecipeResolver<'a, 'b> { + fn resolve(&mut self, recipe: &Recipe<'a>) -> Result<(), CompilationError<'a>> { + if self.resolved.contains(recipe.name) { + return Ok(()) + } + self.stack.push(recipe.name); + self.seen.insert(recipe.name); + for dependency_token in &recipe.dependency_tokens { + match self.recipes.get(dependency_token.lexeme) { + Some(dependency) => if !self.resolved.contains(dependency.name) { + if self.seen.contains(dependency.name) { + let first = self.stack[0]; + self.stack.push(first); + return Err(dependency_token.error(CompilationErrorKind::CircularRecipeDependency { + recipe: recipe.name, + circle: self.stack.iter() + .skip_while(|name| **name != dependency.name) + .cloned().collect() + })); + } + self.resolve(dependency)?; + }, + None => return Err(dependency_token.error(CompilationErrorKind::UnknownDependency { + recipe: recipe.name, + unknown: dependency_token.lexeme + })), + } + } + self.resolved.insert(recipe.name); + self.stack.pop(); + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use testing::parse_error; + +#[test] +fn circular_recipe_dependency() { + let text = "a: b\nb: a"; + parse_error(text, CompilationError { + text: text, + index: 8, + line: 1, + column: 3, + width: Some(1), + kind: CompilationErrorKind::CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]} + }); +} + +#[test] +fn self_recipe_dependency() { + let text = "a: a"; + parse_error(text, CompilationError { + text: text, + index: 3, + line: 0, + column: 3, + width: Some(1), + kind: CompilationErrorKind::CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]} + }); +} + + +#[test] +fn unknown_dependency() { + let text = "a: b"; + parse_error(text, CompilationError { + text: text, + index: 3, + line: 0, + column: 3, + width: Some(1), + kind: CompilationErrorKind::UnknownDependency{recipe: "a", unknown: "b"} + }); +} + +#[test] +fn unknown_interpolation_variable() { + let text = "x:\n {{ hello}}"; + parse_error(text, CompilationError { + text: text, + index: 9, + line: 1, + column: 6, + width: Some(5), + kind: CompilationErrorKind::UndefinedVariable{variable: "hello"}, + }); +} + +#[test] +fn unknown_second_interpolation_variable() { + let text = "wtf=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}"; + parse_error(text, CompilationError { + text: text, + index: 33, + line: 3, + column: 16, + width: Some(3), + kind: CompilationErrorKind::UndefinedVariable{variable: "lol"}, + }); +} + +} diff --git a/src/app.rs b/src/run.rs similarity index 94% rename from src/app.rs rename to src/run.rs index e300870..0c6fa16 100644 --- a/src/app.rs +++ b/src/run.rs @@ -1,12 +1,10 @@ -extern crate clap; -extern crate libc; +use common::*; -use color::Color; -use prelude::*; use std::{convert, ffi}; -use std::collections::BTreeMap; -use self::clap::{App, Arg, ArgGroup, AppSettings}; -use super::{Slurp, RunOptions, compile, DEFAULT_SHELL, maybe_s}; +use clap::{App, Arg, ArgGroup, AppSettings}; +use compile; +use misc::maybe_s; +use configuration::DEFAULT_SHELL; macro_rules! die { ($($arg:tt)*) => {{ @@ -20,7 +18,7 @@ fn edit>(path: P) -> ! { let editor = env::var_os("EDITOR") .unwrap_or_else(|| die!("Error getting EDITOR environment variable")); - let error = process::Command::new(editor) + let error = Command::new(editor) .arg(path) .status(); @@ -30,7 +28,19 @@ fn edit>(path: P) -> ! { } } -pub fn app() { +trait Slurp { + fn slurp(&mut self) -> Result; +} + +impl Slurp for fs::File { + fn slurp(&mut self) -> io::Result { + let mut destination = String::new(); + self.read_to_string(&mut destination)?; + Ok(destination) + } +} + +pub fn run() { let matches = App::new("just") .version(concat!("v", env!("CARGO_PKG_VERSION"))) .author(env!("CARGO_PKG_AUTHORS")) @@ -121,7 +131,7 @@ pub fn app() { }; let set_count = matches.occurrences_of("SET"); - let mut overrides = BTreeMap::new(); + let mut overrides = Map::new(); if set_count > 0 { let mut values = matches.values_of("SET").unwrap(); for _ in 0..set_count { @@ -300,13 +310,13 @@ pub fn app() { die!("Justfile contains no recipes."); }; - let options = RunOptions { + let options = Configuration { dry_run: matches.is_present("DRY-RUN"), evaluate: matches.is_present("EVALUATE"), highlight: matches.is_present("HIGHLIGHT"), overrides: overrides, quiet: matches.is_present("QUIET"), - shell: matches.value_of("SHELL"), + shell: matches.value_of("SHELL").unwrap(), color: color, verbose: matches.is_present("VERBOSE"), }; @@ -320,6 +330,6 @@ pub fn app() { } } - process::exit(run_error.code().unwrap_or(libc::EXIT_FAILURE)); + process::exit(run_error.code().unwrap_or(EXIT_FAILURE)); } } diff --git a/src/runtime_error.rs b/src/runtime_error.rs new file mode 100644 index 0000000..cfaab34 --- /dev/null +++ b/src/runtime_error.rs @@ -0,0 +1,201 @@ +use common::*; + +use brev::OutputError; + +use misc::{And, Or, maybe_s, Tick, ticks, write_error_context}; + +use self::RuntimeError::*; + +fn write_token_error_context(f: &mut fmt::Formatter, token: &Token) -> Result<(), fmt::Error> { + write_error_context( + f, + token.text, + token.index, + token.line, + token.column + token.prefix.len(), + Some(token.lexeme.len()) + ) +} + +#[derive(Debug)] +pub enum RuntimeError<'a> { + ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize}, + Backtick{token: Token<'a>, output_error: OutputError}, + Code{recipe: &'a str, line_number: Option, code: i32}, + Cygpath{recipe: &'a str, output_error: OutputError}, + Internal{message: String}, + IoError{recipe: &'a str, io_error: io::Error}, + Shebang{recipe: &'a str, command: String, argument: Option, io_error: io::Error}, + Signal{recipe: &'a str, line_number: Option, signal: i32}, + TmpdirIoError{recipe: &'a str, io_error: io::Error}, + Unknown{recipe: &'a str, line_number: Option}, + UnknownOverrides{overrides: Vec<&'a str>}, + UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, +} + +impl<'a> RuntimeError<'a> { + pub fn code(&self) -> Option { + match *self { + Code{code, ..} | Backtick{output_error: OutputError::Code(code), ..} => Some(code), + _ => None, + } + } +} + +impl<'a> Display for RuntimeError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use RuntimeError::*; + let color = if f.alternate() { Color::always() } else { Color::never() }; + let error = color.error(); + let message = color.message(); + write!(f, "{} {}", error.paint("error:"), message.prefix())?; + + let mut error_token = None; + + match *self { + UnknownRecipes{ref recipes, ref suggestion} => { + write!(f, "Justfile does not contain recipe{} {}.", + maybe_s(recipes.len()), Or(&ticks(recipes)))?; + if let Some(suggestion) = *suggestion { + write!(f, "\nDid you mean `{}`?", suggestion)?; + } + }, + UnknownOverrides{ref overrides} => { + write!(f, "Variable{} {} overridden on the command line but not present in justfile", + maybe_s(overrides.len()), + And(&overrides.iter().map(Tick).collect::>()))?; + }, + ArgumentCountMismatch{recipe, found, min, max} => { + if min == max { + let expected = min; + write!(f, "Recipe `{}` got {} argument{} but {}takes {}", + recipe, found, maybe_s(found), + if expected < found { "only " } else { "" }, expected)?; + } else if found < min { + write!(f, "Recipe `{}` got {} argument{} but takes at least {}", + recipe, found, maybe_s(found), min)?; + } else if found > max { + write!(f, "Recipe `{}` got {} argument{} but takes at most {}", + recipe, found, maybe_s(found), max)?; + } + }, + Code{recipe, line_number, code} => { + if let Some(n) = line_number { + write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?; + } else { + write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; + } + }, + Cygpath{recipe, ref output_error} => match *output_error { + OutputError::Code(code) => { + write!(f, "Cygpath failed with exit code {} while translating recipe `{}` \ + shebang interpreter path", code, recipe)?; + } + OutputError::Signal(signal) => { + write!(f, "Cygpath terminated by signal {} while translating recipe `{}` \ + shebang interpreter path", signal, recipe)?; + } + OutputError::Unknown => { + write!(f, "Cygpath experienced an unknown failure while translating recipe `{}` \ + shebang interpreter path", recipe)?; + } + OutputError::Io(ref io_error) => { + match io_error.kind() { + io::ErrorKind::NotFound => write!( + f, "Could not find `cygpath` executable to translate recipe `{}` \ + shebang interpreter path:\n{}", recipe, io_error), + io::ErrorKind::PermissionDenied => write!( + f, "Could not run `cygpath` executable to translate recipe `{}` \ + shebang interpreter path:\n{}", recipe, io_error), + _ => write!(f, "Could not run `cygpath` executable:\n{}", io_error), + }?; + } + OutputError::Utf8(ref utf8_error) => { + write!(f, "Cygpath successfully translated recipe `{}` shebang interpreter path, \ + but output was not utf8: {}", recipe, utf8_error)?; + } + }, + Shebang{recipe, ref command, ref argument, ref io_error} => { + if let Some(ref argument) = *argument { + write!(f, "Recipe `{}` with shebang `#!{} {}` execution error: {}", + recipe, command, argument, io_error)?; + } else { + write!(f, "Recipe `{}` with shebang `#!{}` execution error: {}", + recipe, command, io_error)?; + } + } + Signal{recipe, line_number, signal} => { + if let Some(n) = line_number { + write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?; + } else { + write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; + } + } + Unknown{recipe, line_number} => { + if let Some(n) = line_number { + write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?; + } else { + } + }, + IoError{recipe, ref io_error} => { + match io_error.kind() { + io::ErrorKind::NotFound => write!(f, + "Recipe `{}` could not be run because just could not find `sh`:\n{}", + recipe, io_error), + io::ErrorKind::PermissionDenied => write!( + f, "Recipe `{}` could not be run because just could not run `sh`:\n{}", + recipe, io_error), + _ => write!(f, "Recipe `{}` could not be run because of an IO error while \ + launching `sh`:\n{}", recipe, io_error), + }?; + }, + TmpdirIoError{recipe, ref io_error} => + write!(f, "Recipe `{}` could not be run because of an IO error while trying \ + to create a temporary directory or write a file to that directory`:\n{}", + recipe, io_error)?, + Backtick{ref token, ref output_error} => match *output_error { + OutputError::Code(code) => { + write!(f, "Backtick failed with exit code {}\n", code)?; + error_token = Some(token); + } + OutputError::Signal(signal) => { + write!(f, "Backtick was terminated by signal {}", signal)?; + error_token = Some(token); + } + OutputError::Unknown => { + write!(f, "Backtick failed for an unknown reason")?; + error_token = Some(token); + } + OutputError::Io(ref io_error) => { + match io_error.kind() { + io::ErrorKind::NotFound => write!( + f, "Backtick could not be run because just could not find `sh`:\n{}", + io_error), + io::ErrorKind::PermissionDenied => write!( + f, "Backtick could not be run because just could not run `sh`:\n{}", io_error), + _ => write!(f, "Backtick could not be run because of an IO \ + error while launching `sh`:\n{}", io_error), + }?; + error_token = Some(token); + } + OutputError::Utf8(ref utf8_error) => { + write!(f, "Backtick succeeded but stdout was not utf8: {}", utf8_error)?; + error_token = Some(token); + } + }, + Internal{ref message} => { + write!(f, "Internal error, this may indicate a bug in just: {} \ + consider filing an issue: https://github.com/casey/just/issues/new", + message)?; + } + } + + write!(f, "{}", message.suffix())?; + + if let Some(token) = error_token { + write_token_error_context(f, token)?; + } + + Ok(()) + } +} diff --git a/src/shebang.rs b/src/shebang.rs new file mode 100644 index 0000000..ae58b2c --- /dev/null +++ b/src/shebang.rs @@ -0,0 +1,60 @@ +pub struct Shebang<'a> { + pub interpreter: &'a str, + pub argument: Option<&'a str>, +} + +impl<'a> Shebang<'a> { + pub fn new(text: &'a str) -> Option> { + if !text.starts_with("#!") { + return None; + } + + let mut pieces = text[2..] + .lines() + .nth(0) + .unwrap_or("") + .trim() + .splitn(2, |c| c == ' ' || c == '\t'); + + let interpreter = pieces.next().unwrap_or(""); + let argument = pieces.next(); + + if interpreter == "" { + return None; + } + + Some(Shebang{interpreter, argument}) + } +} + +#[cfg(test)] +mod test { + use super::Shebang; + + #[test] + fn split_shebang() { + fn check(text: &str, expected_split: Option<(&str, Option<&str>)>) { + let shebang = Shebang::new(text); + assert_eq!(shebang.map(|shebang| (shebang.interpreter, shebang.argument)), expected_split); + } + + check("#! ", None ); + check("#!", 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("#!/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 ); + 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 ); + } +} diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..8e17104 --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,25 @@ +use common::*; + +use compile; + +pub fn parse_success(text: &str) -> Justfile { + match compile(text) { + Ok(justfile) => justfile, + Err(error) => panic!("Expected successful parse but got error:\n{}", error), + } +} + +pub fn parse_error(text: &str, expected: CompilationError) { + if let Err(error) = compile(text) { + assert_eq!(error.text, expected.text); + assert_eq!(error.index, expected.index); + assert_eq!(error.line, expected.line); + assert_eq!(error.column, expected.column); + assert_eq!(error.kind, expected.kind); + assert_eq!(error.width, expected.width); + assert_eq!(error, expected); + } else { + panic!("Expected {:?} but parse succeeded", expected.kind); + } +} + diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000..3f713dd --- /dev/null +++ b/src/token.rs @@ -0,0 +1,71 @@ +use common::*; + +#[derive(Debug, PartialEq, Clone)] +pub struct Token<'a> { + pub index: usize, + pub line: usize, + pub column: usize, + pub text: &'a str, + pub prefix: &'a str, + pub lexeme: &'a str, + pub kind: TokenKind, +} + +impl<'a> Token<'a> { + pub fn error(&self, kind: CompilationErrorKind<'a>) -> CompilationError<'a> { + CompilationError { + text: self.text, + index: self.index + self.prefix.len(), + line: self.line, + column: self.column + self.prefix.len(), + width: Some(self.lexeme.len()), + kind: kind, + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum TokenKind { + At, + Backtick, + Colon, + Comment, + Dedent, + Eof, + Eol, + Equals, + Indent, + InterpolationEnd, + InterpolationStart, + Line, + Name, + Plus, + RawString, + StringToken, + Text, +} + +impl Display for TokenKind { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + use TokenKind::*; + write!(f, "{}", match *self { + Backtick => "backtick", + Colon => "':'", + Comment => "comment", + Dedent => "dedent", + Eof => "end of file", + Eol => "end of line", + Equals => "'='", + Indent => "indent", + InterpolationEnd => "'}}'", + InterpolationStart => "'{{'", + Line => "command", + Name => "name", + Plus => "'+'", + At => "'@'", + StringToken => "string", + RawString => "raw string", + Text => "command text", + }) + } +} diff --git a/src/tokenizer.rs b/src/tokenizer.rs new file mode 100644 index 0000000..ecd32b2 --- /dev/null +++ b/src/tokenizer.rs @@ -0,0 +1,585 @@ +use common::*; + +use TokenKind::*; + +fn re(pattern: &str) -> Regex { + Regex::new(pattern).unwrap() +} + +fn token(pattern: &str) -> Regex { + let mut s = String::new(); + s += r"^(?m)([ \t]*)("; + s += pattern; + s += ")"; + re(&s) +} + +fn mixed_whitespace(text: &str) -> bool { + !(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t')) +} + +pub fn tokenize(text: &str) -> Result, CompilationError> { + lazy_static! { + static ref BACKTICK: Regex = token(r"`[^`\n\r]*`" ); + static ref COLON: Regex = token(r":" ); + static ref AT: Regex = token(r"@" ); + static ref COMMENT: Regex = token(r"#([^!\n\r].*)?$" ); + static ref EOF: Regex = token(r"(?-m)$" ); + static ref EOL: Regex = token(r"\n|\r\n" ); + static ref EQUALS: Regex = token(r"=" ); + static ref INTERPOLATION_END: 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 PLUS: Regex = token(r"[+]" ); + static ref STRING: Regex = token("\"" ); + static ref RAW_STRING: Regex = token(r#"'[^']*'"# ); + static ref UNTERMINATED_RAW_STRING: Regex = token(r#"'[^']*"# ); + static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" ); + static ref INTERPOLATION_START: Regex = re(r"^[{][{]" ); + static ref LEADING_TEXT: Regex = re(r"^(?m)(.+?)[{][{]" ); + static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$"); + static ref TEXT: Regex = re(r"^(?m)(.+)" ); + } + + #[derive(PartialEq)] + enum State<'a> { + Start, + Indent(&'a str), + Text, + Interpolation, + } + + fn indentation(text: &str) -> Option<&str> { + INDENT.captures(text).map(|captures| captures.get(1).unwrap().as_str()) + } + + let mut tokens = vec![]; + let mut rest = text; + let mut index = 0; + let mut line = 0; + let mut column = 0; + let mut state = vec![State::Start]; + + macro_rules! error { + ($kind:expr) => {{ + Err(CompilationError { + text: text, + index: index, + line: line, + column: column, + width: None, + kind: $kind, + }) + }}; + } + + loop { + if column == 0 { + if let Some(kind) = match (state.last().unwrap(), indentation(rest)) { + // ignore: was no indentation and there still isn't + // or current line is blank + (&State::Start, Some("")) | (_, None) => { + None + } + // indent: was no indentation, now there is + (&State::Start, Some(current)) => { + if mixed_whitespace(current) { + return error!(CompilationErrorKind::MixedLeadingWhitespace{whitespace: current}) + } + //indent = Some(current); + state.push(State::Indent(current)); + Some(Indent) + } + // dedent: there was indentation and now there isn't + (&State::Indent(_), Some("")) => { + // indent = None; + state.pop(); + Some(Dedent) + } + // was indentation and still is, check if the new indentation matches + (&State::Indent(previous), Some(current)) => { + if !current.starts_with(previous) { + return error!(CompilationErrorKind::InconsistentLeadingWhitespace{ + expected: previous, + found: current + }); + } + None + } + // at column 0 in some other state: this should never happen + (&State::Text, _) | (&State::Interpolation, _) => { + return error!(CompilationErrorKind::Internal { + message: "unexpected state at column 0".to_string() + }); + } + } { + tokens.push(Token { + index: index, + line: line, + column: column, + text: text, + prefix: "", + lexeme: "", + kind: kind, + }); + } + } + + // insert a dedent if we're indented and we hit the end of the file + if &State::Start != state.last().unwrap() && EOF.is_match(rest) { + tokens.push(Token { + index: index, + line: line, + column: column, + text: text, + prefix: "", + lexeme: "", + kind: Dedent, + }); + } + + let (prefix, lexeme, kind) = + if let (0, &State::Indent(indent), Some(captures)) = + (column, state.last().unwrap(), LINE.captures(rest)) { + let line = captures.get(0).unwrap().as_str(); + if !line.starts_with(indent) { + return error!(CompilationErrorKind::Internal{message: "unexpected indent".to_string()}); + } + state.push(State::Text); + (&line[0..indent.len()], "", Line) + } else if let Some(captures) = EOF.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eof) + } else if let State::Text = *state.last().unwrap() { + if let Some(captures) = INTERPOLATION_START.captures(rest) { + state.push(State::Interpolation); + ("", captures.get(0).unwrap().as_str(), InterpolationStart) + } else if let Some(captures) = LEADING_TEXT.captures(rest) { + ("", captures.get(1).unwrap().as_str(), Text) + } else if let Some(captures) = TEXT.captures(rest) { + ("", captures.get(1).unwrap().as_str(), Text) + } else if let Some(captures) = EOL.captures(rest) { + state.pop(); + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) + } else { + return error!(CompilationErrorKind::Internal { + message: format!("Could not match token in text state: \"{}\"", rest) + }); + } + } else if let Some(captures) = INTERPOLATION_START_TOKEN.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationStart) + } else if let Some(captures) = INTERPOLATION_END.captures(rest) { + if state.last().unwrap() == &State::Interpolation { + state.pop(); + } + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), InterpolationEnd) + } else if let Some(captures) = NAME.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Name) + } else if let Some(captures) = EOL.captures(rest) { + if state.last().unwrap() == &State::Interpolation { + return error!(CompilationErrorKind::Internal { + message: "hit EOL while still in interpolation state".to_string() + }); + } + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Eol) + } else if let Some(captures) = BACKTICK.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Backtick) + } else if let Some(captures) = COLON.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Colon) + } else if let Some(captures) = AT.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), At) + } else if let Some(captures) = PLUS.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Plus) + } else if let Some(captures) = EQUALS.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Equals) + } else if let Some(captures) = COMMENT.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), Comment) + } else if let Some(captures) = RAW_STRING.captures(rest) { + (captures.get(1).unwrap().as_str(), captures.get(2).unwrap().as_str(), RawString) + } else if UNTERMINATED_RAW_STRING.is_match(rest) { + return error!(CompilationErrorKind::UnterminatedString); + } else if let Some(captures) = STRING.captures(rest) { + let prefix = captures.get(1).unwrap().as_str(); + let contents = &rest[prefix.len()+1..]; + if contents.is_empty() { + return error!(CompilationErrorKind::UnterminatedString); + } + let mut len = 0; + let mut escape = false; + for c in contents.chars() { + if c == '\n' || c == '\r' { + return error!(CompilationErrorKind::UnterminatedString); + } else if !escape && c == '"' { + break; + } else if !escape && c == '\\' { + escape = true; + } else if escape { + escape = false; + } + len += c.len_utf8(); + } + let start = prefix.len(); + let content_end = start + len + 1; + if escape || content_end >= rest.len() { + return error!(CompilationErrorKind::UnterminatedString); + } + (prefix, &rest[start..content_end + 1], StringToken) + } else if rest.starts_with("#!") { + return error!(CompilationErrorKind::OuterShebang) + } else { + return error!(CompilationErrorKind::UnknownStartOfToken) + }; + + tokens.push(Token { + index: index, + line: line, + column: column, + prefix: prefix, + text: text, + lexeme: lexeme, + kind: kind, + }); + + let len = prefix.len() + lexeme.len(); + + if len == 0 { + let last = tokens.last().unwrap(); + match last.kind { + Eof => {}, + _ => return Err(last.error(CompilationErrorKind::Internal { + message: format!("zero length token: {:?}", last) + })), + } + } + + match tokens.last().unwrap().kind { + Eol => { + line += 1; + column = 0; + } + Eof => { + break; + } + RawString => { + let lexeme_lines = lexeme.lines().count(); + line += lexeme_lines - 1; + if lexeme_lines == 1 { + column += len; + } else { + column = lexeme.lines().last().unwrap().len(); + } + } + _ => { + column += len; + } + } + + rest = &rest[len..]; + index += len; + } + + Ok(tokens) +} + +#[cfg(test)] +mod test { + use super::*; + use testing::parse_error; + + fn tokenize_success(text: &str, expected_summary: &str) { + let tokens = tokenize(text).unwrap(); + let roundtrip = tokens.iter().map(|t| { + let mut s = String::new(); + s += t.prefix; + s += t.lexeme; + s + }).collect::>().join(""); + let summary = token_summary(&tokens); + if summary != expected_summary { + panic!("token summary mismatch:\nexpected: {}\ngot: {}\n", expected_summary, summary); + } + assert_eq!(text, roundtrip); + } + + fn tokenize_error(text: &str, expected: CompilationError) { + if let Err(error) = tokenize(text) { + assert_eq!(error.text, expected.text); + assert_eq!(error.index, expected.index); + assert_eq!(error.line, expected.line); + assert_eq!(error.column, expected.column); + assert_eq!(error.kind, expected.kind); + assert_eq!(error, expected); + } else { + panic!("tokenize() succeeded but expected: {}\n{}", expected, text); + } + } + + fn token_summary(tokens: &[Token]) -> String { + tokens.iter().map(|t| { + match t.kind { + At => "@", + Backtick => "`", + Colon => ":", + Comment{..} => "#", + Dedent => "<", + Eof => ".", + Eol => "$", + Equals => "=", + Indent{..} => ">", + InterpolationEnd => "}", + InterpolationStart => "{", + Line{..} => "^", + Name => "N", + Plus => "+", + RawString => "'", + StringToken => "\"", + Text => "_", + } + }).collect::>().join("") + } + +#[test] +fn tokanize_strings() { + tokenize_success( + r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#, + r#"N="+'+"+'#."# + ); +} + +#[test] +fn tokenize_recipe_interpolation_eol() { + let text = "foo: # some comment + {{hello}} +"; + tokenize_success(text, "N:#$>^{N}$<."); +} + +#[test] +fn tokenize_recipe_interpolation_eof() { + let text = "foo: # more comments + {{hello}} +# another comment +"; + tokenize_success(text, "N:#$>^{N}$<#$."); +} + +#[test] +fn tokenize_recipe_complex_interpolation_expression() { + let text = "foo: #lol\n {{a + b + \"z\" + blarg}}"; + tokenize_success(text, "N:#$>^{N+N+\"+N}<."); +} + +#[test] +fn tokenize_recipe_multiple_interpolations() { + let text = "foo:#ok\n {{a}}0{{b}}1{{c}}"; + tokenize_success(text, "N:#$>^{N}_{N}_{N}<."); +} + +#[test] +fn tokenize_junk() { + let text = "bob + +hello blah blah blah : a b c #whatever +"; + tokenize_success(text, "N$$NNNN:NNN#$."); +} + +#[test] +fn tokenize_empty_lines() { + let text = " +# this does something +hello: + asdf + bsdf + + csdf + + dsdf # whatever + +# yolo + "; + + tokenize_success(text, "$#$N:$>^_$^_$$^_$$^_$$<#$."); +} + +#[test] +fn tokenize_comment_before_variable() { + let text = " +# +A='1' +echo: + echo {{A}} + "; + tokenize_success(text, "$#$N='$N:$>^_{N}$<."); +} + +#[test] +fn tokenize_interpolation_backticks() { + tokenize_success( + "hello:\n echo {{`echo hello` + `echo goodbye`}}", + "N:$>^_{`+`}<." + ); +} + +#[test] +fn tokenize_assignment_backticks() { + tokenize_success( + "a = `echo hello` + `echo goodbye`", + "N=`+`." + ); +} + +#[test] +fn tokenize_multiple() { + let text = " +hello: + a + b + + c + + d + +# hello +bob: + frank + "; + + tokenize_success(text, "$N:$>^_$^_$$^_$$^_$$<#$N:$>^_$<."); +} + + +#[test] +fn tokenize_comment() { + tokenize_success("a:=#", "N:=#.") +} + +#[test] +fn tokenize_space_then_tab() { + let text = "a: + 0 + 1 +\t2 +"; + tokenize_error(text, CompilationError { + text: text, + index: 9, + line: 3, + column: 0, + width: None, + kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: " ", found: "\t"}, + }); +} + +#[test] +fn tokenize_tabs_then_tab_space() { + let text = "a: +\t\t0 +\t\t 1 +\t 2 +"; + tokenize_error(text, CompilationError { + text: text, + index: 12, + line: 3, + column: 0, + width: None, + kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "}, + }); +} + +#[test] +fn tokenize_outer_shebang() { + let text = "#!/usr/bin/env bash"; + tokenize_error(text, CompilationError { + text: text, + index: 0, + line: 0, + column: 0, + width: None, + kind: CompilationErrorKind::OuterShebang + }); +} + +#[test] +fn tokenize_unknown() { + let text = "~"; + tokenize_error(text, CompilationError { + text: text, + index: 0, + line: 0, + column: 0, + width: None, + kind: CompilationErrorKind::UnknownStartOfToken + }); +} +#[test] +fn tokenize_order() { + let text = r" +b: a + @mv a b + +a: + @touch F + @touch a + +d: c + @rm c + +c: b + @mv b c"; + tokenize_success(text, "$N:N$>^_$$^_$^_$$^_$$^_<."); +} + +#[test] +fn unterminated_string() { + let text = r#"a = ""#; + parse_error(text, CompilationError { + text: text, + index: 3, + line: 0, + column: 3, + width: None, + kind: CompilationErrorKind::UnterminatedString, + }); +} + +#[test] +fn unterminated_string_with_escapes() { + let text = r#"a = "\n\t\r\"\\"#; + parse_error(text, CompilationError { + text: text, + index: 3, + line: 0, + column: 3, + width: None, + kind: CompilationErrorKind::UnterminatedString, + }); +} +#[test] +fn unterminated_raw_string() { + let text = "r a='asdf"; + parse_error(text, CompilationError { + text: text, + index: 4, + line: 0, + column: 4, + width: None, + kind: CompilationErrorKind::UnterminatedString, + }); +} + + +#[test] +fn mixed_leading_whitespace() { + let text = "a:\n\t echo hello"; + parse_error(text, CompilationError { + text: text, + index: 3, + line: 1, + column: 0, + width: None, + kind: CompilationErrorKind::MixedLeadingWhitespace{whitespace: "\t "} + }); +} + +} diff --git a/src/unit.rs b/src/unit.rs deleted file mode 100644 index 59678cb..0000000 --- a/src/unit.rs +++ /dev/null @@ -1,1144 +0,0 @@ -extern crate tempdir; -extern crate brev; - -use super::{ - And, CompilationError, CompilationErrorKind, Justfile, Or, - OutputError, RuntimeError, RunOptions, Token, - compile, contains, tokenize -}; - -use super::TokenKind::*; - -fn tokenize_success(text: &str, expected_summary: &str) { - let tokens = tokenize(text).unwrap(); - let roundtrip = tokens.iter().map(|t| { - let mut s = String::new(); - s += t.prefix; - s += t.lexeme; - s - }).collect::>().join(""); - let summary = token_summary(&tokens); - if summary != expected_summary { - panic!("token summary mismatch:\nexpected: {}\ngot: {}\n", expected_summary, summary); - } - assert_eq!(text, roundtrip); -} - -fn tokenize_error(text: &str, expected: CompilationError) { - if let Err(error) = tokenize(text) { - assert_eq!(error.text, expected.text); - assert_eq!(error.index, expected.index); - assert_eq!(error.line, expected.line); - assert_eq!(error.column, expected.column); - assert_eq!(error.kind, expected.kind); - assert_eq!(error, expected); - } else { - panic!("tokenize() succeeded but expected: {}\n{}", expected, text); - } -} - -fn token_summary(tokens: &[Token]) -> String { - tokens.iter().map(|t| { - match t.kind { - At => "@", - Backtick => "`", - Colon => ":", - Comment{..} => "#", - Dedent => "<", - Eof => ".", - Eol => "$", - Equals => "=", - Indent{..} => ">", - InterpolationEnd => "}", - InterpolationStart => "{", - Line{..} => "^", - Name => "N", - Plus => "+", - RawString => "'", - StringToken => "\"", - Text => "_", - } - }).collect::>().join("") -} - -fn parse_success(text: &str) -> Justfile { - match compile(text) { - Ok(justfile) => justfile, - Err(error) => panic!("Expected successful parse but got error:\n{}", error), - } -} - -fn parse_summary(input: &str, output: &str) { - let justfile = parse_success(input); - let s = format!("{:#}", justfile); - if s != output { - println!("got:\n\"{}\"\n", s); - println!("\texpected:\n\"{}\"", output); - assert_eq!(s, output); - } -} - -fn parse_error(text: &str, expected: CompilationError) { - if let Err(error) = compile(text) { - assert_eq!(error.text, expected.text); - assert_eq!(error.index, expected.index); - assert_eq!(error.line, expected.line); - assert_eq!(error.column, expected.column); - assert_eq!(error.kind, expected.kind); - assert_eq!(error.width, expected.width); - assert_eq!(error, expected); - } else { - panic!("Expected {:?} but parse succeeded", expected.kind); - } -} - -#[test] -fn tokanize_strings() { - tokenize_success( - r#"a = "'a'" + '"b"' + "'c'" + '"d"'#echo hello"#, - r#"N="+'+"+'#."# - ); -} - -#[test] -fn tokenize_recipe_interpolation_eol() { - let text = "foo: # some comment - {{hello}} -"; - tokenize_success(text, "N:#$>^{N}$<."); -} - -#[test] -fn tokenize_recipe_interpolation_eof() { - let text = "foo: # more comments - {{hello}} -# another comment -"; - tokenize_success(text, "N:#$>^{N}$<#$."); -} - -#[test] -fn tokenize_recipe_complex_interpolation_expression() { - let text = "foo: #lol\n {{a + b + \"z\" + blarg}}"; - tokenize_success(text, "N:#$>^{N+N+\"+N}<."); -} - -#[test] -fn tokenize_recipe_multiple_interpolations() { - let text = "foo:#ok\n {{a}}0{{b}}1{{c}}"; - tokenize_success(text, "N:#$>^{N}_{N}_{N}<."); -} - -#[test] -fn tokenize_junk() { - let text = "bob - -hello blah blah blah : a b c #whatever -"; - tokenize_success(text, "N$$NNNN:NNN#$."); -} - -#[test] -fn tokenize_empty_lines() { - let text = " -# this does something -hello: - asdf - bsdf - - csdf - - dsdf # whatever - -# yolo - "; - - tokenize_success(text, "$#$N:$>^_$^_$$^_$$^_$$<#$."); -} - -#[test] -fn tokenize_comment_before_variable() { - let text = " -# -A='1' -echo: - echo {{A}} - "; - tokenize_success(text, "$#$N='$N:$>^_{N}$<."); -} - -#[test] -fn tokenize_interpolation_backticks() { - tokenize_success( - "hello:\n echo {{`echo hello` + `echo goodbye`}}", - "N:$>^_{`+`}<." - ); -} - -#[test] -fn tokenize_assignment_backticks() { - tokenize_success( - "a = `echo hello` + `echo goodbye`", - "N=`+`." - ); -} - -#[test] -fn tokenize_multiple() { - let text = " -hello: - a - b - - c - - d - -# hello -bob: - frank - "; - - tokenize_success(text, "$N:$>^_$^_$$^_$$^_$$<#$N:$>^_$<."); -} - - -#[test] -fn tokenize_comment() { - tokenize_success("a:=#", "N:=#.") -} - -#[test] -fn tokenize_space_then_tab() { - let text = "a: - 0 - 1 -\t2 -"; - tokenize_error(text, CompilationError { - text: text, - index: 9, - line: 3, - column: 0, - width: None, - kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: " ", found: "\t"}, - }); -} - -#[test] -fn tokenize_tabs_then_tab_space() { - let text = "a: -\t\t0 -\t\t 1 -\t 2 -"; - tokenize_error(text, CompilationError { - text: text, - index: 12, - line: 3, - column: 0, - width: None, - kind: CompilationErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "}, - }); -} - -#[test] -fn tokenize_outer_shebang() { - let text = "#!/usr/bin/env bash"; - tokenize_error(text, CompilationError { - text: text, - index: 0, - line: 0, - column: 0, - width: None, - kind: CompilationErrorKind::OuterShebang - }); -} - -#[test] -fn tokenize_unknown() { - let text = "~"; - tokenize_error(text, CompilationError { - text: text, - index: 0, - line: 0, - column: 0, - width: None, - kind: CompilationErrorKind::UnknownStartOfToken - }); -} - -#[test] -fn parse_empty() { - parse_summary(" - -# hello - - - ", ""); -} - -#[test] -fn parse_string_default() { - parse_summary(r#" - -foo a="b\t": - - - "#, r#"foo a='b\t':"#); -} - -#[test] -fn parse_variadic() { - parse_summary(r#" - -foo +a: - - - "#, r#"foo +a:"#); -} - -#[test] -fn parse_variadic_string_default() { - parse_summary(r#" - -foo +a="Hello": - - - "#, r#"foo +a='Hello':"#); -} - -#[test] -fn parse_raw_string_default() { - parse_summary(r#" - -foo a='b\t': - - - "#, r#"foo a='b\\t':"#); -} - -#[test] -fn parse_export() { - parse_summary(r#" -export a = "hello" - - "#, r#"export a = "hello""#); -} - - -#[test] -fn parse_complex() { - parse_summary(" -x: -y: -z: -foo = \"xx\" -bar = foo -goodbye = \"y\" -hello a b c : x y z #hello - #! blah - #blarg - {{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz - 1 - 2 - 3 -", "bar = foo - -foo = \"xx\" - -goodbye = \"y\" - -hello a b c: x y z - #! blah - #blarg - {{foo + bar}}abc{{goodbye + \"x\"}}xyz - 1 - 2 - 3 - -x: - -y: - -z:"); -} - -#[test] -fn parse_shebang() { - parse_summary(" -practicum = 'hello' -install: -\t#!/bin/sh -\tif [[ -f {{practicum}} ]]; then -\t\treturn -\tfi -", "practicum = \"hello\" - -install: - #!/bin/sh - if [[ -f {{practicum}} ]]; then - \treturn - fi" - ); -} - -#[test] -fn parse_assignments() { - parse_summary( -r#"a = "0" -c = a + b + a + b -b = "1" -"#, - -r#"a = "0" - -b = "1" - -c = a + b + a + b"#); -} - -#[test] -fn parse_assignment_backticks() { - parse_summary( -"a = `echo hello` -c = a + b + a + b -b = `echo goodbye`", - -"a = `echo hello` - -b = `echo goodbye` - -c = a + b + a + b"); -} - -#[test] -fn parse_interpolation_backticks() { - parse_summary( -r#"a: - echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#, -r#"a: - echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#, - ); -} - -#[test] -fn missing_colon() { - let text = "a b c\nd e f"; - parse_error(text, CompilationError { - text: text, - index: 5, - line: 0, - column: 5, - width: Some(1), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Plus, Colon], found: Eol}, - }); -} - -#[test] -fn missing_default_eol() { - let text = "hello arg=\n"; - parse_error(text, CompilationError { - text: text, - index: 10, - line: 0, - column: 10, - width: Some(1), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eol}, - }); -} - -#[test] -fn missing_default_eof() { - let text = "hello arg="; - parse_error(text, CompilationError { - text: text, - index: 10, - line: 0, - column: 10, - width: Some(0), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Eof}, - }); -} - -#[test] -fn missing_default_colon() { - let text = "hello arg=:"; - parse_error(text, CompilationError { - text: text, - index: 10, - line: 0, - column: 10, - width: Some(1), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Colon}, - }); -} - -#[test] -fn missing_default_backtick() { - let text = "hello arg=`hello`"; - parse_error(text, CompilationError { - text: text, - index: 10, - line: 0, - column: 10, - width: Some(7), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![StringToken, RawString], found: Backtick}, - }); -} - -#[test] -fn parameter_after_variadic() { - let text = "foo +a bbb:"; - parse_error(text, CompilationError { - text: text, - index: 7, - line: 0, - column: 7, - width: Some(3), - kind: CompilationErrorKind::ParameterFollowsVariadicParameter{parameter: "bbb"} - }); -} - -#[test] -fn required_after_default() { - let text = "hello arg='foo' bar:"; - parse_error(text, CompilationError { - text: text, - index: 16, - line: 0, - column: 16, - width: Some(3), - kind: CompilationErrorKind::RequiredParameterFollowsDefaultParameter{parameter: "bar"}, - }); -} - -#[test] -fn missing_eol() { - let text = "a b c: z ="; - parse_error(text, CompilationError { - text: text, - index: 9, - line: 0, - column: 9, - width: Some(1), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, Eol, Eof], found: Equals}, - }); -} - -#[test] -fn eof_test() { - parse_summary("x:\ny:\nz:\na b c: x y z", "a b c: x y z\n\nx:\n\ny:\n\nz:"); -} - -#[test] -fn duplicate_parameter() { - let text = "a b b:"; - parse_error(text, CompilationError { - text: text, - index: 4, - line: 0, - column: 4, - width: Some(1), - kind: CompilationErrorKind::DuplicateParameter{recipe: "a", parameter: "b"} - }); -} - -#[test] -fn parameter_shadows_varible() { - let text = "foo = \"h\"\na foo:"; - parse_error(text, CompilationError { - text: text, - index: 12, - line: 1, - column: 2, - width: Some(3), - kind: CompilationErrorKind::ParameterShadowsVariable{parameter: "foo"} - }); -} - -#[test] -fn dependency_has_parameters() { - let text = "foo arg:\nb: foo"; - parse_error(text, CompilationError { - text: text, - index: 12, - line: 1, - column: 3, - width: Some(3), - kind: CompilationErrorKind::DependencyHasParameters{recipe: "b", dependency: "foo"} - }); -} - -#[test] -fn duplicate_dependency() { - let text = "a b c: b c z z"; - parse_error(text, CompilationError { - text: text, - index: 13, - line: 0, - column: 13, - width: Some(1), - kind: CompilationErrorKind::DuplicateDependency{recipe: "a", dependency: "z"} - }); -} - -#[test] -fn duplicate_recipe() { - let text = "a:\nb:\na:"; - parse_error(text, CompilationError { - text: text, - index: 6, - line: 2, - column: 0, - width: Some(1), - kind: CompilationErrorKind::DuplicateRecipe{recipe: "a", first: 0} - }); -} - -#[test] -fn circular_recipe_dependency() { - let text = "a: b\nb: a"; - parse_error(text, CompilationError { - text: text, - index: 8, - line: 1, - column: 3, - width: Some(1), - kind: CompilationErrorKind::CircularRecipeDependency{recipe: "b", circle: vec!["a", "b", "a"]} - }); -} - -#[test] -fn circular_variable_dependency() { - let text = "a = b\nb = a"; - parse_error(text, CompilationError { - text: text, - index: 0, - line: 0, - column: 0, - width: Some(1), - kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "b", "a"]} - }); -} - -#[test] -fn duplicate_variable() { - let text = "a = \"0\"\na = \"0\""; - parse_error(text, CompilationError { - text: text, - index: 8, - line: 1, - column: 0, - width: Some(1), - kind: CompilationErrorKind::DuplicateVariable{variable: "a"} - }); -} - -#[test] -fn unterminated_string() { - let text = r#"a = ""#; - parse_error(text, CompilationError { - text: text, - index: 3, - line: 0, - column: 3, - width: None, - kind: CompilationErrorKind::UnterminatedString, - }); -} - -#[test] -fn unterminated_string_with_escapes() { - let text = r#"a = "\n\t\r\"\\"#; - parse_error(text, CompilationError { - text: text, - index: 3, - line: 0, - column: 3, - width: None, - kind: CompilationErrorKind::UnterminatedString, - }); -} - -#[test] -fn unterminated_raw_string() { - let text = "r a='asdf"; - parse_error(text, CompilationError { - text: text, - index: 4, - line: 0, - column: 4, - width: None, - kind: CompilationErrorKind::UnterminatedString, - }); -} - -#[test] -fn string_quote_escape() { - parse_summary( - r#"a = "hello\"""#, - r#"a = "hello\"""# - ); -} - -#[test] -fn string_escapes() { - parse_summary( - r#"a = "\n\t\r\"\\""#, - r#"a = "\n\t\r\"\\""# - ); -} - -#[test] -fn parameters() { - parse_summary( -"a b c: - {{b}} {{c}}", -"a b c: - {{b}} {{c}}", - ); -} - -#[test] -fn self_recipe_dependency() { - let text = "a: a"; - parse_error(text, CompilationError { - text: text, - index: 3, - line: 0, - column: 3, - width: Some(1), - kind: CompilationErrorKind::CircularRecipeDependency{recipe: "a", circle: vec!["a", "a"]} - }); -} - -#[test] -fn self_variable_dependency() { - let text = "a = a"; - parse_error(text, CompilationError { - text: text, - index: 0, - line: 0, - column: 0, - width: Some(1), - kind: CompilationErrorKind::CircularVariableDependency{variable: "a", circle: vec!["a", "a"]} - }); -} - -#[test] -fn unknown_dependency() { - let text = "a: b"; - parse_error(text, CompilationError { - text: text, - index: 3, - line: 0, - column: 3, - width: Some(1), - kind: CompilationErrorKind::UnknownDependency{recipe: "a", unknown: "b"} - }); -} - -#[test] -fn mixed_leading_whitespace() { - let text = "a:\n\t echo hello"; - parse_error(text, CompilationError { - text: text, - index: 3, - line: 1, - column: 0, - width: None, - kind: CompilationErrorKind::MixedLeadingWhitespace{whitespace: "\t "} - }); -} - -#[test] -fn conjoin_or() { - assert_eq!("1", Or(&[1 ]).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, 3, or 4", Or(&[1,2,3,4]).to_string()); -} - -#[test] -fn conjoin_and() { - assert_eq!("1", And(&[1 ]).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, 3, and 4", And(&[1,2,3,4]).to_string()); -} - -#[test] -fn range() { - assert!(contains(&(0..1), 0)); - assert!(contains(&(10..20), 15)); - assert!(!contains(&(0..0), 0)); - assert!(!contains(&(1..10), 0)); - assert!(!contains(&(1..10), 10)); -} - -#[test] -fn unknown_recipes() { - match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() { - RuntimeError::UnknownRecipes{recipes, suggestion} => { - assert_eq!(recipes, &["x", "y", "z"]); - assert_eq!(suggestion, None); - } - other => panic!("expected an unknown recipe error, but got: {}", other), - } -} - -#[test] -fn extra_whitespace() { - let text = "a:\n blah\n blarg"; - parse_error(text, CompilationError { - text: text, - index: 10, - line: 2, - column: 1, - width: Some(6), - kind: CompilationErrorKind::ExtraLeadingWhitespace - }); - - // extra leading whitespace is okay in a shebang recipe - parse_success("a:\n #!\n print(1)"); -} - -#[test] -fn interpolation_outside_of_recipe() { - let text = "{{"; - parse_error(text, CompilationError { - text: text, - index: 0, - line: 0, - column: 0, - width: Some(2), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name, At], found: InterpolationStart}, - }); -} - -#[test] -fn unclosed_interpolation_delimiter() { - let text = "a:\n echo {{ foo"; - parse_error(text, CompilationError { - text: text, - index: 15, - line: 1, - column: 12, - width: Some(0), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![Plus, Eol, InterpolationEnd], found: Dedent}, - }); -} - -#[test] -fn unknown_expression_variable() { - let text = "x = yy"; - parse_error(text, CompilationError { - text: text, - index: 4, - line: 0, - column: 4, - width: Some(2), - kind: CompilationErrorKind::UndefinedVariable{variable: "yy"}, - }); -} - -#[test] -fn unknown_interpolation_variable() { - let text = "x:\n {{ hello}}"; - parse_error(text, CompilationError { - text: text, - index: 9, - line: 1, - column: 6, - width: Some(5), - kind: CompilationErrorKind::UndefinedVariable{variable: "hello"}, - }); -} - -#[test] -fn unknown_second_interpolation_variable() { - let text = "wtf=\"x\"\nx:\n echo\n foo {{wtf}} {{ lol }}"; - parse_error(text, CompilationError { - text: text, - index: 33, - line: 3, - column: 16, - width: Some(3), - kind: CompilationErrorKind::UndefinedVariable{variable: "lol"}, - }); -} - -#[test] -fn plus_following_parameter() { - let text = "a b c+:"; - parse_error(text, CompilationError { - text: text, - index: 5, - line: 0, - column: 5, - width: Some(1), - kind: CompilationErrorKind::UnexpectedToken{expected: vec![Name], found: Plus}, - }); -} - -#[test] -fn tokenize_order() { - let text = r" -b: a - @mv a b - -a: - @touch F - @touch a - -d: c - @rm c - -c: b - @mv b c"; - tokenize_success(text, "$N:N$>^_$$^_$^_$$^_$$^_<."); -} - -#[test] -fn run_shebang() { - // this test exists to make sure that shebang recipes - // run correctly. although this script is still - // executed by a shell its behavior depends on the value of a - // variable and continuing even though a command fails, - // whereas in plain recipes variables are not available - // in subsequent lines and execution stops when a line - // fails - let text = " -a: - #!/usr/bin/env sh - code=200 - x() { return $code; } - x - x -"; - - match parse_success(text).run(&["a"], &Default::default()).unwrap_err() { - RuntimeError::Code{recipe, line_number, code} => { - assert_eq!(recipe, "a"); - assert_eq!(code, 200); - assert_eq!(line_number, None); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn code_error() { - match parse_success("fail:\n @exit 100") - .run(&["fail"], &Default::default()).unwrap_err() { - RuntimeError::Code{recipe, line_number, code} => { - assert_eq!(recipe, "fail"); - assert_eq!(code, 100); - assert_eq!(line_number, Some(2)); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn run_args() { - let text = r#" -a return code: - @x() { {{return}} {{code + "0"}}; }; x"#; - - match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() { - RuntimeError::Code{recipe, line_number, code} => { - assert_eq!(recipe, "a"); - assert_eq!(code, 150); - assert_eq!(line_number, Some(3)); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn missing_some_arguments() { - match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() { - RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { - assert_eq!(recipe, "a"); - assert_eq!(found, 2); - assert_eq!(min, 3); - assert_eq!(max, 3); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn missing_some_arguments_variadic() { - match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() { - RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { - assert_eq!(recipe, "a"); - assert_eq!(found, 2); - assert_eq!(min, 3); - assert_eq!(max, super::std::usize::MAX - 1); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn missing_all_arguments() { - match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}") - .run(&["a"], &Default::default()).unwrap_err() { - RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { - assert_eq!(recipe, "a"); - assert_eq!(found, 0); - assert_eq!(min, 3); - assert_eq!(max, 3); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn missing_some_defaults() { - match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() { - RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { - assert_eq!(recipe, "a"); - assert_eq!(found, 1); - assert_eq!(min, 2); - assert_eq!(max, 3); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn missing_all_defaults() { - match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() { - RuntimeError::ArgumentCountMismatch{recipe, found, min, max} => { - assert_eq!(recipe, "a"); - assert_eq!(found, 0); - assert_eq!(min, 1); - assert_eq!(max, 3); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn backtick_code() { - match parse_success("a:\n echo {{`f() { return 100; }; f`}}") - .run(&["a"], &Default::default()).unwrap_err() { - RuntimeError::Backtick{token, output_error: OutputError::Code(code)} => { - assert_eq!(code, 100); - assert_eq!(token.lexeme, "`f() { return 100; }; f`"); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn unknown_overrides() { - let mut options: RunOptions = Default::default(); - options.overrides.insert("foo", "bar"); - options.overrides.insert("baz", "bob"); - match parse_success("a:\n echo {{`f() { return 100; }; f`}}") - .run(&["a"], &options).unwrap_err() { - RuntimeError::UnknownOverrides{overrides} => { - assert_eq!(overrides, &["baz", "foo"]); - }, - other => panic!("expected a code run error, but got: {}", other), - } -} - -#[test] -fn export_assignment_backtick() { - let text = r#" -export exported_variable = "A" -b = `echo $exported_variable` - -recipe: - echo {{b}} -"#; - - let options = RunOptions { - quiet: true, - ..Default::default() - }; - - match parse_success(text).run(&["recipe"], &options).unwrap_err() { - RuntimeError::Backtick{token, output_error: OutputError::Code(_)} => { - assert_eq!(token.lexeme, "`echo $exported_variable`"); - }, - other => panic!("expected a backtick code errror, but got: {}", other), - } -} - -#[test] -fn export_failure() { - let text = r#" -export foo = "a" -baz = "c" -export bar = "b" -export abc = foo + bar + baz - -wut: - echo $foo $bar $baz -"#; - - let options = RunOptions { - quiet: true, - ..Default::default() - }; - - match parse_success(text).run(&["wut"], &options).unwrap_err() { - RuntimeError::Code{code: _, line_number, recipe} => { - assert_eq!(recipe, "wut"); - assert_eq!(line_number, Some(8)); - }, - other => panic!("expected a recipe code errror, but got: {}", other), - } -} - -#[test] -fn readme_test() { - let mut justfiles = vec![]; - let mut current = None; - - for line in brev::slurp("README.asc").lines() { - if let Some(mut justfile) = current { - if line == "```" { - justfiles.push(justfile); - current = None; - } else { - justfile += line; - justfile += "\n"; - current = Some(justfile); - } - } else if line == "```make" { - current = Some(String::new()); - } - } - - for justfile in justfiles { - parse_success(&justfile); - } -} - -#[test] -fn split_shebang() { - use ::split_shebang; - - fn check(shebang: &str, expected_split: Option<(&str, Option<&str>)>) { - assert_eq!(split_shebang(shebang), expected_split); - } - - check("#! ", Some(("", None ))); - check("#!", Some(("", 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("#!/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 ); -} diff --git a/src/integration.rs b/tests/integration.rs similarity index 99% rename from src/integration.rs rename to tests/integration.rs index f18a5fe..cb14089 100644 --- a/src/integration.rs +++ b/tests/integration.rs @@ -1,9 +1,14 @@ extern crate tempdir; extern crate brev; +extern crate libc; +extern crate utilities; -use ::prelude::*; -use tempdir::TempDir; +use libc::{EXIT_FAILURE, EXIT_SUCCESS}; +use std::env; +use std::process; use std::str; +use tempdir::TempDir; +use utilities::just_binary_path; /// Instantiate integration tests for a given test case using /// sh, dash, and bash. @@ -22,8 +27,7 @@ macro_rules! integration_test { status: $status:expr, ) => { mod $name { - use ::prelude::*; - use super::integration_test; + use super::*; // silence unused import warnings const __: i32 = EXIT_SUCCESS; @@ -50,7 +54,7 @@ fn integration_test( path.push("justfile"); brev::dump(path, justfile); - let output = process::Command::new(&super::test_utils::just_binary_path()) + let output = process::Command::new(&just_binary_path()) .current_dir(tmp.path()) .args(&["--shell", shell]) .args(args) diff --git a/src/search.rs b/tests/search.rs similarity index 96% rename from src/search.rs rename to tests/search.rs index 80339ba..90e2deb 100644 --- a/src/search.rs +++ b/tests/search.rs @@ -1,10 +1,13 @@ -use ::prelude::*; +extern crate utilities; +extern crate brev; +extern crate tempdir; + +use utilities::just_binary_path; use tempdir::TempDir; -use std::{path, str}; -use super::brev; +use std::{path, str, fs, process}; fn search_test>(path: P, args: &[&str]) { - let binary = super::test_utils::just_binary_path(); + let binary = just_binary_path(); let output = process::Command::new(binary) .current_dir(path) diff --git a/utilities/Cargo.toml b/utilities/Cargo.toml new file mode 100644 index 0000000..080c4fd --- /dev/null +++ b/utilities/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "utilities" +version = "0.0.0" +authors = ["Casey Rodarmor "] +publish = false diff --git a/src/test_utils.rs b/utilities/src/lib.rs similarity index 85% rename from src/test_utils.rs rename to utilities/src/lib.rs index ccb2ae4..0c22444 100644 --- a/src/test_utils.rs +++ b/utilities/src/lib.rs @@ -1,4 +1,5 @@ -use ::prelude::*; +use std::env; +use std::path::PathBuf; pub fn just_binary_path() -> PathBuf { let mut path = env::current_exe().unwrap(); @@ -10,3 +11,4 @@ pub fn just_binary_path() -> PathBuf { path.push(exe); path } +