Added proper errors
This commit is contained in:
parent
8ec000c159
commit
ec2b6c59f5
338
src/main.rs
338
src/main.rs
@ -2,8 +2,10 @@ extern crate regex;
|
|||||||
|
|
||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
|
|
||||||
use std::{io, fs, env};
|
use std::{io, fs, env, fmt};
|
||||||
use std::collections::{HashSet, BTreeMap};
|
use std::collections::{HashSet, BTreeMap};
|
||||||
|
use std::fmt::Display;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
macro_rules! warn {
|
macro_rules! warn {
|
||||||
($($arg:tt)*) => {{
|
($($arg:tt)*) => {{
|
||||||
@ -20,63 +22,239 @@ macro_rules! die {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn re(pattern: &str) -> regex::Regex {
|
trait Slurp {
|
||||||
regex::Regex::new(pattern).unwrap()
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Recipe<'a> {
|
struct Recipe<'a> {
|
||||||
_line: u64,
|
line: usize,
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
leading_whitespace: &'a str,
|
leading_whitespace: &'a str,
|
||||||
commands: Vec<&'a str>,
|
commands: Vec<&'a str>,
|
||||||
dependencies: HashSet<&'a str>,
|
dependencies: HashSet<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Resolver<'a> {
|
struct Error<'a> {
|
||||||
recipes: &'a BTreeMap<&'a str, Recipe<'a>>,
|
text: &'a str,
|
||||||
resolved: HashSet<&'a str>,
|
line: usize,
|
||||||
seen: HashSet<&'a str>,
|
kind: ErrorKind<'a>
|
||||||
stack: Vec<&'a str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve<'a> (recipes: &'a BTreeMap<&'a str, Recipe<'a>>) {
|
enum ErrorKind<'a> {
|
||||||
let mut resolver = Resolver {
|
CircularDependency{circle: Vec<&'a str>},
|
||||||
recipes: recipes,
|
DuplicateDependency{name: &'a str},
|
||||||
resolved: HashSet::new(),
|
DuplicateRecipe{first: usize, name: &'a str},
|
||||||
seen: HashSet::new(),
|
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
||||||
stack: vec![],
|
Shebang,
|
||||||
};
|
UnknownDependency{name: &'a str, unknown: &'a str},
|
||||||
|
Unparsable,
|
||||||
|
UnparsableDependencies,
|
||||||
|
}
|
||||||
|
|
||||||
for (_, recipe) in recipes {
|
fn error<'a>(text: &'a str, line: usize, kind: ErrorKind<'a>)
|
||||||
resolver.resolve(recipe);
|
-> Error<'a>
|
||||||
|
{
|
||||||
|
Error {
|
||||||
|
text: text,
|
||||||
|
line: line,
|
||||||
|
kind: kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Resolver<'a> {
|
fn show_whitespace(text: &str) -> String {
|
||||||
fn resolve(&mut self, recipe: &'a Recipe) {
|
text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect()
|
||||||
self.stack.push(recipe.name);
|
}
|
||||||
self.seen.insert(recipe.name);
|
|
||||||
for dependency_name in &recipe.dependencies {
|
impl<'a> Display for Error<'a> {
|
||||||
match self.recipes.get(dependency_name) {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
Some(dependency) => if !self.resolved.contains(dependency.name) {
|
try!(write!(f, "justfile:{}: ", self.line));
|
||||||
if self.seen.contains(dependency.name) {
|
|
||||||
let first = self.stack[0];
|
match self.kind {
|
||||||
self.stack.push(first);
|
ErrorKind::CircularDependency{ref circle} => {
|
||||||
die!("Circular dependency: {}",
|
try!(write!(f, "circular dependency: {}", circle.join(" -> ")));
|
||||||
self.stack.iter()
|
return Ok(());
|
||||||
.skip_while(|name| **name != dependency.name)
|
|
||||||
.cloned().collect::<Vec<&str>>().join(" -> "));
|
|
||||||
}
|
}
|
||||||
self.resolve(dependency);
|
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<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<&'a str, Recipe<'a>> = BTreeMap::new();
|
||||||
|
let mut current_recipe: Option<Recipe> = 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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
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.",
|
None => {
|
||||||
recipe.name, dependency_name),
|
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,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
self.resolved.insert(recipe.name);
|
|
||||||
self.stack.pop();
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
fn main() {
|
||||||
@ -100,91 +278,14 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut contents = String::new();
|
let text = fs::File::open("justfile")
|
||||||
|
|
||||||
fs::File::open("justfile")
|
|
||||||
.unwrap_or_else(|error| die!("Error opening justfile: {}", error))
|
.unwrap_or_else(|error| die!("Error opening justfile: {}", error))
|
||||||
.read_to_string(&mut contents)
|
.slurp()
|
||||||
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
||||||
|
|
||||||
let shebang_re = re(r"^\s*#!(.*)$");
|
let _justfile = parse(&text).unwrap_or_else(|error| die!("{}", error));
|
||||||
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<Recipe> = 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 requests: Vec<String> = std::env::args().skip(1).collect();
|
// let requests: Vec<String> = std::env::args().skip(1).collect();
|
||||||
// for request in requests {
|
// for request in requests {
|
||||||
// println!("{}", request);
|
// println!("{}", request);
|
||||||
@ -196,7 +297,6 @@ fn main() {
|
|||||||
// std::env::set_var(format!("ARG{}", i), argument);
|
// std::env::set_var(format!("ARG{}", i), argument);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/*
|
|
||||||
let mut command = std::process::Command::new(make.command());
|
let mut command = std::process::Command::new(make.command());
|
||||||
|
|
||||||
command.arg("MAKEFLAGS=");
|
command.arg("MAKEFLAGS=");
|
||||||
|
Loading…
Reference in New Issue
Block a user