just/src/lib.rs

1165 lines
35 KiB
Rust

#[cfg(test)]
mod tests;
mod app;
pub use app::app;
#[macro_use]
extern crate lazy_static;
extern crate regex;
extern crate tempdir;
use std::io::prelude::*;
use std::{fs, fmt, process, io};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fmt::Display;
use regex::Regex;
use std::os::unix::fs::PermissionsExt;
macro_rules! warn {
($($arg:tt)*) => {{
extern crate std;
use std::io::prelude::*;
let _ = writeln!(&mut std::io::stderr(), $($arg)*);
}};
}
macro_rules! die {
($($arg:tt)*) => {{
extern crate std;
warn!($($arg)*);
std::process::exit(-1)
}};
}
trait Slurp {
fn slurp(&mut self) -> Result<String, std::io::Error>;
}
impl Slurp for fs::File {
fn slurp(&mut self) -> Result<String, std::io::Error> {
let mut destination = String::new();
try!(self.read_to_string(&mut destination));
Ok(destination)
}
}
fn re(pattern: &str) -> Regex {
Regex::new(pattern).unwrap()
}
#[derive(PartialEq, Debug)]
struct Recipe<'a> {
line_number: usize,
name: &'a str,
lines: Vec<&'a str>,
fragments: Vec<Vec<Fragment<'a>>>,
variables: BTreeSet<&'a str>,
dependencies: Vec<&'a str>,
dependency_tokens: Vec<Token<'a>>,
arguments: Vec<&'a str>,
argument_tokens: Vec<Token<'a>>,
shebang: bool,
}
#[derive(PartialEq, Debug)]
enum Fragment<'a> {
Text{text: &'a str},
Variable{name: &'a str},
}
enum Expression<'a> {
Variable{name: &'a str},
String{contents: &'a str},
Concatination{lhs: Box<Expression<'a>>, rhs: Box<Expression<'a>>},
}
impl<'a> Display for Expression<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Expression::Variable {name } => try!(write!(f, "{}", name)),
Expression::String {contents } => try!(write!(f, "\"{}\"", contents)),
Expression::Concatination{ref lhs, ref rhs} => try!(write!(f, "{} + {}", lhs, rhs)),
}
Ok(())
}
}
#[cfg(unix)]
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError {
use std::os::unix::process::ExitStatusExt;
match exit_status.signal() {
Some(signal) => RunError::Signal{recipe: recipe, signal: signal},
None => RunError::UnknownFailure{recipe: recipe},
}
}
#[cfg(windows)]
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError {
RunError::UnknownFailure{recipe: recipe}
}
impl<'a> Recipe<'a> {
fn run(&self) -> Result<(), RunError<'a>> {
if self.shebang {
let tmp = try!(
tempdir::TempDir::new("j")
.map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})
);
let mut path = tmp.path().to_path_buf();
path.push(self.name);
{
let mut f = try!(
fs::File::create(&path)
.map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})
);
let mut text = String::new();
// add the shebang
text += self.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 &self.lines[1..] {
text += line;
text += "\n";
}
try!(
f.write_all(text.as_bytes())
.map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})
);
}
// get current permissions
let mut perms = try!(
fs::metadata(&path)
.map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error})
).permissions();
// make the script executable
let current_mode = perms.mode();
perms.set_mode(current_mode | 0o100);
try!(fs::set_permissions(&path, perms).map_err(|error| RunError::TmpdirIoError{recipe: self.name, io_error: error}));
// run it!
let status = process::Command::new(path).status();
try!(match status {
Ok(exit_status) => if let Some(code) = exit_status.code() {
if code == 0 {
Ok(())
} else {
Err(RunError::Code{recipe: self.name, code: code})
}
} else {
Err(error_from_signal(self.name, exit_status))
},
Err(io_error) => Err(RunError::TmpdirIoError{recipe: self.name, io_error: io_error})
});
} else {
for command in &self.lines {
let mut command = *command;
if !command.starts_with('@') {
warn!("{}", command);
} else {
command = &command[1..];
}
let status = process::Command::new("sh")
.arg("-cu")
.arg(command)
.status();
try!(match status {
Ok(exit_status) => if let Some(code) = exit_status.code() {
if code == 0 {
Ok(())
} else {
Err(RunError::Code{recipe: self.name, code: code})
}
} else {
Err(error_from_signal(self.name, exit_status))
},
Err(io_error) => Err(RunError::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> {
try!(write!(f, "{}", self.name));
for argument in &self.arguments {
try!(write!(f, " {}", argument));
}
try!(write!(f, ":"));
for dependency in &self.dependencies {
try!(write!(f, " {}", dependency))
}
for (i, fragments) in self.fragments.iter().enumerate() {
if i == 0 {
try!(writeln!(f, ""));
}
for (j, fragment) in fragments.iter().enumerate() {
if j == 0 {
try!(write!(f, " "));
}
match *fragment {
Fragment::Text{text} => try!(write!(f, "{}", text)),
Fragment::Variable{name} => try!(write!(f, "{}{}{}", "{{", name, "}}")),
}
}
if i + 1 < self.fragments.len() {
try!(write!(f, "\n"));
}
}
Ok(())
}
}
fn resolve<'a>(recipes: &BTreeMap<&'a str, Recipe<'a>>) -> Result<(), Error<'a>> {
let mut resolver = Resolver {
seen: HashSet::new(),
stack: vec![],
resolved: HashSet::new(),
recipes: recipes,
};
for recipe in recipes.values() {
try!(resolver.resolve(&recipe));
}
Ok(())
}
struct Resolver<'a: 'b, 'b> {
stack: Vec<&'a str>,
seen: HashSet<&'a str>,
resolved: HashSet<&'a str>,
recipes: &'b BTreeMap<&'a str, Recipe<'a>>
}
impl<'a, 'b> Resolver<'a, 'b> {
fn resolve(&mut self, recipe: &Recipe<'a>) -> Result<(), Error<'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(ErrorKind::CircularRecipeDependency {
recipe: recipe.name,
circle: self.stack.iter()
.skip_while(|name| **name != dependency.name)
.cloned().collect()
}));
}
return self.resolve(dependency);
},
None => return Err(dependency_token.error(ErrorKind::UnknownDependency {
recipe: recipe.name,
unknown: dependency_token.lexeme
})),
}
}
self.resolved.insert(recipe.name);
self.stack.pop();
Ok(())
}
}
fn evaluate<'a>(
assignments: &BTreeMap<&'a str, Expression<'a>>,
assignment_tokens: &BTreeMap<&'a str, Token<'a>>,
) -> Result<BTreeMap<&'a str, String>, Error<'a>> {
let mut evaluator = Evaluator{
seen: HashSet::new(),
stack: vec![],
evaluated: BTreeMap::new(),
assignments: assignments,
assignment_tokens: assignment_tokens,
};
for name in assignments.keys() {
try!(evaluator.evaluate_assignment(name));
}
Ok(evaluator.evaluated)
}
struct Evaluator<'a: 'b, 'b> {
stack: Vec<&'a str>,
seen: HashSet<&'a str>,
evaluated: BTreeMap<&'a str, String>,
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
assignment_tokens: &'b BTreeMap<&'a str, Token<'a>>,
}
impl<'a, 'b> Evaluator<'a, 'b> {
fn evaluate_assignment(&mut self, name: &'a str) -> Result<(), Error<'a>> {
if self.evaluated.contains_key(name) {
return Ok(());
}
self.stack.push(name);
self.seen.insert(name);
if let Some(expression) = self.assignments.get(name) {
let value = try!(self.evaluate_expression(expression));
self.evaluated.insert(name, value);
} else {
let token = self.assignment_tokens.get(name).unwrap();
return Err(token.error(ErrorKind::UnknownVariable {variable: name}));
}
self.stack.pop();
Ok(())
}
fn evaluate_expression(&mut self, expression: &Expression<'a>,) -> Result<String, Error<'a>> {
Ok(match *expression {
Expression::Variable{name} => {
if self.evaluated.contains_key(name) {
self.evaluated.get(name).unwrap().clone()
} else if self.seen.contains(name) {
let token = self.assignment_tokens.get(name).unwrap();
self.stack.push(name);
return Err(token.error(ErrorKind::CircularVariableDependency {
variable: name,
circle: self.stack.clone(),
}));
} else {
try!(self.evaluate_assignment(name));
self.evaluated.get(name).unwrap().clone()
}
}
Expression::String{contents} => {
contents.to_string()
}
Expression::Concatination{ref lhs, ref rhs} => {
try!(self.evaluate_expression(lhs))
+
&try!(self.evaluate_expression(rhs))
}
})
}
}
#[derive(Debug, PartialEq)]
struct Error<'a> {
text: &'a str,
index: usize,
line: usize,
column: usize,
width: Option<usize>,
kind: ErrorKind<'a>,
}
#[derive(Debug, PartialEq)]
enum ErrorKind<'a> {
BadName{name: &'a str},
CircularRecipeDependency{recipe: &'a str, circle: Vec<&'a str>},
CircularVariableDependency{variable: &'a str, circle: Vec<&'a str>},
DuplicateDependency{recipe: &'a str, dependency: &'a str},
DuplicateArgument{recipe: &'a str, argument: &'a str},
DuplicateRecipe{recipe: &'a str, first: usize},
DuplicateVariable{variable: &'a str},
ArgumentShadowsVariable{argument: &'a str},
MixedLeadingWhitespace{whitespace: &'a str},
UnmatchedInterpolationDelimiter{recipe: &'a str},
BadInterpolationVariableName{recipe: &'a str, text: &'a str},
ExtraLeadingWhitespace,
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
OuterShebang,
UnknownDependency{recipe: &'a str, unknown: &'a str},
UnknownVariable{variable: &'a str},
UnknownStartOfToken,
UnexpectedToken{expected: Vec<TokenKind>, found: TokenKind},
InternalError{message: String},
}
fn show_whitespace(text: &str) -> String {
text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect()
}
fn mixed_whitespace(text: &str) -> bool {
!(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t'))
}
struct Or<'a, T: 'a + Display>(&'a [T]);
impl<'a, T: Display> Display for Or<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self.0.len() {
0 => {},
1 => try!(write!(f, "{}", self.0[0])),
2 => try!(write!(f, "{} or {}", self.0[0], self.0[1])),
_ => for (i, item) in self.0.iter().enumerate() {
try!(write!(f, "{}", item));
if i == self.0.len() - 1 {
} else if i == self.0.len() - 2 {
try!(write!(f, ", or "));
} else {
try!(write!(f, ", "))
}
},
}
Ok(())
}
}
impl<'a> Display for Error<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
try!(write!(f, "justfile:{}: ", self.line));
match self.kind {
ErrorKind::BadName{name} => {
try!(writeln!(f, "name did not match /[a-z](-?[a-z0-9])*/: {}", name));
}
ErrorKind::CircularRecipeDependency{recipe, ref circle} => {
if circle.len() == 2 {
try!(write!(f, "recipe 1{} depends on itself", recipe));
} else {
try!(write!(f, "recipe {} has circular dependency: {}", recipe, circle.join(" -> ")));
}
return Ok(());
}
ErrorKind::CircularVariableDependency{variable, ref circle} => {
try!(write!(f, "assignment to {} has circular dependency: {}", variable, circle.join(" -> ")));
return Ok(());
}
ErrorKind::DuplicateArgument{recipe, argument} => {
try!(writeln!(f, "recipe {} has duplicate argument: {}", recipe, argument));
}
ErrorKind::DuplicateVariable{variable} => {
try!(writeln!(f, "variable \"{}\" is has multiple definitions", variable));
}
ErrorKind::UnexpectedToken{ref expected, found} => {
try!(writeln!(f, "expected {} but found {}", Or(expected), found));
}
ErrorKind::DuplicateDependency{recipe, dependency} => {
try!(writeln!(f, "recipe {} has duplicate dependency: {}", recipe, dependency));
}
ErrorKind::DuplicateRecipe{recipe, first} => {
try!(write!(f, "duplicate recipe: {} appears on lines {} and {}",
recipe, first, self.line));
return Ok(());
}
ErrorKind::ArgumentShadowsVariable{argument} => {
try!(writeln!(f, "argument {} shadows variable of the same name", argument));
}
ErrorKind::MixedLeadingWhitespace{whitespace} => {
try!(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)
));
}
ErrorKind::ExtraLeadingWhitespace => {
try!(writeln!(f, "recipe line has extra leading whitespace"));
}
ErrorKind::InconsistentLeadingWhitespace{expected, found} => {
try!(writeln!(f,
"inconsistant leading whitespace: recipe started with \"{}\" but found line with \"{}\":",
show_whitespace(expected), show_whitespace(found)
));
}
ErrorKind::OuterShebang => {
try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes"))
}
ErrorKind::UnmatchedInterpolationDelimiter{recipe} => {
try!(writeln!(f, "recipe {} contains an unmatched {}", recipe, "{{"))
}
ErrorKind::BadInterpolationVariableName{recipe, text} => {
try!(writeln!(f, "recipe {} contains a bad variable interpolation: {}", recipe, text))
}
ErrorKind::UnknownDependency{recipe, unknown} => {
try!(writeln!(f, "recipe {} has unknown dependency {}", recipe, unknown));
}
ErrorKind::UnknownVariable{variable} => {
try!(writeln!(f, "variable \"{}\" is unknown", variable));
}
ErrorKind::UnknownStartOfToken => {
try!(writeln!(f, "unknown start of token:"));
}
ErrorKind::InternalError{ref message} => {
try!(writeln!(f, "internal error, this may indicate a bug in j: {}\n consider filing an issue: https://github.com/casey/j/issues/new", message));
}
}
match self.text.lines().nth(self.line) {
Some(line) => try!(write!(f, "{}", line)),
None => if self.index != self.text.len() {
try!(write!(f, "internal error: Error has invalid line number: {}", self.line))
},
};
Ok(())
}
}
struct Justfile<'a> {
recipes: BTreeMap<&'a str, Recipe<'a>>,
assignments: BTreeMap<&'a str, Expression<'a>>,
values: BTreeMap<&'a str, String>,
}
impl<'a> Justfile<'a> {
fn first(&self) -> Option<&'a str> {
let mut first: Option<&Recipe<'a>> = 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.map(|recipe| recipe.name)
}
fn count(&self) -> usize {
self.recipes.len()
}
fn recipes(&self) -> Vec<&'a str> {
self.recipes.keys().cloned().collect()
}
fn run_recipe(&self, recipe: &Recipe<'a>, ran: &mut HashSet<&'a str>) -> Result<(), RunError> {
for dependency_name in &recipe.dependencies {
if !ran.contains(dependency_name) {
try!(self.run_recipe(&self.recipes[dependency_name], ran));
}
}
try!(recipe.run());
ran.insert(recipe.name);
Ok(())
}
fn run<'b>(&'a self, names: &[&'b str]) -> Result<(), RunError<'b>>
where 'a: 'b
{
let mut missing = vec![];
for recipe in names {
if !self.recipes.contains_key(recipe) {
missing.push(*recipe);
}
}
if !missing.is_empty() {
return Err(RunError::UnknownRecipes{recipes: missing});
}
let recipes = names.iter().map(|name| self.recipes.get(name).unwrap()).collect::<Vec<_>>();
let mut ran = HashSet::new();
for recipe in recipes {
try!(self.run_recipe(recipe, &mut ran));
}
Ok(())
}
fn get(&self, name: &str) -> Option<&Recipe<'a>> {
self.recipes.get(name)
}
}
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 {
try!(write!(f, "{} = {} # \"{}\"", name, expression, self.values.get(name).unwrap()));
items -= 1;
if items != 0 {
try!(write!(f, "\n"));
}
}
for recipe in self.recipes.values() {
try!(write!(f, "{}", recipe));
items -= 1;
if items != 0 {
try!(write!(f, "\n"));
}
}
Ok(())
}
}
#[derive(Debug)]
enum RunError<'a> {
UnknownRecipes{recipes: Vec<&'a str>},
Signal{recipe: &'a str, signal: i32},
Code{recipe: &'a str, code: i32},
UnknownFailure{recipe: &'a str},
IoError{recipe: &'a str, io_error: io::Error},
TmpdirIoError{recipe: &'a str, io_error: io::Error},
}
impl<'a> Display for RunError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
RunError::UnknownRecipes{ref recipes} => {
if recipes.len() == 1 {
try!(write!(f, "Justfile does not contain recipe: {}", recipes[0]));
} else {
try!(write!(f, "Justfile does not contain recipes: {}", recipes.join(" ")));
};
},
RunError::Code{recipe, code} => {
try!(write!(f, "Recipe \"{}\" failed with code {}", recipe, code));
},
RunError::Signal{recipe, signal} => {
try!(write!(f, "Recipe \"{}\" wast terminated by signal {}", recipe, signal));
}
RunError::UnknownFailure{recipe} => {
try!(write!(f, "Recipe \"{}\" failed for an unknown reason", recipe));
},
RunError::IoError{recipe, ref io_error} => {
try!(match io_error.kind() {
io::ErrorKind::NotFound => write!(f, "Recipe \"{}\" could not be run because j could not find `sh` the command:\n{}", recipe, io_error),
io::ErrorKind::PermissionDenied => write!(f, "Recipe \"{}\" could not be run because j could not run `sh`:\n{}", recipe, io_error),
_ => write!(f, "Recipe \"{}\" could not be run because of an IO error while launching the `sh`:\n{}", recipe, io_error),
});
},
RunError::TmpdirIoError{recipe, ref io_error} =>
try!(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)),
}
Ok(())
}
}
#[derive(Debug, PartialEq)]
struct Token<'a> {
index: usize,
line: usize,
column: usize,
text: &'a str,
prefix: &'a str,
lexeme: &'a str,
class: TokenKind,
}
impl<'a> Token<'a> {
fn error(&self, kind: ErrorKind<'a>) -> Error<'a> {
Error {
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 {
Name,
Colon,
StringToken,
Plus,
Equals,
Comment,
Line,
Indent,
Dedent,
Eol,
Eof,
}
impl Display for TokenKind {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
try!(write!(f, "{}", match *self {
Name => "name",
Colon => "\":\"",
Plus => "\"+\"",
Equals => "\"=\"",
StringToken => "string",
Comment => "comment",
Line => "command",
Indent => "indent",
Dedent => "dedent",
Eol => "end of line",
Eof => "end of file",
}));
Ok(())
}
}
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<Vec<Token>, Error> {
lazy_static! {
static ref EOF: Regex = token(r"(?-m)$" );
static ref NAME: Regex = token(r"([a-zA-Z0-9_-]+)" );
static ref COLON: Regex = token(r":" );
static ref EQUALS: Regex = token(r"=" );
static ref PLUS: Regex = token(r"[+]" );
static ref COMMENT: Regex = token(r"#([^!].*)?$" );
static ref STRING: Regex = token("\"[a-z0-9]\"" );
static ref EOL: Regex = token(r"\n|\r\n" );
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" );
}
fn indentation(text: &str) -> Option<&str> {
INDENT.captures(text).map(|captures| captures.at(1).unwrap())
}
let mut tokens = vec![];
let mut rest = text;
let mut index = 0;
let mut line = 0;
let mut column = 0;
let mut indent: Option<&str> = None;
macro_rules! error {
($kind:expr) => {{
Err(Error {
text: text,
index: index,
line: line,
column: column,
width: None,
kind: $kind,
})
}};
}
loop {
if column == 0 {
if let Some(class) = match (indent, indentation(rest)) {
// ignore: was no indentation and there still isn't
// or current line is blank
(None, Some("")) | (_, None) => {
None
}
// indent: was no indentation, now there is
(None, Some(current)) => {
if mixed_whitespace(current) {
return error!(ErrorKind::MixedLeadingWhitespace{whitespace: current})
}
indent = Some(current);
Some(Indent)
}
// dedent: there was indentation and now there isn't
(Some(_), Some("")) => {
indent = None;
Some(Dedent)
}
// was indentation and still is, check if the new indentation matches
(Some(previous), Some(current)) => {
if !current.starts_with(previous) {
return error!(ErrorKind::InconsistentLeadingWhitespace{
expected: previous,
found: current
});
}
None
}
} {
tokens.push(Token {
index: index,
line: line,
column: column,
text: text,
prefix: "",
lexeme: "",
class: class,
});
}
}
// insert a dedent if we're indented and we hit the end of the file
if indent.is_some() && EOF.is_match(rest) {
tokens.push(Token {
index: index,
line: line,
column: column,
text: text,
prefix: "",
lexeme: "",
class: Dedent,
});
}
let (prefix, lexeme, class) =
if let (0, Some(indent), Some(captures)) = (column, indent, LINE.captures(rest)) {
let line = captures.at(0).unwrap();
if !line.starts_with(indent) {
return error!(ErrorKind::InternalError{message: "unexpected indent".to_string()});
}
let (prefix, lexeme) = line.split_at(indent.len());
(prefix, lexeme, Line)
} else if let Some(captures) = NAME.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Name)
} else if let Some(captures) = EOL.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Eol)
} else if let Some(captures) = EOF.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Eof)
} else if let Some(captures) = COLON.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Colon)
} else if let Some(captures) = PLUS.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Plus)
} else if let Some(captures) = EQUALS.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Equals)
} else if let Some(captures) = COMMENT.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), Comment)
} else if let Some(captures) = STRING.captures(rest) {
(captures.at(1).unwrap(), captures.at(2).unwrap(), StringToken)
} else if rest.starts_with("#!") {
return error!(ErrorKind::OuterShebang)
} else {
return error!(ErrorKind::UnknownStartOfToken)
};
let len = prefix.len() + lexeme.len();
tokens.push(Token {
index: index,
line: line,
column: column,
prefix: prefix,
text: text,
lexeme: lexeme,
class: class,
});
match tokens.last().unwrap().class {
Eol => {
line += 1;
column = 0;
},
Eof => {
break;
},
_ => {
column += len;
}
}
rest = &rest[len..];
index += len;
}
Ok(tokens)
}
fn parse(text: &str) -> Result<Justfile, Error> {
let tokens = try!(tokenize(text));
let filtered: Vec<_> = tokens.into_iter().filter(|token| token.class != Comment).collect();
if let Some(token) = filtered.iter().find(|token| {
lazy_static! {
static ref GOOD_NAME: Regex = re("^[a-z](-?[a-z0-9])*$");
}
token.class == Name && !GOOD_NAME.is_match(token.lexeme)
}) {
return Err(token.error(ErrorKind::BadName{name: token.lexeme}));
}
let parser = Parser{
text: text,
tokens: filtered.into_iter().peekable()
};
let justfile = try!(parser.file());
Ok(justfile)
}
struct Parser<'a> {
text: &'a str,
tokens: std::iter::Peekable<std::vec::IntoIter<Token<'a>>>
}
impl<'a> Parser<'a> {
fn peek(&mut self, class: TokenKind) -> bool {
self.tokens.peek().unwrap().class == class
}
fn accept(&mut self, class: TokenKind) -> Option<Token<'a>> {
if self.peek(class) {
self.tokens.next()
} else {
None
}
}
fn accepted(&mut self, class: TokenKind) -> bool {
self.accept(class).is_some()
}
fn expect(&mut self, class: TokenKind) -> Option<Token<'a>> {
if self.peek(class) {
self.tokens.next();
None
} else {
self.tokens.next()
}
}
fn expect_eol(&mut self) -> Option<Token<'a>> {
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]) -> Error<'a> {
found.error(ErrorKind::UnexpectedToken {
expected: expected.to_vec(),
found: found.class,
})
}
fn recipe(&mut self, name: &'a str, line_number: usize) -> Result<Recipe<'a>, Error<'a>> {
let mut arguments = vec![];
let mut argument_tokens = vec![];
while let Some(argument) = self.accept(Name) {
if arguments.contains(&argument.lexeme) {
return Err(argument.error(ErrorKind::DuplicateArgument{
recipe: name, argument: argument.lexeme
}));
}
arguments.push(argument.lexeme);
argument_tokens.push(argument);
}
if let Some(token) = self.expect(Colon) {
// if we haven't accepted any arguments, an equals
// would have been fine as part of an expression
if arguments.is_empty() {
return Err(self.unexpected_token(&token, &[Name, Colon, Equals]));
} else {
return Err(self.unexpected_token(&token, &[Name, 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(ErrorKind::DuplicateDependency {
recipe: name,
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![];
let mut line_tokens = vec![];
let mut shebang = false;
if self.accepted(Indent) {
while !self.peek(Dedent) {
if let Some(line) = self.accept(Line) {
if lines.is_empty() {
if line.lexeme.starts_with("#!") {
shebang = true;
}
} else if !shebang && (line.lexeme.starts_with(' ') || line.lexeme.starts_with('\t')) {
return Err(line.error(ErrorKind::ExtraLeadingWhitespace));
}
lines.push(line.lexeme);
line_tokens.push(line);
if !self.peek(Dedent) {
if let Some(token) = self.expect_eol() {
return Err(self.unexpected_token(&token, &[Eol]));
}
}
} else if let Some(_) = self.accept(Eol) {
} else {
let token = self.tokens.next().unwrap();
return Err(self.unexpected_token(&token, &[Line, Eol]));
}
}
if let Some(token) = self.expect(Dedent) {
return Err(self.unexpected_token(&token, &[Dedent]));
}
}
let mut fragments = vec![];
let mut variables = BTreeSet::new();
lazy_static! {
static ref FRAGMENT: Regex = re(r"^(.*?)\{\{(.*?)\}\}" );
static ref UNMATCHED: Regex = re(r"^.*?\{\{" );
static ref VARIABLE: Regex = re(r"^[ \t]*([a-z](-?[a-z0-9])*)[ \t]*$");
}
for line in &line_tokens {
let mut line_fragments = vec![];
let mut rest = line.lexeme;
while !rest.is_empty() {
if let Some(captures) = FRAGMENT.captures(rest) {
let prefix = captures.at(1).unwrap();
if !prefix.is_empty() {
line_fragments.push(Fragment::Text{text: prefix});
}
let interior = captures.at(2).unwrap();
if let Some(captures) = VARIABLE.captures(interior) {
let name = captures.at(1).unwrap();
line_fragments.push(Fragment::Variable{name: name});
variables.insert(name);
} else {
return Err(line.error(ErrorKind::BadInterpolationVariableName{
recipe: name,
text: interior,
}));
}
rest = &rest[captures.at(0).unwrap().len()..];
} else if UNMATCHED.is_match(rest) {
return Err(line.error(ErrorKind::UnmatchedInterpolationDelimiter{recipe: name}));
} else {
line_fragments.push(Fragment::Text{text: rest});
rest = "";
}
}
fragments.push(line_fragments);
}
Ok(Recipe {
line_number: line_number,
name: name,
dependencies: dependencies,
dependency_tokens: dependency_tokens,
arguments: arguments,
argument_tokens: argument_tokens,
fragments: fragments,
variables: variables,
lines: lines,
shebang: shebang,
})
}
fn expression(&mut self) -> Result<Expression<'a>, Error<'a>> {
let first = self.tokens.next().unwrap();
let lhs = match first.class {
Name => Expression::Variable{name: first.lexeme},
StringToken => Expression::String{contents: &first.lexeme[1..2]},
_ => return Err(self.unexpected_token(&first, &[Name, StringToken])),
};
if self.accepted(Plus) {
let rhs = try!(self.expression());
Ok(Expression::Concatination{lhs: Box::new(lhs), rhs: Box::new(rhs)})
} else if let Some(token) = self.expect_eol() {
Err(self.unexpected_token(&token, &[Plus, Eol]))
} else {
Ok(lhs)
}
}
fn file(mut self) -> Result<Justfile<'a>, Error<'a>> {
let mut recipes = BTreeMap::<&str, Recipe>::new();
let mut assignments = BTreeMap::<&str, Expression>::new();
let mut assignment_tokens = BTreeMap::<&str, Token<'a>>::new();
loop {
match self.tokens.next() {
Some(token) => match token.class {
Eof => break,
Eol => continue,
Name => if self.accepted(Equals) {
if assignments.contains_key(token.lexeme) {
return Err(token.error(ErrorKind::DuplicateVariable {
variable: token.lexeme,
}));
}
assignments.insert(token.lexeme, try!(self.expression()));
assignment_tokens.insert(token.lexeme, token);
} else {
if let Some(recipe) = recipes.remove(token.lexeme) {
return Err(token.error(ErrorKind::DuplicateRecipe {
recipe: recipe.name,
first: recipe.line_number
}));
}
recipes.insert(token.lexeme, try!(self.recipe(token.lexeme, token.line)));
},
Comment => return Err(token.error(ErrorKind::InternalError {
message: "found comment in token stream".to_string()
})),
_ => return Err(token.error(ErrorKind::InternalError {
message: format!("unhandled token class: {:?}", token.class)
})),
},
None => return Err(Error {
text: self.text,
index: 0,
line: 0,
column: 0,
width: None,
kind: ErrorKind::InternalError {
message: "unexpected end of token stream".to_string()
}
}),
}
}
if let Some(token) = self.tokens.next() {
return Err(token.error(ErrorKind::InternalError{
message: format!("unexpected token remaining after parsing completed: {:?}", token.class)
}))
}
try!(resolve(&recipes));
for recipe in recipes.values() {
for argument in &recipe.argument_tokens {
if assignments.contains_key(argument.lexeme) {
return Err(argument.error(ErrorKind::ArgumentShadowsVariable {
argument: argument.lexeme
}));
}
}
for variable in &recipe.variables {
if !(assignments.contains_key(variable) || recipe.arguments.contains(variable)) {
panic!("we fucked");
}
}
}
// variables have no associated tokens because fragment parsing
// is done in parsing
//
// options:
// . do it in parsing but generate tokens then
// . do it in lexing
// . generate error positions by hand
let values = try!(evaluate(&assignments, &assignment_tokens));
Ok(Justfile{
recipes: recipes,
assignments: assignments,
values: values,
})
}
}