diff --git a/src/main.rs b/src/main.rs index 1851f55..bd92ff5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,10 @@ extern crate regex; use std::io::prelude::*; -use std::{io, fs, env}; +use std::{io, fs, env, fmt}; use std::collections::{HashSet, BTreeMap}; +use std::fmt::Display; +use regex::Regex; macro_rules! warn { ($($arg:tt)*) => {{ @@ -20,63 +22,239 @@ macro_rules! die { }}; } -fn re(pattern: &str) -> regex::Regex { - regex::Regex::new(pattern).unwrap() +trait Slurp { + fn slurp(&mut self) -> Result; +} + +impl Slurp for fs::File { + fn slurp(&mut self) -> Result { + let mut destination = String::new(); + try!(self.read_to_string(&mut destination)); + Ok(destination) + } +} + +fn re(pattern: &str) -> Regex { + Regex::new(pattern).unwrap() } struct Recipe<'a> { - _line: u64, + line: usize, name: &'a str, leading_whitespace: &'a str, commands: Vec<&'a str>, dependencies: HashSet<&'a str>, } -struct Resolver<'a> { - recipes: &'a BTreeMap<&'a str, Recipe<'a>>, - resolved: HashSet<&'a str>, - seen: HashSet<&'a str>, - stack: Vec<&'a str>, +struct Error<'a> { + text: &'a str, + line: usize, + kind: ErrorKind<'a> } -fn resolve<'a> (recipes: &'a BTreeMap<&'a str, Recipe<'a>>) { - let mut resolver = Resolver { - recipes: recipes, - resolved: HashSet::new(), - seen: HashSet::new(), - stack: vec![], - }; +enum ErrorKind<'a> { + CircularDependency{circle: Vec<&'a str>}, + DuplicateDependency{name: &'a str}, + DuplicateRecipe{first: usize, name: &'a str}, + InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, + Shebang, + UnknownDependency{name: &'a str, unknown: &'a str}, + Unparsable, + UnparsableDependencies, +} - for (_, recipe) in recipes { - resolver.resolve(recipe); +fn error<'a>(text: &'a str, line: usize, kind: ErrorKind<'a>) + -> Error<'a> +{ + Error { + text: text, + line: line, + kind: kind, } } -impl<'a> Resolver<'a> { - fn resolve(&mut self, recipe: &'a Recipe) { - self.stack.push(recipe.name); - self.seen.insert(recipe.name); - for dependency_name in &recipe.dependencies { - match self.recipes.get(dependency_name) { - Some(dependency) => if !self.resolved.contains(dependency.name) { - if self.seen.contains(dependency.name) { - let first = self.stack[0]; - self.stack.push(first); - die!("Circular dependency: {}", - self.stack.iter() - .skip_while(|name| **name != dependency.name) - .cloned().collect::>().join(" -> ")); +fn show_whitespace(text: &str) -> String { + text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect() +} + +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::CircularDependency{ref circle} => { + try!(write!(f, "circular dependency: {}", circle.join(" -> "))); + return Ok(()); + } + ErrorKind::DuplicateDependency{name} => { + try!(writeln!(f, "duplicate dependency: {}", name)); + } + ErrorKind::DuplicateRecipe{first, name} => { + try!(write!(f, "duplicate recipe: {} appears on lines {} and {}", + name, first, self.line)); + return Ok(()); + } + ErrorKind::InconsistentLeadingWhitespace{expected, found} => { + try!(writeln!(f, + "inconsistant leading whitespace: recipe started with {} but found line with {}:", + show_whitespace(expected), show_whitespace(found) + )); + } + ErrorKind::Shebang => { + try!(writeln!(f, "shebang \"#!\" is reserved syntax")) + } + ErrorKind::UnknownDependency{name, unknown} => { + try!(writeln!(f, "recipe {} has unknown dependency {}", name, unknown)); + } + ErrorKind::Unparsable => { + try!(writeln!(f, "could not parse line:")); + } + ErrorKind::UnparsableDependencies => { + try!(writeln!(f, "could not parse dependencies:")); + } + } + + match self.text.lines().nth(self.line) { + Some(line) => try!(write!(f, "{}", line)), + None => die!("internal error: Error has invalid line number: {}", self.line), + } + + Ok(()) + } +} + +struct Justfile<'a> { + _recipes: BTreeMap<&'a str, Recipe<'a>> +} + +fn parse<'a>(text: &'a str) -> Result { + let shebang_re = re(r"^\s*#!(.*)$"); + let comment_re = re(r"^\s*#[^!].*$"); + let command_re = re(r"^(\s+)(.*)$"); + let blank_re = re(r"^\s*$"); + let label_re = re(r"^([a-z](-[a-z]|[a-z])*):(.*)$"); + let name_re = re(r"^[a-z](-[a-z]|[a-z])*$"); + let whitespace_re = re(r"\s+"); + + let mut recipes: BTreeMap<&'a str, Recipe<'a>> = BTreeMap::new(); + let mut current_recipe: Option = None; + for (i, line) in text.lines().enumerate() { + if blank_re.is_match(line) { + continue; + } else if shebang_re.is_match(line) { + return Err(error(text, i, ErrorKind::Shebang)); + } + + if let Some(mut recipe) = current_recipe { + match command_re.captures(line) { + Some(captures) => { + let leading_whitespace = captures.at(1).unwrap(); + if recipe.leading_whitespace == "" { + recipe.leading_whitespace = leading_whitespace; + } else if !line.starts_with(recipe.leading_whitespace) { + return Err(error(text, i, ErrorKind::InconsistentLeadingWhitespace{ + expected: recipe.leading_whitespace, + found: leading_whitespace, + })); } - self.resolve(dependency); + let command = captures.at(2).unwrap(); + recipe.commands.push(command); + current_recipe = Some(recipe); + continue; }, - None => die!("Recipe \"{}\" depends on recipe \"{}\", which doesn't exist.", - recipe.name, dependency_name), + None => { + recipes.insert(recipe.name, recipe); + current_recipe = None; + }, + } + } + + if comment_re.is_match(line) { + // ignore + } else if let Some(captures) = label_re.captures(line) { + let name = captures.at(1).unwrap(); + if let Some(recipe) = recipes.get(name) { + return Err(error(text, i, ErrorKind::DuplicateRecipe { + first: recipe.line, + name: name, + })); } + let rest = captures.at(3).unwrap().trim(); + let mut dependencies = HashSet::new(); + for part in whitespace_re.split(rest) { + if name_re.is_match(part) { + if dependencies.contains(part) { + return Err(error(text, i, ErrorKind::DuplicateDependency{ + name: part, + })); + } + dependencies.insert(part); + } else { + return Err(error(text, i, ErrorKind::UnparsableDependencies)); + } + } + + current_recipe = Some(Recipe{ + line: i, + name: name, + leading_whitespace: "", + commands: vec![], + dependencies: dependencies, + }); + } else { + return Err(error(text, i, ErrorKind::Unparsable)); } - self.resolved.insert(recipe.name); - self.stack.pop(); } + + if let Some(recipe) = current_recipe { + recipes.insert(recipe.name, recipe); + } + + let mut resolved = HashSet::new(); + let mut seen = HashSet::new(); + let mut stack = vec![]; + + fn resolve<'a>( + text: &'a str, + recipes: &BTreeMap<&str, Recipe<'a>>, + resolved: &mut HashSet<&'a str>, + seen: &mut HashSet<&'a str>, + stack: &mut Vec<&'a str>, + recipe: &Recipe<'a>, + ) -> Result<(), Error<'a>> { + stack.push(recipe.name); + seen.insert(recipe.name); + for dependency_name in &recipe.dependencies { + match recipes.get(dependency_name) { + Some(dependency) => if !resolved.contains(dependency.name) { + if seen.contains(dependency.name) { + let first = stack[0]; + stack.push(first); + return Err(error(text, recipe.line, ErrorKind::CircularDependency { + circle: stack.iter() + .skip_while(|name| **name != dependency.name) + .cloned().collect() + })); + } + return resolve(text, recipes, resolved, seen, stack, dependency); + }, + None => return Err(error(text, recipe.line, ErrorKind::UnknownDependency { + name: recipe.name, + unknown: dependency_name + })), + } + } + resolved.insert(recipe.name); + stack.pop(); + Ok(()) + } + + for (_, ref recipe) in &recipes { + try!(resolve(text, &recipes, &mut resolved, &mut seen, &mut stack, &recipe)); + } + + Ok(Justfile{_recipes: recipes}) } fn main() { @@ -100,91 +278,14 @@ fn main() { } } - let mut contents = String::new(); - - fs::File::open("justfile") + let text = fs::File::open("justfile") .unwrap_or_else(|error| die!("Error opening justfile: {}", error)) - .read_to_string(&mut contents) + .slurp() .unwrap_or_else(|error| die!("Error reading justfile: {}", error)); - let shebang_re = re(r"^\s*#!(.*)$"); - let comment_re = re(r"^\s*#[^!].*$"); - let command_re = re(r"^(\s+)(.*)$"); - let blank_re = re(r"^\s*$"); - let label_re = re(r"^([a-z](-[a-z]|[a-z])*):(.*)$"); - let name_re = re(r"^[a-z](-[a-z]|[a-z])*$"); - let whitespace_re = re(r"\s+"); - - let mut recipes = BTreeMap::new(); - let mut current_recipe: Option = None; - for (i, line) in contents.lines().enumerate() { - if blank_re.is_match(line) { - continue; - } else if shebang_re.is_match(line) { - die!("Unexpected shebang on line {}: {}", i, line); - } - - if let Some(mut recipe) = current_recipe { - match command_re.captures(line) { - Some(captures) => { - let leading_whitespace = captures.at(1).unwrap(); - if recipe.leading_whitespace == "" { - recipe.leading_whitespace = leading_whitespace; - } else if leading_whitespace != recipe.leading_whitespace { - die!("Command on line {} has inconsistent leading whitespace: {}", - i, line); - } - let command = captures.at(2).unwrap(); - recipe.commands.push(command); - current_recipe = Some(recipe); - continue; - }, - None => { - recipes.insert(recipe.name, recipe); - current_recipe = None; - }, - } - } - - if comment_re.is_match(line) { - // ignore - } else if let Some(captures) = label_re.captures(line) { - let name = captures.at(1).unwrap(); - let rest = captures.at(3).unwrap().trim(); - let mut dependencies = HashSet::new(); - for part in whitespace_re.split(rest) { - if name_re.is_match(part) { - if dependencies.contains(part) { - die!("Duplicate dependency \"{}\" on line {}", part, i); - } - dependencies.insert(part); - } else { - die!("Bad label on line {}: {}", i, line); - } - } - - if recipes.contains_key(name) { - die!("Duplicate recipe name \"{}\" on line {}.", name, i); - } - - current_recipe = Some(Recipe{ - _line: i as u64, - name: name, - leading_whitespace: "", - commands: vec![], - dependencies: dependencies, - }); - } else { - die!("Error parsing line {} of justfile: {}", i, line); - } - } - - if let Some(recipe) = current_recipe { - recipes.insert(recipe.name, recipe); - } - - resolve(&recipes); + let _justfile = parse(&text).unwrap_or_else(|error| die!("{}", error)); + /* // let requests: Vec = std::env::args().skip(1).collect(); // for request in requests { // println!("{}", request); @@ -196,7 +297,6 @@ fn main() { // std::env::set_var(format!("ARG{}", i), argument); // } - /* let mut command = std::process::Command::new(make.command()); command.arg("MAKEFLAGS=");