just/src/justfile.rs

349 lines
9.5 KiB
Rust
Raw Normal View History

2017-11-16 23:30:08 -08:00
use common::*;
use edit_distance::edit_distance;
pub struct Justfile<'a> {
pub recipes: Map<&'a str, Recipe<'a>>,
pub assignments: Map<&'a str, Expression<'a>>,
pub exports: Set<&'a str>,
}
impl<'a, 'b> Justfile<'a> where 'a: 'b {
pub fn first(&self) -> Option<&Recipe> {
let mut first: Option<&Recipe> = None;
for recipe in self.recipes.values() {
if let Some(first_recipe) = first {
if recipe.line_number < first_recipe.line_number {
first = Some(recipe)
}
} else {
first = Some(recipe);
}
}
first
}
pub fn count(&self) -> usize {
self.recipes.len()
}
pub fn suggest(&self, name: &str) -> Option<&'a str> {
let mut suggestions = self.recipes.keys()
.map(|suggestion| (edit_distance(suggestion, name), suggestion))
.collect::<Vec<_>>();
suggestions.sort();
if let Some(&(distance, suggestion)) = suggestions.first() {
if distance < 3 {
return Some(suggestion)
}
}
None
}
pub fn run(
&'a self,
arguments: &[&'a str],
configuration: &Configuration<'a>,
2017-11-17 17:28:06 -08:00
) -> RunResult<'a, ()> {
let unknown_overrides = configuration.overrides.keys().cloned()
2017-11-16 23:30:08 -08:00
.filter(|name| !self.assignments.contains_key(name))
.collect::<Vec<_>>();
if !unknown_overrides.is_empty() {
return Err(RuntimeError::UnknownOverrides{overrides: unknown_overrides});
}
2018-03-05 13:21:35 -08:00
let dotenv = load_dotenv()?;
let scope = AssignmentEvaluator::evaluate_assignments(
2017-11-16 23:30:08 -08:00
&self.assignments,
2018-03-05 13:21:35 -08:00
&dotenv,
&configuration.overrides,
configuration.quiet,
configuration.shell,
configuration.dry_run,
2017-11-16 23:30:08 -08:00
)?;
if configuration.evaluate {
2017-11-16 23:30:08 -08:00
let mut width = 0;
for name in scope.keys() {
width = cmp::max(name.len(), width);
}
for (name, value) in scope {
println!("{0:1$} = \"{2}\"", name, width, value);
}
return Ok(());
}
let mut missing = vec![];
let mut grouped = vec![];
let mut rest = arguments;
while let Some((argument, mut tail)) = rest.split_first() {
if let Some(recipe) = self.recipes.get(argument) {
if recipe.parameters.is_empty() {
grouped.push((recipe, &tail[0..0]));
} else {
let argument_range = recipe.argument_range();
let argument_count = cmp::min(tail.len(), recipe.max_arguments());
if !argument_range.range_contains(argument_count) {
return Err(RuntimeError::ArgumentCountMismatch {
recipe: recipe.name,
found: tail.len(),
min: recipe.min_arguments(),
max: recipe.max_arguments(),
});
}
grouped.push((recipe, &tail[0..argument_count]));
tail = &tail[argument_count..];
}
} else {
missing.push(*argument);
}
rest = tail;
}
if !missing.is_empty() {
let suggestion = if missing.len() == 1 {
self.suggest(missing.first().unwrap())
} else {
None
};
2018-03-05 13:21:35 -08:00
return Err(RuntimeError::UnknownRecipes{recipes: missing, suggestion});
2017-11-16 23:30:08 -08:00
}
let mut ran = empty();
for (recipe, arguments) in grouped {
2018-03-05 13:21:35 -08:00
self.run_recipe(recipe, arguments, &scope, &dotenv, configuration, &mut ran)?
2017-11-16 23:30:08 -08:00
}
Ok(())
}
fn run_recipe<'c>(
&'c self,
recipe: &Recipe<'a>,
arguments: &[&'a str],
scope: &Map<&'c str, String>,
2018-03-05 13:21:35 -08:00
dotenv: &Map<String, String>,
configuration: &Configuration<'a>,
2018-03-05 13:21:35 -08:00
ran: &mut Set<&'a str>,
2017-11-17 17:28:06 -08:00
) -> RunResult<()> {
2017-11-16 23:30:08 -08:00
for dependency_name in &recipe.dependencies {
if !ran.contains(dependency_name) {
2018-03-05 13:21:35 -08:00
self.run_recipe(&self.recipes[dependency_name], &[], scope, dotenv, configuration, ran)?;
2017-11-16 23:30:08 -08:00
}
}
2018-03-05 13:21:35 -08:00
recipe.run(arguments, scope, dotenv, &self.exports, configuration)?;
2017-11-16 23:30:08 -08:00
ran.insert(recipe.name);
Ok(())
}
}
impl<'a> Display for Justfile<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
let mut items = self.recipes.len() + self.assignments.len();
for (name, expression) in &self.assignments {
if self.exports.contains(name) {
write!(f, "export ")?;
}
write!(f, "{} = {}", name, expression)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
for recipe in self.recipes.values() {
write!(f, "{}", recipe)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use testing::parse_success;
use RuntimeError::*;
2017-11-16 23:30:08 -08:00
#[test]
fn unknown_recipes() {
match parse_success("a:\nb:\nc:").run(&["a", "x", "y", "z"], &Default::default()).unwrap_err() {
UnknownRecipes{recipes, suggestion} => {
2017-11-16 23:30:08 -08:00
assert_eq!(recipes, &["x", "y", "z"]);
assert_eq!(suggestion, None);
}
other => panic!("expected an unknown recipe error, but got: {}", other),
}
}
2017-11-17 17:28:06 -08:00
#[test]
fn run_shebang() {
// this test exists to make sure that shebang recipes
// run correctly. although this script is still
// executed by a shell its behavior depends on the value of a
// variable and continuing even though a command fails,
// whereas in plain recipes variables are not available
// in subsequent lines and execution stops when a line
// fails
let text = "
2017-11-16 23:30:08 -08:00
a:
#!/usr/bin/env sh
code=200
x() { return $code; }
x
x
";
2017-11-17 17:28:06 -08:00
match parse_success(text).run(&["a"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(code, 200);
assert_eq!(line_number, None);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn code_error() {
match parse_success("fail:\n @exit 100")
.run(&["fail"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn run_args() {
let text = r#"
2017-11-16 23:30:08 -08:00
a return code:
@x() { {{return}} {{code + "0"}}; }; x"#;
2017-11-17 17:28:06 -08:00
match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() {
Code{recipe, line_number, code} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(code, 150);
assert_eq!(line_number, Some(3));
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn missing_some_arguments() {
match parse_success("a b c d:").run(&["a", "b", "c"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, 3);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn missing_some_arguments_variadic() {
match parse_success("a b c +d:").run(&["a", "B", "C"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, usize::MAX - 1);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn missing_all_arguments() {
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}")
.run(&["a"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(found, 0);
assert_eq!(min, 3);
assert_eq!(max, 3);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn missing_some_defaults() {
match parse_success("a b c d='hello':").run(&["a", "b"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(found, 1);
assert_eq!(min, 2);
assert_eq!(max, 3);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn missing_all_defaults() {
match parse_success("a b c='r' d='h':").run(&["a"], &Default::default()).unwrap_err() {
ArgumentCountMismatch{recipe, found, min, max} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "a");
assert_eq!(found, 0);
assert_eq!(min, 1);
assert_eq!(max, 3);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn unknown_overrides() {
let mut configuration: Configuration = Default::default();
configuration.overrides.insert("foo", "bar");
configuration.overrides.insert("baz", "bob");
2017-11-17 17:28:06 -08:00
match parse_success("a:\n echo {{`f() { return 100; }; f`}}")
.run(&["a"], &configuration).unwrap_err() {
UnknownOverrides{overrides} => {
2017-11-17 17:28:06 -08:00
assert_eq!(overrides, &["baz", "foo"]);
},
other => panic!("expected a code run error, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
#[test]
fn export_failure() {
let text = r#"
2017-11-16 23:30:08 -08:00
export foo = "a"
baz = "c"
export bar = "b"
export abc = foo + bar + baz
wut:
echo $foo $bar $baz
"#;
let configuration = Configuration {
2017-11-17 17:28:06 -08:00
quiet: true,
..Default::default()
};
match parse_success(text).run(&["wut"], &configuration).unwrap_err() {
Code{code: _, line_number, recipe} => {
2017-11-17 17:28:06 -08:00
assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(8));
},
other => panic!("expected a recipe code errror, but got: {}", other),
}
2017-11-16 23:30:08 -08:00
}
}