just/src/justfile.rs

837 lines
16 KiB
Rust
Raw Normal View History

use crate::common::*;
2017-11-16 23:30:08 -08:00
#[derive(Debug, PartialEq)]
pub(crate) struct Justfile<'src> {
pub(crate) recipes: Table<'src, Recipe<'src>>,
pub(crate) assignments: Table<'src, Assignment<'src>>,
pub(crate) aliases: Table<'src, Alias<'src>>,
pub(crate) settings: Settings<'src>,
pub(crate) warnings: Vec<Warning<'src>>,
2017-11-16 23:30:08 -08:00
}
impl<'src> Justfile<'src> {
pub(crate) fn first(&self) -> Option<&Recipe> {
2017-11-16 23:30:08 -08:00
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() {
2017-11-16 23:30:08 -08:00
first = Some(recipe)
}
} else {
first = Some(recipe);
}
}
first
}
pub(crate) fn count(&self) -> usize {
2017-11-16 23:30:08 -08:00
self.recipes.len()
}
pub(crate) fn suggest(&self, name: &str) -> Option<&'src str> {
let mut suggestions = self
.recipes
.keys()
2017-11-16 23:30:08 -08:00
.map(|suggestion| (edit_distance(suggestion, name), suggestion))
.collect::<Vec<_>>();
suggestions.sort();
if let Some(&(distance, suggestion)) = suggestions.first() {
if distance < 3 {
return Some(suggestion);
2017-11-16 23:30:08 -08:00
}
}
None
}
pub(crate) fn run(
&'src self,
config: &'src Config,
working_directory: &'src Path,
overrides: &'src BTreeMap<String, String>,
arguments: &'src Vec<String>,
) -> RunResult<'src, ()> {
let argvec: Vec<&str> = if !arguments.is_empty() {
arguments.iter().map(|argument| argument.as_str()).collect()
} else if let Some(recipe) = self.first() {
let min_arguments = recipe.min_arguments();
if min_arguments > 0 {
return Err(RuntimeError::DefaultRecipeRequiresArguments {
recipe: recipe.name.lexeme(),
min_arguments,
});
}
vec![recipe.name()]
} else {
return Err(RuntimeError::NoRecipes);
};
let arguments = argvec.as_slice();
let unknown_overrides = overrides
.keys()
.filter(|name| !self.assignments.contains_key(name.as_str()))
.map(|name| name.as_str())
.collect::<Vec<&str>>();
2017-11-16 23:30:08 -08:00
if !unknown_overrides.is_empty() {
return Err(RuntimeError::UnknownOverrides {
overrides: unknown_overrides,
});
2017-11-16 23:30:08 -08:00
}
2018-03-05 13:21:35 -08:00
let dotenv = load_dotenv()?;
let scope = AssignmentEvaluator::evaluate_assignments(
config,
working_directory,
2018-03-05 13:21:35 -08:00
&dotenv,
&self.assignments,
overrides,
&self.settings,
2017-11-16 23:30:08 -08:00
)?;
if let Subcommand::Evaluate { .. } = config.subcommand {
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, (_export, value)) in scope {
println!("{0:1$} := \"{2}\"", name, width, value);
2017-11-16 23:30:08 -08:00
}
return Ok(());
}
let mut missing = vec![];
let mut grouped = vec![];
let mut rest = arguments;
2017-11-16 23:30:08 -08:00
while let Some((argument, mut tail)) = rest.split_first() {
if let Some(recipe) = self.get_recipe(argument) {
2017-11-16 23:30:08 -08:00
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) {
2017-11-16 23:30:08 -08:00
return Err(RuntimeError::ArgumentCountMismatch {
recipe: recipe.name(),
parameters: recipe.parameters.iter().collect(),
found: tail.len(),
min: recipe.min_arguments(),
max: recipe.max_arguments(),
2017-11-16 23:30:08 -08:00
});
}
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
};
return Err(RuntimeError::UnknownRecipes {
recipes: missing,
suggestion,
});
2017-11-16 23:30:08 -08:00
}
let context = RecipeContext {
settings: &self.settings,
config,
scope,
working_directory,
};
2018-08-27 18:36:40 -07:00
2017-11-16 23:30:08 -08:00
let mut ran = empty();
for (recipe, arguments) in grouped {
self.run_recipe(&context, recipe, arguments, &dotenv, &mut ran, overrides)?
2017-11-16 23:30:08 -08:00
}
Ok(())
}
pub(crate) fn get_alias(&self, name: &str) -> Option<&Alias> {
self.aliases.get(name)
}
pub(crate) fn get_recipe(&self, name: &str) -> Option<&Recipe<'src>> {
if let Some(recipe) = self.recipes.get(name) {
Some(recipe)
} else if let Some(alias) = self.aliases.get(name) {
self.recipes.get(alias.target.lexeme())
} else {
None
}
}
2018-08-27 18:36:40 -07:00
fn run_recipe<'b>(
&self,
context: &'b RecipeContext<'src>,
recipe: &Recipe<'src>,
arguments: &[&'src str],
dotenv: &BTreeMap<String, String>,
ran: &mut BTreeSet<&'src str>,
overrides: &BTreeMap<String, String>,
2017-11-17 17:28:06 -08:00
) -> RunResult<()> {
2017-11-16 23:30:08 -08:00
for dependency_name in &recipe.dependencies {
let lexeme = dependency_name.lexeme();
if !ran.contains(lexeme) {
self.run_recipe(context, &self.recipes[lexeme], &[], dotenv, ran, overrides)?;
2017-11-16 23:30:08 -08:00
}
}
recipe.run(context, arguments, dotenv, overrides)?;
ran.insert(recipe.name());
2017-11-16 23:30:08 -08:00
Ok(())
}
}
impl<'src> Display for Justfile<'src> {
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
let mut items = self.recipes.len() + self.assignments.len() + self.aliases.len();
for (name, assignment) in &self.assignments {
if assignment.export {
2017-11-16 23:30:08 -08:00
write!(f, "export ")?;
}
write!(f, "{} := {}", name, assignment.expression)?;
2017-11-16 23:30:08 -08:00
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
for alias in self.aliases.values() {
write!(f, "{}", alias)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
2017-11-16 23:30:08 -08:00
for recipe in self.recipes.values() {
write!(f, "{}", recipe)?;
items -= 1;
if items != 0 {
write!(f, "\n\n")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
2017-11-16 23:30:08 -08:00
use super::*;
use testing::compile;
use RuntimeError::*;
run_error! {
name: unknown_recipes,
src: "a:\nb:\nc:",
args: ["a", "x", "y", "z"],
error: UnknownRecipes {
recipes,
suggestion,
},
check: {
assert_eq!(recipes, &["x", "y", "z"]);
assert_eq!(suggestion, None);
2017-11-16 23:30:08 -08:00
}
}
// 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
run_error! {
name: run_shebang,
src: "
a:
#!/usr/bin/env sh
code=200
x() { return $code; }
x
x
",
args: ["a"],
error: Code {
recipe,
line_number,
code,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 200);
assert_eq!(line_number, None);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
2017-11-17 17:28:06 -08:00
run_error! {
name: code_error,
src: "
fail:
@exit 100
",
args: ["fail"],
error: Code {
recipe,
line_number,
code,
},
check: {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: run_args,
src: r#"
a return code:
@x() { {{return}} {{code + "0"}}; }; x
"#,
args: ["a", "return", "15"],
error: Code {
recipe,
line_number,
code,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 150);
assert_eq!(line_number, Some(2));
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_arguments,
src: "a b c d:",
args: ["a", "b", "c"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_arguments_variadic,
src: "a b c +d:",
args: ["a", "B", "C"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 2);
assert_eq!(min, 3);
assert_eq!(max, usize::MAX - 1);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_all_arguments,
src: "a b c d:\n echo {{b}}{{c}}{{d}}",
args: ["a"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0);
assert_eq!(min, 3);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_some_defaults,
src: "a b c d='hello':",
args: ["a", "b"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 1);
assert_eq!(min, 2);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: missing_all_defaults,
src: "a b c='r' d='h':",
args: ["a"],
error: ArgumentCountMismatch {
recipe,
parameters,
found,
min,
max,
},
check: {
let param_names = parameters
.iter()
.map(|p| p.name.lexeme())
.collect::<Vec<&str>>();
assert_eq!(recipe, "a");
assert_eq!(param_names, ["b", "c", "d"]);
assert_eq!(found, 0);
assert_eq!(min, 1);
assert_eq!(max, 3);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: unknown_overrides,
src: "
a:
echo {{`f() { return 100; }; f`}}
",
args: ["foo=bar", "baz=bob", "a"],
error: UnknownOverrides { overrides },
check: {
assert_eq!(overrides, &["baz", "foo"]);
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
run_error! {
name: export_failure,
src: r#"
export foo = "a"
baz = "c"
export bar = "b"
export abc = foo + bar + baz
wut:
echo $foo $bar $baz
"#,
args: ["--quiet", "wut"],
error: Code {
code: _,
line_number,
recipe,
},
check: {
assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(7));
2017-11-17 17:28:06 -08:00
}
2017-11-16 23:30:08 -08:00
}
macro_rules! test {
($name:ident, $input:expr, $expected:expr $(,)*) => {
#[test]
fn $name() {
test($input, $expected);
}
};
}
fn test(input: &str, expected: &str) {
let justfile = compile(input);
let actual = format!("{:#}", justfile);
assert_eq!(actual, expected);
println!("Re-parsing...");
let reparsed = compile(&actual);
let redumped = format!("{:#}", reparsed);
assert_eq!(redumped, actual);
}
test! {
parse_empty,
"
# hello
",
"",
}
test! {
parse_string_default,
r#"
foo a="b\t":
"#,
r#"foo a="b\t":"#,
}
test! {
parse_multiple,
r#"
a:
b:
"#,
r#"a:
b:"#,
}
test! {
parse_variadic,
r#"
foo +a:
"#,
r#"foo +a:"#,
}
test! {
parse_variadic_string_default,
r#"
foo +a="Hello":
"#,
r#"foo +a="Hello":"#,
}
test! {
parse_raw_string_default,
r#"
foo a='b\t':
"#,
r#"foo a='b\t':"#,
}
test! {
parse_export,
r#"
export a := "hello"
"#,
r#"export a := "hello""#,
}
test! {
parse_alias_after_target,
r#"
foo:
echo a
alias f := foo
"#,
r#"alias f := foo
foo:
echo a"#
}
test! {
parse_alias_before_target,
r#"
alias f := foo
foo:
echo a
"#,
r#"alias f := foo
foo:
echo a"#
}
test! {
parse_alias_with_comment,
r#"
alias f := foo #comment
foo:
echo a
"#,
r#"alias f := foo
foo:
echo a"#
}
test! {
parse_complex,
"
x:
y:
z:
foo := \"xx\"
bar := foo
goodbye := \"y\"
hello a b c : x y z #hello
#! blah
#blarg
{{ foo + bar}}abc{{ goodbye\t + \"x\" }}xyz
1
2
3
",
"bar := foo
foo := \"xx\"
goodbye := \"y\"
hello a b c: x y z
#! blah
#blarg
{{foo + bar}}abc{{goodbye + \"x\"}}xyz
1
2
3
x:
y:
z:"
}
test! {
parse_shebang,
"
practicum := 'hello'
install:
\t#!/bin/sh
\tif [[ -f {{practicum}} ]]; then
\t\treturn
\tfi
",
"practicum := 'hello'
install:
#!/bin/sh
if [[ -f {{practicum}} ]]; then
\treturn
fi",
}
test! {
parse_simple_shebang,
"a:\n #!\n print(1)",
"a:\n #!\n print(1)",
}
test! {
parse_assignments,
r#"a := "0"
c := a + b + a + b
b := "1"
"#,
r#"a := "0"
b := "1"
c := a + b + a + b"#,
}
test! {
parse_assignment_backticks,
"a := `echo hello`
c := a + b + a + b
b := `echo goodbye`",
"a := `echo hello`
b := `echo goodbye`
c := a + b + a + b",
}
test! {
parse_interpolation_backticks,
r#"a:
echo {{ `echo hello` + "blarg" }} {{ `echo bob` }}"#,
r#"a:
echo {{`echo hello` + "blarg"}} {{`echo bob`}}"#,
}
test! {
eof_test,
"x:\ny:\nz:\na b c: x y z",
"a b c: x y z\n\nx:\n\ny:\n\nz:",
}
test! {
string_quote_escape,
r#"a := "hello\"""#,
r#"a := "hello\"""#,
}
test! {
string_escapes,
r#"a := "\n\t\r\"\\""#,
r#"a := "\n\t\r\"\\""#,
}
test! {
parameters,
"a b c:
{{b}} {{c}}",
"a b c:
{{b}} {{c}}",
}
test! {
unary_functions,
"
x := arch()
a:
{{os()}} {{os_family()}}",
"x := arch()
a:
{{os()}} {{os_family()}}",
}
test! {
env_functions,
r#"
x := env_var('foo',)
a:
{{env_var_or_default('foo' + 'bar', 'baz',)}} {{env_var(env_var("baz"))}}"#,
r#"x := env_var('foo')
a:
{{env_var_or_default('foo' + 'bar', 'baz')}} {{env_var(env_var("baz"))}}"#,
}
test! {
parameter_default_string,
r#"
f x="abc":
"#,
r#"f x="abc":"#,
}
test! {
parameter_default_raw_string,
r#"
f x='abc':
"#,
r#"f x='abc':"#,
}
test! {
parameter_default_backtick,
r#"
f x=`echo hello`:
"#,
r#"f x=`echo hello`:"#,
}
test! {
parameter_default_concatination_string,
r#"
f x=(`echo hello` + "foo"):
"#,
r#"f x=(`echo hello` + "foo"):"#,
}
test! {
parameter_default_concatination_variable,
r#"
x := "10"
f y=(`echo hello` + x) +z="foo":
"#,
r#"x := "10"
f y=(`echo hello` + x) +z="foo":"#,
}
test! {
parameter_default_multiple,
r#"
x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):
"#,
r#"x := "10"
f y=(`echo hello` + x) +z=("foo" + "bar"):"#,
}
test! {
concatination_in_group,
"x := ('0' + '1')",
"x := ('0' + '1')",
}
test! {
string_in_group,
"x := ('0' )",
"x := ('0')",
}
#[rustfmt::skip]
test! {
escaped_dos_newlines,
"@spam:\r
\t{ \\\r
\t\tfiglet test; \\\r
\t\tcargo build --color always 2>&1; \\\r
\t\tcargo test --color always -- --color always 2>&1; \\\r
\t} | less\r
",
"@spam:
{ \\
\tfiglet test; \\
\tcargo build --color always 2>&1; \\
\tcargo test --color always -- --color always 2>&1; \\
} | less",
}
2017-11-16 23:30:08 -08:00
}