Reached feature parity with new parser
This commit is contained in:
parent
913bcba5f7
commit
acc97a4eba
100
src/app.rs
Normal file
100
src/app.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
extern crate clap;
|
||||||
|
|
||||||
|
use std::{io, fs, env, process};
|
||||||
|
use self::clap::{App, Arg};
|
||||||
|
use super::Slurp;
|
||||||
|
|
||||||
|
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)*);
|
||||||
|
process::exit(-1)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn app() {
|
||||||
|
let matches = App::new("j")
|
||||||
|
.version("0.2.0")
|
||||||
|
.author("Casey R. <casey@rodarmor.com>")
|
||||||
|
.about("Just a command runner")
|
||||||
|
.arg(Arg::with_name("list")
|
||||||
|
.short("l")
|
||||||
|
.long("list")
|
||||||
|
.help("Lists available recipes"))
|
||||||
|
.arg(Arg::with_name("show")
|
||||||
|
.short("s")
|
||||||
|
.long("show")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("Show information about a recipe"))
|
||||||
|
.arg(Arg::with_name("recipe")
|
||||||
|
.multiple(true)
|
||||||
|
.help("recipe(s) to run, defaults to the first recipe in the justfile"))
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match fs::metadata("justfile") {
|
||||||
|
Ok(metadata) => if metadata.is_file() { break; },
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() != io::ErrorKind::NotFound {
|
||||||
|
die!("Error fetching justfile metadata: {}", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match env::current_dir() {
|
||||||
|
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); },
|
||||||
|
Err(error) => die!("Error getting current dir: {}", error),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(error) = env::set_current_dir("..") {
|
||||||
|
die!("Error changing directory: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = fs::File::open("justfile")
|
||||||
|
.unwrap_or_else(|error| die!("Error opening justfile: {}", error))
|
||||||
|
.slurp()
|
||||||
|
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
||||||
|
|
||||||
|
let justfile = super::parse(&text).unwrap_or_else(|error| die!("{}", error));
|
||||||
|
|
||||||
|
if matches.is_present("list") {
|
||||||
|
if justfile.count() == 0 {
|
||||||
|
warn!("Justfile contains no recipes");
|
||||||
|
} else {
|
||||||
|
warn!("{}", justfile.recipes().join(" "));
|
||||||
|
}
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = matches.value_of("show") {
|
||||||
|
match justfile.get(name) {
|
||||||
|
Some(recipe) => {
|
||||||
|
warn!("{}", recipe);
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
None => die!("justfile contains no recipe \"{}\"", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let names = if let Some(names) = matches.values_of("recipe") {
|
||||||
|
names.collect::<Vec<_>>()
|
||||||
|
} else if let Some(name) = justfile.first() {
|
||||||
|
vec![name]
|
||||||
|
} else {
|
||||||
|
die!("Justfile contains no recipes");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(run_error) = justfile.run(&names) {
|
||||||
|
warn!("{}", run_error);
|
||||||
|
//process::exit(if let super::RunError::Code{code, ..} = run_error { code } else { -1 });
|
||||||
|
process::exit(-1);
|
||||||
|
}
|
||||||
|
}
|
506
src/lib.rs
506
src/lib.rs
@ -1,6 +1,10 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
|
||||||
|
pub use app::app;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
@ -30,7 +34,7 @@ macro_rules! die {
|
|||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Slurp {
|
trait Slurp {
|
||||||
fn slurp(&mut self) -> Result<String, std::io::Error>;
|
fn slurp(&mut self) -> Result<String, std::io::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,16 +50,17 @@ fn re(pattern: &str) -> Regex {
|
|||||||
Regex::new(pattern).unwrap()
|
Regex::new(pattern).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Recipe<'a> {
|
#[derive(PartialEq, Debug)]
|
||||||
|
struct Recipe<'a> {
|
||||||
line_number: usize,
|
line_number: usize,
|
||||||
label: &'a str,
|
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
// leading_whitespace: &'a str,
|
|
||||||
lines: Vec<&'a str>,
|
lines: Vec<&'a str>,
|
||||||
// fragments: Vec<Vec<Fragment<'a>>>,
|
// fragments: Vec<Vec<Fragment<'a>>>,
|
||||||
// variables: BTreeSet<&'a str>,
|
// variables: BTreeSet<&'a str>,
|
||||||
dependencies: Vec<&'a str>,
|
dependencies: Vec<&'a str>,
|
||||||
// arguments: Vec<&'a str>,
|
dependency_tokens: Vec<Token<'a>>,
|
||||||
|
arguments: Vec<&'a str>,
|
||||||
|
argument_tokens: Vec<Token<'a>>,
|
||||||
shebang: bool,
|
shebang: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,20 +71,6 @@ enum Fragment<'a> {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
impl<'a> Display for Recipe<'a> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
|
||||||
try!(writeln!(f, "{}", self.label));
|
|
||||||
for (i, line) in self.lines.iter().enumerate() {
|
|
||||||
if i + 1 < self.lines.len() {
|
|
||||||
try!(writeln!(f, " {}", line));
|
|
||||||
} {
|
|
||||||
try!(write!(f, " {}", line));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn error_from_signal<'a>(recipe: &'a str, exit_status: process::ExitStatus) -> RunError<'a> {
|
fn error_from_signal<'a>(recipe: &'a str, exit_status: process::ExitStatus) -> RunError<'a> {
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
@ -183,9 +174,30 @@ impl<'a> Recipe<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
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, line) in self.lines.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
try!(writeln!(f, ""));
|
||||||
|
}
|
||||||
|
try!(write!(f, " {}", line));
|
||||||
|
if i + 1 < self.lines.len() {
|
||||||
|
try!(writeln!(f, ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve<'a>(
|
fn resolve<'a>(
|
||||||
text: &'a str,
|
|
||||||
recipes: &BTreeMap<&str, Recipe<'a>>,
|
recipes: &BTreeMap<&str, Recipe<'a>>,
|
||||||
resolved: &mut HashSet<&'a str>,
|
resolved: &mut HashSet<&'a str>,
|
||||||
seen: &mut HashSet<&'a str>,
|
seen: &mut HashSet<&'a str>,
|
||||||
@ -197,23 +209,24 @@ fn resolve<'a>(
|
|||||||
}
|
}
|
||||||
stack.push(recipe.name);
|
stack.push(recipe.name);
|
||||||
seen.insert(recipe.name);
|
seen.insert(recipe.name);
|
||||||
for dependency_name in &recipe.dependencies {
|
for dependency_token in &recipe.dependency_tokens {
|
||||||
match recipes.get(dependency_name) {
|
match recipes.get(dependency_token.lexeme) {
|
||||||
Some(dependency) => if !resolved.contains(dependency.name) {
|
Some(dependency) => if !resolved.contains(dependency.name) {
|
||||||
if seen.contains(dependency.name) {
|
if seen.contains(dependency.name) {
|
||||||
let first = stack[0];
|
let first = stack[0];
|
||||||
stack.push(first);
|
stack.push(first);
|
||||||
return Err(error(text, recipe.line_number, ErrorKind::CircularDependency {
|
return Err(dependency_token.error(ErrorKind::CircularDependency {
|
||||||
|
recipe: recipe.name,
|
||||||
circle: stack.iter()
|
circle: stack.iter()
|
||||||
.skip_while(|name| **name != dependency.name)
|
.skip_while(|name| **name != dependency.name)
|
||||||
.cloned().collect()
|
.cloned().collect()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return resolve(text, recipes, resolved, seen, stack, dependency);
|
return resolve(recipes, resolved, seen, stack, dependency);
|
||||||
},
|
},
|
||||||
None => return Err(error(text, recipe.line_number, ErrorKind::UnknownDependency {
|
None => return Err(dependency_token.error(ErrorKind::UnknownDependency {
|
||||||
name: recipe.name,
|
recipe: recipe.name,
|
||||||
unknown: dependency_name
|
unknown: dependency_token.lexeme
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,108 +234,103 @@ fn resolve<'a>(
|
|||||||
stack.pop();
|
stack.pop();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub struct Error<'a> {
|
struct Error<'a> {
|
||||||
text: &'a str,
|
text: &'a str,
|
||||||
index: usize,
|
index: usize,
|
||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
|
width: Option<usize>,
|
||||||
kind: ErrorKind<'a>,
|
kind: ErrorKind<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum ErrorKind<'a> {
|
enum ErrorKind<'a> {
|
||||||
// BadRecipeName{name: &'a str},
|
BadName{name: &'a str},
|
||||||
// CircularDependency{circle: Vec<&'a str>},
|
CircularDependency{recipe: &'a str, circle: Vec<&'a str>},
|
||||||
// DuplicateDependency{name: &'a str},
|
DuplicateDependency{recipe: &'a str, dependency: &'a str},
|
||||||
// DuplicateArgument{recipe: &'a str, argument: &'a str},
|
DuplicateArgument{recipe: &'a str, argument: &'a str},
|
||||||
// DuplicateRecipe{first: usize, name: &'a str},
|
DuplicateRecipe{recipe: &'a str, first: usize},
|
||||||
// TabAfterSpace{whitespace: &'a str},
|
MixedLeadingWhitespace{whitespace: &'a str},
|
||||||
// MixedLeadingWhitespace{whitespace: &'a str},
|
ExtraLeadingWhitespace,
|
||||||
// ExtraLeadingWhitespace,
|
|
||||||
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
InconsistentLeadingWhitespace{expected: &'a str, found: &'a str},
|
||||||
OuterShebang,
|
OuterShebang,
|
||||||
// NonLeadingShebang{recipe: &'a str},
|
AssignmentUnimplemented,
|
||||||
// UnknownDependency{name: &'a str, unknown: &'a str},
|
UnknownDependency{recipe: &'a str, unknown: &'a str},
|
||||||
// Unparsable,
|
|
||||||
// UnparsableDependencies,
|
|
||||||
UnknownStartOfToken,
|
UnknownStartOfToken,
|
||||||
|
UnexpectedToken{expected: Vec<TokenClass>, found: TokenClass},
|
||||||
InternalError{message: String},
|
InternalError{message: String},
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn error<'a>(text: &'a str, line: usize, kind: ErrorKind<'a>)
|
|
||||||
// -> Error<'a>
|
|
||||||
// {
|
|
||||||
// Error {
|
|
||||||
// text: text,
|
|
||||||
// line: line,
|
|
||||||
// kind: kind,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn show_whitespace(text: &str) -> String {
|
fn show_whitespace(text: &str) -> String {
|
||||||
text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect()
|
text.chars().map(|c| match c { '\t' => 't', ' ' => 's', _ => c }).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
fn mixed_whitespace(text: &str) -> bool {
|
||||||
fn mixed(text: &str) -> bool {
|
|
||||||
!(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t'))
|
!(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t'))
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
struct Or<'a, T: 'a + Display>(&'a [T]);
|
||||||
fn tab_after_space(text: &str) -> bool {
|
|
||||||
let mut space = false;
|
impl<'a, T: Display> Display for Or<'a, T> {
|
||||||
for c in text.chars() {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
match c {
|
match self.0.len() {
|
||||||
' ' => space = true,
|
0 => {},
|
||||||
'\t' => if space {
|
1 => try!(write!(f, "{}", self.0[0])),
|
||||||
return true;
|
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(())
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
impl<'a> Display for Error<'a> {
|
impl<'a> Display for Error<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
try!(write!(f, "justfile:{}: ", self.line));
|
try!(write!(f, "justfile:{}: ", self.line));
|
||||||
|
|
||||||
match self.kind {
|
match self.kind {
|
||||||
// ErrorKind::BadRecipeName{name} => {
|
ErrorKind::BadName{name} => {
|
||||||
// try!(writeln!(f, "recipe name does not match /[a-z](-[a-z]|[a-z])*/: {}", name));
|
try!(writeln!(f, "name did not match /[a-z](-?[a-z0-9])*/: {}", name));
|
||||||
// }
|
}
|
||||||
// ErrorKind::CircularDependency{ref circle} => {
|
ErrorKind::CircularDependency{recipe, ref circle} => {
|
||||||
// try!(write!(f, "circular dependency: {}", circle.join(" -> ")));
|
try!(write!(f, "recipe {} has circular dependency: {}", recipe, circle.join(" -> ")));
|
||||||
// return Ok(());
|
return Ok(());
|
||||||
// }
|
}
|
||||||
// ErrorKind::DuplicateArgument{recipe, argument} => {
|
ErrorKind::DuplicateArgument{recipe, argument} => {
|
||||||
// try!(writeln!(f, "recipe {} has duplicate argument: {}", recipe, argument));
|
try!(writeln!(f, "recipe {} has duplicate argument: {}", recipe, argument));
|
||||||
//}
|
}
|
||||||
// ErrorKind::DuplicateDependency{name} => {
|
ErrorKind::UnexpectedToken{ref expected, found} => {
|
||||||
// try!(writeln!(f, "duplicate dependency: {}", name));
|
try!(writeln!(f, "expected {} but found {}", Or(expected), found));
|
||||||
// }
|
}
|
||||||
// ErrorKind::DuplicateRecipe{first, name} => {
|
ErrorKind::DuplicateDependency{recipe, dependency} => {
|
||||||
// try!(write!(f, "duplicate recipe: {} appears on lines {} and {}",
|
try!(writeln!(f, "recipe {} has duplicate dependency: {}", recipe, dependency));
|
||||||
// name, first, self.line));
|
}
|
||||||
// return Ok(());
|
ErrorKind::DuplicateRecipe{recipe, first} => {
|
||||||
// }
|
try!(write!(f, "duplicate recipe: {} appears on lines {} and {}",
|
||||||
// ErrorKind::TabAfterSpace{whitespace} => {
|
recipe, first, self.line));
|
||||||
// try!(writeln!(f, "found tab after space: {}", show_whitespace(whitespace)));
|
return Ok(());
|
||||||
// }
|
}
|
||||||
// ErrorKind::MixedLeadingWhitespace{whitespace} => {
|
ErrorKind::MixedLeadingWhitespace{whitespace} => {
|
||||||
// try!(writeln!(f,
|
try!(writeln!(f,
|
||||||
// "inconsistant leading whitespace: recipe started with {}:",
|
"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)
|
show_whitespace(whitespace)
|
||||||
// ));
|
));
|
||||||
// }
|
}
|
||||||
// ErrorKind::ExtraLeadingWhitespace => {
|
ErrorKind::ExtraLeadingWhitespace => {
|
||||||
// try!(writeln!(f, "line has extra leading whitespace"));
|
try!(writeln!(f, "recipe line has extra leading whitespace"));
|
||||||
// }
|
}
|
||||||
|
ErrorKind::AssignmentUnimplemented => {
|
||||||
|
try!(writeln!(f, "variable assignment is not yet implemented"));
|
||||||
|
}
|
||||||
ErrorKind::InconsistentLeadingWhitespace{expected, found} => {
|
ErrorKind::InconsistentLeadingWhitespace{expected, found} => {
|
||||||
try!(writeln!(f,
|
try!(writeln!(f,
|
||||||
"inconsistant leading whitespace: recipe started with \"{}\" but found line with \"{}\":",
|
"inconsistant leading whitespace: recipe started with \"{}\" but found line with \"{}\":",
|
||||||
@ -332,18 +340,9 @@ impl<'a> Display for Error<'a> {
|
|||||||
ErrorKind::OuterShebang => {
|
ErrorKind::OuterShebang => {
|
||||||
try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes"))
|
try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes"))
|
||||||
}
|
}
|
||||||
// ErrorKind::NonLeadingShebang{..} => {
|
ErrorKind::UnknownDependency{recipe, unknown} => {
|
||||||
// try!(writeln!(f, "a shebang \"#!\" may only appear on the first line of a recipe"))
|
try!(writeln!(f, "recipe {} has unknown dependency {}", recipe, unknown));
|
||||||
//}
|
}
|
||||||
// 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:"));
|
|
||||||
// }
|
|
||||||
ErrorKind::UnknownStartOfToken => {
|
ErrorKind::UnknownStartOfToken => {
|
||||||
try!(writeln!(f, "uknown start of token:"));
|
try!(writeln!(f, "uknown start of token:"));
|
||||||
}
|
}
|
||||||
@ -354,19 +353,21 @@ impl<'a> Display for Error<'a> {
|
|||||||
|
|
||||||
match self.text.lines().nth(self.line) {
|
match self.text.lines().nth(self.line) {
|
||||||
Some(line) => try!(write!(f, "{}", line)),
|
Some(line) => try!(write!(f, "{}", line)),
|
||||||
None => try!(write!(f, "internal error: Error has invalid line number: {}", self.line)),
|
None => if self.index != self.text.len() {
|
||||||
|
try!(write!(f, "internal error: Error has invalid line number: {}", self.line))
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Justfile<'a> {
|
struct Justfile<'a> {
|
||||||
recipes: BTreeMap<&'a str, Recipe<'a>>,
|
recipes: BTreeMap<&'a str, Recipe<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Justfile<'a> {
|
impl<'a> Justfile<'a> {
|
||||||
pub fn first(&self) -> Option<&'a str> {
|
fn first(&self) -> Option<&'a str> {
|
||||||
let mut first: Option<&Recipe<'a>> = None;
|
let mut first: Option<&Recipe<'a>> = None;
|
||||||
for (_, recipe) in self.recipes.iter() {
|
for (_, recipe) in self.recipes.iter() {
|
||||||
if let Some(first_recipe) = first {
|
if let Some(first_recipe) = first {
|
||||||
@ -380,11 +381,11 @@ impl<'a> Justfile<'a> {
|
|||||||
first.map(|recipe| recipe.name)
|
first.map(|recipe| recipe.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(&self) -> usize {
|
fn count(&self) -> usize {
|
||||||
self.recipes.len()
|
self.recipes.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recipes(&self) -> Vec<&'a str> {
|
fn recipes(&self) -> Vec<&'a str> {
|
||||||
self.recipes.keys().cloned().collect()
|
self.recipes.keys().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +400,7 @@ impl<'a> Justfile<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run<'b>(&'a self, names: &[&'b str]) -> Result<(), RunError<'b>>
|
fn run<'b>(&'a self, names: &[&'b str]) -> Result<(), RunError<'b>>
|
||||||
where 'a: 'b
|
where 'a: 'b
|
||||||
{
|
{
|
||||||
let mut missing = vec![];
|
let mut missing = vec![];
|
||||||
@ -419,13 +420,13 @@ impl<'a> Justfile<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, name: &str) -> Option<&Recipe<'a>> {
|
fn get(&self, name: &str) -> Option<&Recipe<'a>> {
|
||||||
self.recipes.get(name)
|
self.recipes.get(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RunError<'a> {
|
enum RunError<'a> {
|
||||||
UnknownRecipes{recipes: Vec<&'a str>},
|
UnknownRecipes{recipes: Vec<&'a str>},
|
||||||
Signal{recipe: &'a str, signal: i32},
|
Signal{recipe: &'a str, signal: i32},
|
||||||
Code{recipe: &'a str, code: i32},
|
Code{recipe: &'a str, code: i32},
|
||||||
@ -467,22 +468,25 @@ impl<'a> Display for RunError<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
struct Token<'a> {
|
struct Token<'a> {
|
||||||
index: usize,
|
index: usize,
|
||||||
line: usize,
|
line: usize,
|
||||||
column: usize,
|
column: usize,
|
||||||
|
text: &'a str,
|
||||||
prefix: &'a str,
|
prefix: &'a str,
|
||||||
lexeme: &'a str,
|
lexeme: &'a str,
|
||||||
class: TokenClass,
|
class: TokenClass,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Token<'a> {
|
impl<'a> Token<'a> {
|
||||||
fn error(&self, text: &'a str, kind: ErrorKind<'a>) -> Error<'a> {
|
fn error(&self, kind: ErrorKind<'a>) -> Error<'a> {
|
||||||
Error {
|
Error {
|
||||||
text: text,
|
text: self.text,
|
||||||
index: self.index,
|
index: self.index + self.prefix.len(),
|
||||||
line: self.line,
|
line: self.line,
|
||||||
column: self.column,
|
column: self.column + self.prefix.len(),
|
||||||
|
width: Some(self.lexeme.len()),
|
||||||
kind: kind,
|
kind: kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -501,6 +505,23 @@ enum TokenClass {
|
|||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for TokenClass {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
try!(write!(f, "{}", match *self {
|
||||||
|
Name => "name",
|
||||||
|
Colon => "\":\"",
|
||||||
|
Equals => "\"=\"",
|
||||||
|
Comment => "comment",
|
||||||
|
Line => "command",
|
||||||
|
Indent => "indent",
|
||||||
|
Dedent => "dedent",
|
||||||
|
Eol => "end of line",
|
||||||
|
Eof => "end of file",
|
||||||
|
}));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use TokenClass::*;
|
use TokenClass::*;
|
||||||
|
|
||||||
fn token(pattern: &str) -> Regex {
|
fn token(pattern: &str) -> Regex {
|
||||||
@ -513,14 +534,14 @@ fn token(pattern: &str) -> Regex {
|
|||||||
|
|
||||||
fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref EOF: Regex = token(r"(?-m)$" );
|
static ref EOF: Regex = token(r"(?-m)$" );
|
||||||
static ref NAME: Regex = token(r"[a-z]((_|-)?[a-z0-9])*");
|
static ref NAME: Regex = token(r"([a-zA-Z0-9_-]+)" );
|
||||||
static ref COLON: Regex = token(r":" );
|
static ref COLON: Regex = token(r":" );
|
||||||
static ref EQUALS: Regex = token(r"=" );
|
static ref EQUALS: Regex = token(r"=" );
|
||||||
static ref COMMENT: Regex = token(r"#([^!].*)?$" );
|
static ref COMMENT: Regex = token(r"#([^!].*)?$" );
|
||||||
static ref EOL: Regex = token(r"\n|\r\n" );
|
static ref EOL: Regex = token(r"\n|\r\n" );
|
||||||
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
|
static ref LINE: Regex = re(r"^(?m)[ \t]+[^ \t\n\r].*$");
|
||||||
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" );
|
static ref INDENT: Regex = re(r"^([ \t]*)[^ \t\n\r]" );
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indentation(text: &str) -> Option<&str> {
|
fn indentation(text: &str) -> Option<&str> {
|
||||||
@ -541,6 +562,7 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
index: index,
|
index: index,
|
||||||
line: line,
|
line: line,
|
||||||
column: column,
|
column: column,
|
||||||
|
width: None,
|
||||||
kind: $kind,
|
kind: $kind,
|
||||||
})
|
})
|
||||||
}};
|
}};
|
||||||
@ -559,7 +581,9 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
}
|
}
|
||||||
// indent: was no indentation, now there is
|
// indent: was no indentation, now there is
|
||||||
(None, Some(current @ _)) => {
|
(None, Some(current @ _)) => {
|
||||||
// check mixed leading whitespace
|
if mixed_whitespace(current) {
|
||||||
|
return error!(ErrorKind::MixedLeadingWhitespace{whitespace: current})
|
||||||
|
}
|
||||||
indent = Some(current);
|
indent = Some(current);
|
||||||
Some(Indent)
|
Some(Indent)
|
||||||
}
|
}
|
||||||
@ -577,13 +601,13 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
// check tabs after spaces
|
|
||||||
}
|
}
|
||||||
} {
|
} {
|
||||||
tokens.push(Token {
|
tokens.push(Token {
|
||||||
index: index,
|
index: index,
|
||||||
line: line,
|
line: line,
|
||||||
column: column,
|
column: column,
|
||||||
|
text: text,
|
||||||
prefix: "",
|
prefix: "",
|
||||||
lexeme: "",
|
lexeme: "",
|
||||||
class: class,
|
class: class,
|
||||||
@ -591,6 +615,19 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) =
|
let (prefix, lexeme, class) =
|
||||||
if let (0, Some(indent), Some(captures)) = (column, indent, LINE.captures(rest)) {
|
if let (0, Some(indent), Some(captures)) = (column, indent, LINE.captures(rest)) {
|
||||||
let line = captures.at(0).unwrap();
|
let line = captures.at(0).unwrap();
|
||||||
@ -626,6 +663,7 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
line: line,
|
line: line,
|
||||||
column: column,
|
column: column,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
|
text: text,
|
||||||
lexeme: lexeme,
|
lexeme: lexeme,
|
||||||
class: class,
|
class: class,
|
||||||
});
|
});
|
||||||
@ -650,9 +688,18 @@ fn tokenize(text: &str) -> Result<Vec<Token>, Error> {
|
|||||||
Ok(tokens)
|
Ok(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse<'a>(text: &'a str) -> Result<Justfile, Error> {
|
fn parse<'a>(text: &'a str) -> Result<Justfile, Error> {
|
||||||
let tokens = try!(tokenize(text));
|
let tokens = try!(tokenize(text));
|
||||||
let filtered: Vec<_> = tokens.into_iter().filter(|t| t.class != Comment).collect();
|
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{
|
let parser = Parser{
|
||||||
text: text,
|
text: text,
|
||||||
tokens: filtered.into_iter().peekable()
|
tokens: filtered.into_iter().peekable()
|
||||||
@ -667,7 +714,10 @@ struct Parser<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Parser<'a> {
|
||||||
/*
|
fn peek(&mut self, class: TokenClass) -> bool {
|
||||||
|
self.tokens.peek().unwrap().class == class
|
||||||
|
}
|
||||||
|
|
||||||
fn accept(&mut self, class: TokenClass) -> Option<Token<'a>> {
|
fn accept(&mut self, class: TokenClass) -> Option<Token<'a>> {
|
||||||
if self.peek(class) {
|
if self.peek(class) {
|
||||||
self.tokens.next()
|
self.tokens.next()
|
||||||
@ -680,80 +730,134 @@ impl<'a> Parser<'a> {
|
|||||||
self.accept(class).is_some()
|
self.accept(class).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peek(&mut self, class: TokenClass) -> bool {
|
fn expect(&mut self, class: TokenClass) -> Option<Token<'a>> {
|
||||||
self.tokens.peek().unwrap().class == class
|
if self.peek(class) {
|
||||||
}
|
self.tokens.next();
|
||||||
*/
|
None
|
||||||
|
} else {
|
||||||
/*
|
self.tokens.next()
|
||||||
|
|
||||||
fn expect(&mut self, class: TokenClass) {
|
|
||||||
if !self.accepted(class) {
|
|
||||||
panic!("we fucked");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
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 recipe(&mut self, name: &'a str, line_number: usize) -> Result<Recipe<'a>, Error<'a>> {
|
||||||
// fn accept(&mut self) -> Result<Token<'t>, Error<'t>> {
|
|
||||||
// match self.peek(
|
|
||||||
// }
|
|
||||||
|
|
||||||
fn recipe(&mut self, name: &'a str) -> Result<Recipe<'a>, Error<'a>> {
|
|
||||||
let mut arguments = vec![];
|
let mut arguments = vec![];
|
||||||
loop {
|
let mut argument_tokens = vec![];
|
||||||
if let Some(name_token) = self.accept(Name) {
|
while let Some(argument) = self.accept(Name) {
|
||||||
if arguments.contains(&name_token.lexeme) {
|
if arguments.contains(&argument.lexeme) {
|
||||||
return Err(error(self.text, name_token.line, ErrorKind::DuplicateArgument{
|
return Err(argument.error(ErrorKind::DuplicateArgument{
|
||||||
recipe: name, argument: name_token.lexeme}));
|
recipe: name, argument: argument.lexeme
|
||||||
}
|
}));
|
||||||
arguments.push(name_token.lexeme);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
arguments.push(argument.lexeme);
|
||||||
|
argument_tokens.push(argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.expect(Colon);
|
if let Some(token) = self.expect(Colon) {
|
||||||
|
return Err(self.unexpected_token(&token, &[Name, Colon]));
|
||||||
|
}
|
||||||
|
|
||||||
let mut dependencies = vec![];
|
let mut dependencies = vec![];
|
||||||
loop {
|
let mut dependency_tokens = vec![];
|
||||||
if let Some(name_token) = self.accept(Name) {
|
while let Some(dependency) = self.accept(Name) {
|
||||||
if dependencies.contains(&name_token.lexeme) {
|
if dependencies.contains(&dependency.lexeme) {
|
||||||
panic!("duplicate dependency");
|
return Err(dependency.error(ErrorKind::DuplicateDependency {
|
||||||
// return Err(error(self.text, name_token.line, ErrorKind::DuplicateDependency{
|
recipe: name,
|
||||||
// name: name_token.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![];
|
||||||
|
let mut shebang = false;
|
||||||
|
|
||||||
|
if self.accepted(Indent) {
|
||||||
|
while !self.peek(Dedent) {
|
||||||
|
if let Some(line) = self.accept(Line) {
|
||||||
|
if lines.len() == 0 {
|
||||||
|
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);
|
||||||
|
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]));
|
||||||
}
|
}
|
||||||
dependencies.push(name_token.lexeme);
|
}
|
||||||
} else {
|
|
||||||
break;
|
if let Some(token) = self.expect(Dedent) {
|
||||||
|
return Err(self.unexpected_token(&token, &[Dedent]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if !self.accept_eol() {
|
Ok(Recipe {
|
||||||
// return Err(error(self.text, i, ErrorKind::UnparsableDependencies));
|
line_number: line_number,
|
||||||
// }
|
name: name,
|
||||||
|
dependencies: dependencies,
|
||||||
panic!("we fucked");
|
dependency_tokens: dependency_tokens,
|
||||||
// Ok(Recipe{
|
arguments: arguments,
|
||||||
// })
|
argument_tokens: argument_tokens,
|
||||||
|
lines: lines,
|
||||||
|
shebang: shebang,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
fn error(self, token: &Token<'a>, kind: ErrorKind<'a>) -> Error<'a> {
|
fn unexpected_token(&self, found: &Token<'a>, expected: &[TokenClass]) -> Error<'a> {
|
||||||
token.error(self.text, kind)
|
found.error(ErrorKind::UnexpectedToken {
|
||||||
|
expected: expected.to_vec(),
|
||||||
|
found: found.class,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file(mut self) -> Result<Justfile<'a>, Error<'a>> {
|
fn file(mut self) -> Result<Justfile<'a>, Error<'a>> {
|
||||||
let recipes = BTreeMap::new();
|
let mut recipes = BTreeMap::<&str, Recipe>::new();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match self.tokens.next() {
|
match self.tokens.next() {
|
||||||
Some(token) => match token.class {
|
Some(token) => match token.class {
|
||||||
Eof => break,
|
Eof => break,
|
||||||
Eol => continue,
|
Eol => continue,
|
||||||
_ => return Err(self.error(&token, ErrorKind::InternalError {
|
Name => if let Some(equals) = self.accept(Equals) {
|
||||||
|
return Err(equals.error(ErrorKind::AssignmentUnimplemented));
|
||||||
|
} 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)
|
message: format!("unhandled token class: {:?}", token.class)
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@ -762,6 +866,7 @@ impl<'a> Parser<'a> {
|
|||||||
index: 0,
|
index: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
|
width: None,
|
||||||
kind: ErrorKind::InternalError {
|
kind: ErrorKind::InternalError {
|
||||||
message: "unexpected end of token stream".to_string()
|
message: "unexpected end of token stream".to_string()
|
||||||
}
|
}
|
||||||
@ -769,37 +874,20 @@ impl<'a> Parser<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
if let Some(token) = self.tokens.next() {
|
||||||
loop {
|
return Err(token.error(ErrorKind::InternalError{
|
||||||
if self.accepted(Eof) { break; }
|
|
||||||
if self.accept_eol() { continue; }
|
|
||||||
|
|
||||||
match self.tokens.next() {
|
|
||||||
Some(Token{class: Name, lexeme: name, ..}) => {
|
|
||||||
if self.accepted(Equals) {
|
|
||||||
panic!("Variable assignment not yet implemented");
|
|
||||||
} else {
|
|
||||||
if recipes.contains_key(name) {
|
|
||||||
// return Err(error(self.text, line, ErrorKind::DuplicateDependency{
|
|
||||||
// name: name,
|
|
||||||
// }));
|
|
||||||
panic!("duplicate dep");
|
|
||||||
}
|
|
||||||
let recipe = try!(self.recipe(name));
|
|
||||||
recipes.insert(name, recipe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("got something else")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if let Some(ref token) = self.tokens.next() {
|
|
||||||
return Err(self.error(token, ErrorKind::InternalError{
|
|
||||||
message: format!("unexpected token remaining after parsing completed: {:?}", token.class)
|
message: format!("unexpected token remaining after parsing completed: {:?}", token.class)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut resolved = HashSet::new();
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut stack = vec![];
|
||||||
|
|
||||||
|
for recipe in recipes.values() {
|
||||||
|
try!(resolve(&recipes, &mut resolved, &mut seen, &mut stack, &recipe));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Justfile{recipes: recipes})
|
Ok(Justfile{recipes: recipes})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
97
src/main.rs
97
src/main.rs
@ -1,100 +1,5 @@
|
|||||||
extern crate j;
|
extern crate j;
|
||||||
extern crate clap;
|
|
||||||
|
|
||||||
use std::{io, fs, env};
|
|
||||||
use clap::{App, Arg};
|
|
||||||
use j::Slurp;
|
|
||||||
|
|
||||||
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)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("j")
|
j::app();
|
||||||
.version("0.1.5")
|
|
||||||
.author("Casey R. <casey@rodarmor.com>")
|
|
||||||
.about("Just a command runner")
|
|
||||||
.arg(Arg::with_name("list")
|
|
||||||
.short("l")
|
|
||||||
.long("list")
|
|
||||||
.help("Lists available recipes"))
|
|
||||||
.arg(Arg::with_name("show")
|
|
||||||
.short("s")
|
|
||||||
.long("show")
|
|
||||||
.takes_value(true)
|
|
||||||
.help("Show information about a recipe"))
|
|
||||||
.arg(Arg::with_name("recipe")
|
|
||||||
.multiple(true)
|
|
||||||
.help("recipe(s) to run, defaults to the first recipe in the justfile"))
|
|
||||||
.get_matches();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match fs::metadata("justfile") {
|
|
||||||
Ok(metadata) => if metadata.is_file() { break; },
|
|
||||||
Err(error) => {
|
|
||||||
if error.kind() != io::ErrorKind::NotFound {
|
|
||||||
die!("Error fetching justfile metadata: {}", error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match env::current_dir() {
|
|
||||||
Ok(pathbuf) => if pathbuf.as_os_str() == "/" { die!("No justfile found."); },
|
|
||||||
Err(error) => die!("Error getting current dir: {}", error),
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(error) = env::set_current_dir("..") {
|
|
||||||
die!("Error changing directory: {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = fs::File::open("justfile")
|
|
||||||
.unwrap_or_else(|error| die!("Error opening justfile: {}", error))
|
|
||||||
.slurp()
|
|
||||||
.unwrap_or_else(|error| die!("Error reading justfile: {}", error));
|
|
||||||
|
|
||||||
let justfile = j::parse(&text).unwrap_or_else(|error| die!("{}", error));
|
|
||||||
|
|
||||||
if matches.is_present("list") {
|
|
||||||
if justfile.count() == 0 {
|
|
||||||
warn!("Justfile contains no recipes");
|
|
||||||
} else {
|
|
||||||
warn!("{}", justfile.recipes().join(" "));
|
|
||||||
}
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(name) = matches.value_of("show") {
|
|
||||||
match justfile.get(name) {
|
|
||||||
Some(recipe) => {
|
|
||||||
warn!("{}", recipe);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
None => die!("justfile contains no recipe \"{}\"", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let names = if let Some(names) = matches.values_of("recipe") {
|
|
||||||
names.collect::<Vec<_>>()
|
|
||||||
} else if let Some(name) = justfile.first() {
|
|
||||||
vec![name]
|
|
||||||
} else {
|
|
||||||
die!("Justfile contains no recipes");
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(run_error) = justfile.run(&names) {
|
|
||||||
warn!("{}", run_error);
|
|
||||||
std::process::exit(if let j::RunError::Code{code, ..} = run_error { code } else { -1 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
510
src/tests.rs
510
src/tests.rs
@ -1,213 +1,9 @@
|
|||||||
/*
|
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use super::{ErrorKind, Justfile};
|
|
||||||
|
|
||||||
fn expect_error(text: &str, line: usize, expected_error_kind: ErrorKind) {
|
|
||||||
match super::parse(text) {
|
|
||||||
Ok(_) => panic!("Expected {:?} but parse succeeded", expected_error_kind),
|
|
||||||
Err(error) => {
|
|
||||||
if error.line != line {
|
|
||||||
panic!("Expected {:?} error on line {} but error was on line {}",
|
|
||||||
expected_error_kind, line, error.line);
|
|
||||||
}
|
|
||||||
if error.kind != expected_error_kind {
|
|
||||||
panic!("Expected {:?} error but got {:?}", expected_error_kind, error.kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_recipe(
|
|
||||||
justfile: &Justfile,
|
|
||||||
name: &str,
|
|
||||||
line: usize,
|
|
||||||
leading_whitespace: &str,
|
|
||||||
lines: &[&str],
|
|
||||||
dependencies: &[&str]
|
|
||||||
) {
|
|
||||||
let recipe = match justfile.recipes.get(name) {
|
|
||||||
Some(recipe) => recipe,
|
|
||||||
None => panic!("Justfile had no recipe \"{}\"", name),
|
|
||||||
};
|
|
||||||
assert_eq!(recipe.name, name);
|
|
||||||
assert_eq!(recipe.line_number, line);
|
|
||||||
assert_eq!(recipe.leading_whitespace, leading_whitespace);
|
|
||||||
assert_eq!(recipe.lines, lines);
|
|
||||||
assert_eq!(recipe.dependencies.iter().cloned().collect::<Vec<_>>(), dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn circular_dependency() {
|
|
||||||
expect_error("a: b\nb: a", 1, ErrorKind::CircularDependency{circle: vec!["a", "b", "a"]});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn duplicate_dependency() {
|
|
||||||
expect_error("a: b b", 0, ErrorKind::DuplicateDependency{name: "b"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn duplicate_recipe() {
|
|
||||||
expect_error(
|
|
||||||
"a:\na:",
|
|
||||||
1, ErrorKind::DuplicateRecipe{first: 0, name: "a"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tab_after_spaces() {
|
|
||||||
expect_error(
|
|
||||||
"a:\n \tspaces",
|
|
||||||
1, ErrorKind::TabAfterSpace{whitespace: " \t"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mixed_leading_whitespace() {
|
|
||||||
expect_error(
|
|
||||||
"a:\n\t spaces",
|
|
||||||
1, ErrorKind::MixedLeadingWhitespace{whitespace: "\t "}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn inconsistent_leading_whitespace() {
|
|
||||||
expect_error(
|
|
||||||
"a:\n\t\ttabs\n\t\ttabs\n spaces",
|
|
||||||
3, ErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: " "}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn shebang_errors() {
|
|
||||||
expect_error("#!/bin/sh", 0, ErrorKind::OuterShebang);
|
|
||||||
expect_error("a:\n echo hello\n #!/bin/sh", 2, ErrorKind::NonLeadingShebang{recipe:"a"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_dependency() {
|
|
||||||
expect_error("a: b", 0, ErrorKind::UnknownDependency{name: "a", unknown: "b"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extra_whitespace() {
|
|
||||||
expect_error("a:\n blah\n blarg", 2, ErrorKind::ExtraLeadingWhitespace);
|
|
||||||
expect_success("a:\n #!\n print(1)");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unparsable() {
|
|
||||||
expect_error("hello", 0, ErrorKind::Unparsable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
can we bring this error back?
|
|
||||||
#[test]
|
|
||||||
fn unparsable_dependencies() {
|
|
||||||
expect_error("a: -f", 0, ErrorKind::UnparsableDependencies);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
we should be able to emit these errors
|
|
||||||
#[test]
|
|
||||||
fn bad_recipe_names() {
|
|
||||||
fn expect_bad_name(text: &str, name: &str) {
|
|
||||||
expect_error(text, 0, ErrorKind::UnknownStartOfToken{name: name});
|
|
||||||
}
|
|
||||||
expect_bad_name("Z:", "Z");
|
|
||||||
expect_bad_name("a-:", "a-");
|
|
||||||
expect_bad_name("-a:", "-a");
|
|
||||||
expect_bad_name("a--a:", "a--a");
|
|
||||||
expect_bad_name("@:", "@");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
let justfile = expect_success("a: b c\nb: c\n echo hello\n\nc:\n\techo goodbye\n#\n#hello");
|
|
||||||
assert!(justfile.recipes.keys().cloned().collect::<Vec<_>>() == vec!["a", "b", "c"]);
|
|
||||||
check_recipe(&justfile, "a", 0, "", &[ ], &["b", "c"]);
|
|
||||||
check_recipe(&justfile, "b", 1, " ", &["echo hello" ], &["c" ]);
|
|
||||||
check_recipe(&justfile, "c", 4, "\t", &["echo goodbye"], &[ ]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn first() {
|
|
||||||
let justfile = expect_success("#hello\n#goodbye\na:\nb:\nc:\n");
|
|
||||||
assert!(justfile.first().unwrap() == "a");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unknown_recipes() {
|
|
||||||
match expect_success("a:\nb:\nc:").run(&["a", "x", "y", "z"]).unwrap_err() {
|
|
||||||
super::RunError::UnknownRecipes{recipes} => assert_eq!(recipes, &["x", "y", "z"]),
|
|
||||||
other @ _ => panic!("expected an unknown recipe error, but got: {}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn code_error() {
|
|
||||||
match expect_success("fail:\n @function x { return 100; }; x").run(&["fail"]).unwrap_err() {
|
|
||||||
super::RunError::Code{recipe, code} => {
|
|
||||||
assert_eq!(recipe, "fail");
|
|
||||||
assert_eq!(code, 100);
|
|
||||||
},
|
|
||||||
other @ _ => panic!("expected a code run error, but got: {}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn run_order() {
|
|
||||||
let tmp = tempdir::TempDir::new("run_order").unwrap_or_else(|err| panic!("tmpdir: failed to create temporary directory: {}", err));
|
|
||||||
let path = tmp.path().to_str().unwrap_or_else(|| panic!("tmpdir: path was not valid UTF-8")).to_owned();
|
|
||||||
let text = r"
|
|
||||||
a:
|
|
||||||
@touch a
|
|
||||||
|
|
||||||
b: a
|
|
||||||
@mv a b
|
|
||||||
|
|
||||||
c: b
|
|
||||||
@mv b c
|
|
||||||
|
|
||||||
d: c
|
|
||||||
@rm c
|
|
||||||
";
|
|
||||||
super::std::env::set_current_dir(path).expect("failed to set current directory");
|
|
||||||
expect_success(text).run(&["a", "d"]).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn shebang() {
|
|
||||||
// this test exists to make sure that shebang recipes
|
|
||||||
// run correctly. although this script is still
|
|
||||||
// executed by sh its behavior depends on the value of a
|
|
||||||
// variable and continuing even though a command fails
|
|
||||||
let text = "
|
|
||||||
a:
|
|
||||||
#!/usr/bin/env sh
|
|
||||||
code=200
|
|
||||||
function x { return $code; }
|
|
||||||
x
|
|
||||||
x
|
|
||||||
";
|
|
||||||
|
|
||||||
match expect_success(text).run(&["a"]).unwrap_err() {
|
|
||||||
super::RunError::Code{recipe, code} => {
|
|
||||||
assert_eq!(recipe, "a");
|
|
||||||
assert_eq!(code, 200);
|
|
||||||
},
|
|
||||||
other @ _ => panic!("expected an code run error, but got: {}", other),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
use super::{Token, Error, ErrorKind, Justfile};
|
use super::{Token, Error, ErrorKind, Justfile};
|
||||||
|
|
||||||
|
use super::TokenClass::*;
|
||||||
|
|
||||||
fn tokenize_success(text: &str, expected_summary: &str) {
|
fn tokenize_success(text: &str, expected_summary: &str) {
|
||||||
let tokens = super::tokenize(text).unwrap();
|
let tokens = super::tokenize(text).unwrap();
|
||||||
let roundtrip = tokens.iter().map(|t| {
|
let roundtrip = tokens.iter().map(|t| {
|
||||||
@ -252,7 +48,30 @@ fn token_summary(tokens: &[Token]) -> String {
|
|||||||
fn parse_success(text: &str) -> Justfile {
|
fn parse_success(text: &str) -> Justfile {
|
||||||
match super::parse(text) {
|
match super::parse(text) {
|
||||||
Ok(justfile) => justfile,
|
Ok(justfile) => justfile,
|
||||||
Err(error) => panic!("Expected successful parse but got error {}", error),
|
Err(error) => panic!("Expected successful parse but got error:\n{}", error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_summary(input: &str, output: &str) {
|
||||||
|
let justfile = parse_success(input);
|
||||||
|
let mut s = String::new();
|
||||||
|
for recipe in justfile.recipes {
|
||||||
|
s += &format!("{}\n", recipe.1);
|
||||||
|
}
|
||||||
|
assert_eq!(s, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_error(text: &str, expected: Error) {
|
||||||
|
if let Err(error) = super::parse(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +96,7 @@ bob:
|
|||||||
frank
|
frank
|
||||||
";
|
";
|
||||||
|
|
||||||
tokenize_success(text, "$N:$>*$*$$*$$*$$<N:$>*$.");
|
tokenize_success(text, "$N:$>*$*$$*$$*$$<N:$>*$<.");
|
||||||
|
|
||||||
tokenize_success("a:=#", "N:=#.")
|
tokenize_success("a:=#", "N:=#.")
|
||||||
}
|
}
|
||||||
@ -294,6 +113,7 @@ fn inconsistent_leading_whitespace() {
|
|||||||
index: 9,
|
index: 9,
|
||||||
line: 3,
|
line: 3,
|
||||||
column: 0,
|
column: 0,
|
||||||
|
width: None,
|
||||||
kind: ErrorKind::InconsistentLeadingWhitespace{expected: " ", found: "\t"},
|
kind: ErrorKind::InconsistentLeadingWhitespace{expected: " ", found: "\t"},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -307,6 +127,7 @@ fn inconsistent_leading_whitespace() {
|
|||||||
index: 12,
|
index: 12,
|
||||||
line: 3,
|
line: 3,
|
||||||
column: 0,
|
column: 0,
|
||||||
|
width: None,
|
||||||
kind: ErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "},
|
kind: ErrorKind::InconsistentLeadingWhitespace{expected: "\t\t", found: "\t "},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -319,6 +140,7 @@ fn outer_shebang() {
|
|||||||
index: 0,
|
index: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
|
width: None,
|
||||||
kind: ErrorKind::OuterShebang
|
kind: ErrorKind::OuterShebang
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -331,16 +153,284 @@ fn unknown_start_of_token() {
|
|||||||
index: 0,
|
index: 0,
|
||||||
line: 0,
|
line: 0,
|
||||||
column: 0,
|
column: 0,
|
||||||
|
width: None,
|
||||||
kind: ErrorKind::UnknownStartOfToken
|
kind: ErrorKind::UnknownStartOfToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse() {
|
fn parse() {
|
||||||
parse_success("
|
parse_summary("
|
||||||
|
|
||||||
# hello
|
# hello
|
||||||
|
|
||||||
|
|
||||||
");
|
", "");
|
||||||
|
|
||||||
|
parse_summary("
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z:
|
||||||
|
hello a b c : x y z #hello
|
||||||
|
#! blah
|
||||||
|
#blarg
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
", "hello a b c: x y z
|
||||||
|
#! blah
|
||||||
|
#blarg
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
x:
|
||||||
|
y:
|
||||||
|
z:
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assignment_unimplemented() {
|
||||||
|
let text = "a = z";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 2,
|
||||||
|
line: 0,
|
||||||
|
column: 2,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::AssignmentUnimplemented
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_colon() {
|
||||||
|
let text = "a b c\nd e f";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 5,
|
||||||
|
line: 0,
|
||||||
|
column: 5,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::UnexpectedToken{expected: vec![Name, Colon], found: Eol},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_eol() {
|
||||||
|
let text = "a b c: z =";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 9,
|
||||||
|
line: 0,
|
||||||
|
column: 9,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::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\nx:\ny:\nz:\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_argument() {
|
||||||
|
let text = "a b b:";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 4,
|
||||||
|
line: 0,
|
||||||
|
column: 4,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::DuplicateArgument{recipe: "a", argument: "b"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_dependency() {
|
||||||
|
let text = "a b c: b c z z";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 13,
|
||||||
|
line: 0,
|
||||||
|
column: 13,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::DuplicateDependency{recipe: "a", dependency: "z"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_recipe() {
|
||||||
|
let text = "a:\nb:\na:";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 6,
|
||||||
|
line: 2,
|
||||||
|
column: 0,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::DuplicateRecipe{recipe: "a", first: 0}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn circular_dependency() {
|
||||||
|
let text = "a: b\nb: a";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 8,
|
||||||
|
line: 1,
|
||||||
|
column: 3,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::CircularDependency{recipe: "b", circle: vec!["a", "b", "a"]}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_dependency() {
|
||||||
|
let text = "a: b";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 0,
|
||||||
|
column: 3,
|
||||||
|
width: Some(1),
|
||||||
|
kind: ErrorKind::UnknownDependency{recipe: "a", unknown: "b"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_leading_whitespace() {
|
||||||
|
let text = "a:\n\t echo hello";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 3,
|
||||||
|
line: 1,
|
||||||
|
column: 0,
|
||||||
|
width: None,
|
||||||
|
kind: ErrorKind::MixedLeadingWhitespace{whitespace: "\t "}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn write_or() {
|
||||||
|
assert_eq!("1", super::Or(&[1 ]).to_string());
|
||||||
|
assert_eq!("1 or 2", super::Or(&[1,2 ]).to_string());
|
||||||
|
assert_eq!("1, 2, or 3", super::Or(&[1,2,3 ]).to_string());
|
||||||
|
assert_eq!("1, 2, 3, or 4", super::Or(&[1,2,3,4]).to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_shebang() {
|
||||||
|
// this test exists to make sure that shebang recipes
|
||||||
|
// run correctly. although this script is still
|
||||||
|
// executed by sh 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
|
||||||
|
function x { return $code; }
|
||||||
|
x
|
||||||
|
x
|
||||||
|
";
|
||||||
|
|
||||||
|
match parse_success(text).run(&["a"]).unwrap_err() {
|
||||||
|
super::RunError::Code{recipe, code} => {
|
||||||
|
assert_eq!(recipe, "a");
|
||||||
|
assert_eq!(code, 200);
|
||||||
|
},
|
||||||
|
other @ _ => panic!("expected an code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_order() {
|
||||||
|
let tmp = tempdir::TempDir::new("run_order").unwrap_or_else(|err| panic!("tmpdir: failed to create temporary directory: {}", err));
|
||||||
|
let path = tmp.path().to_str().unwrap_or_else(|| panic!("tmpdir: path was not valid UTF-8")).to_owned();
|
||||||
|
let text = r"
|
||||||
|
a:
|
||||||
|
@touch a
|
||||||
|
|
||||||
|
b: a
|
||||||
|
@mv a b
|
||||||
|
|
||||||
|
c: b
|
||||||
|
@mv b c
|
||||||
|
|
||||||
|
d: c
|
||||||
|
@rm c
|
||||||
|
";
|
||||||
|
super::std::env::set_current_dir(path).expect("failed to set current directory");
|
||||||
|
parse_success(text).run(&["a", "d"]).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_recipes() {
|
||||||
|
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"]).unwrap_err() {
|
||||||
|
super::RunError::UnknownRecipes{recipes} => assert_eq!(recipes, &["x", "y", "z"]),
|
||||||
|
other @ _ => panic!("expected an unknown recipe error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn code_error() {
|
||||||
|
match parse_success("fail:\n @function x { return 100; }; x").run(&["fail"]).unwrap_err() {
|
||||||
|
super::RunError::Code{recipe, code} => {
|
||||||
|
assert_eq!(recipe, "fail");
|
||||||
|
assert_eq!(code, 100);
|
||||||
|
},
|
||||||
|
other @ _ => panic!("expected a code run error, but got: {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extra_whitespace() {
|
||||||
|
// we might want to make extra leading whitespace a line continuation in the future,
|
||||||
|
// so make it a error for now
|
||||||
|
let text = "a:\n blah\n blarg";
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: 10,
|
||||||
|
line: 2,
|
||||||
|
column: 1,
|
||||||
|
width: Some(6),
|
||||||
|
kind: ErrorKind::ExtraLeadingWhitespace
|
||||||
|
});
|
||||||
|
|
||||||
|
// extra leading whitespace is okay in a shebang recipe
|
||||||
|
parse_success("a:\n #!\n print(1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_recipe_names() {
|
||||||
|
// We are extra strict with names. Although the tokenizer
|
||||||
|
// will tokenize anything that matches /[a-zA-Z0-9_-]+/
|
||||||
|
// as a name, we throw an error if names do not match
|
||||||
|
// /[a-z](-?[a-z])*/. This is to support future expansion
|
||||||
|
// of justfile and command line syntax.
|
||||||
|
fn bad_name(text: &str, name: &str, index: usize, line: usize, column: usize) {
|
||||||
|
parse_error(text, Error {
|
||||||
|
text: text,
|
||||||
|
index: index,
|
||||||
|
line: line,
|
||||||
|
column: column,
|
||||||
|
width: Some(name.len()),
|
||||||
|
kind: ErrorKind::BadName{name: name}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bad_name("-a", "-a", 0, 0, 0);
|
||||||
|
bad_name("_a", "_a", 0, 0, 0);
|
||||||
|
bad_name("a-", "a-", 0, 0, 0);
|
||||||
|
bad_name("a_", "a_", 0, 0, 0);
|
||||||
|
bad_name("a__a", "a__a", 0, 0, 0);
|
||||||
|
bad_name("a--a", "a--a", 0, 0, 0);
|
||||||
|
bad_name("a: a--", "a--", 3, 0, 3);
|
||||||
|
bad_name("a: 9a", "9a", 3, 0, 3);
|
||||||
|
bad_name("a: 9a", "9a", 3, 0, 3);
|
||||||
|
bad_name("a:\nZ:", "Z", 3, 1, 0);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user