variable=value overrides done

This commit is contained in:
Casey Rodarmor 2016-10-30 03:08:28 -07:00
parent 25c6432fa3
commit 9a368fb351
6 changed files with 189 additions and 44 deletions

View file

@ -13,6 +13,8 @@ build:
check:
cargo check
nop:
publish: clippy build
# make sure version is up to date
git diff --no-ext-diff --quiet --exit-code

15
notes
View file

@ -7,13 +7,7 @@ notes
- test values of interpolations
- test results of concatination
- test string escape parsing
- --debug mode will evaluate everything and print values after assignments and interpolation expressions
. should it evaluate `` in recipes?
- set variables from the command line:
. j --set build linux
. j build=linux
- should it evaluate `` in recipes?
- before release:
@ -39,6 +33,8 @@ notes
. clean
. update logs (repetitive git flow)
- full documentation
. man page
. record sessions and replay them to output docs
. talk about why the syntax is so unforgiving
easier to accept a program that you once rejected than to
no longer accept a program or change its meaning
@ -71,4 +67,9 @@ enhancements:
. just xyz/foo # xyz/justfile:foo
. just xyz/ # xyz/justfile:DEFAULT
- allow setting and exporting environment variables
. export a as "HELLO_BAR"
. export a
. export HELLO_BAR = a
. export CC_FLAGS = "-g"
. will have to support crazy names
- indentation or slash for line continuation in plain recipes

View file

@ -1,6 +1,8 @@
extern crate clap;
extern crate regex;
use std::{io, fs, env, process};
use std::collections::BTreeMap;
use self::clap::{App, Arg};
use super::Slurp;
@ -37,6 +39,13 @@ pub fn app() {
.takes_value(true)
.value_name("recipe")
.help("Show information about <recipe>"))
.arg(Arg::with_name("set")
.long("set")
.takes_value(true)
.number_of_values(2)
.value_names(&["variable", "value"])
.multiple(true)
.help("set <variable> to <value>"))
.arg(Arg::with_name("working-directory")
.long("working-directory")
.takes_value(true)
@ -123,15 +132,37 @@ pub fn app() {
}
}
let set_count = matches.occurrences_of("set");
let mut overrides = BTreeMap::new();
if set_count > 0 {
let mut values = matches.values_of("set").unwrap();
for _ in 0..set_count {
overrides.insert(values.next().unwrap(), values.next().unwrap());
}
}
let override_re = regex::Regex::new("^([^=]+)=(.*)$").unwrap();
let arguments = if let Some(arguments) = matches.values_of("arguments") {
arguments.collect::<Vec<_>>()
let mut done = false;
let mut rest = vec![];
for argument in arguments {
if !done && override_re.is_match(argument) {
let captures = override_re.captures(argument).unwrap();
overrides.insert(captures.at(1).unwrap(), captures.at(2).unwrap());
} else {
rest.push(argument);
done = true;
}
}
rest
} else if let Some(recipe) = justfile.first() {
vec![recipe]
} else {
die!("Justfile contains no recipes");
};
if let Err(run_error) = justfile.run(&arguments) {
if let Err(run_error) = justfile.run(&overrides, &arguments) {
warn!("{}", run_error);
match run_error {
super::RunError::Code{code, .. } => process::exit(code),

View file

@ -349,3 +349,52 @@ a = `exit 222`",
",
);
}
#[test]
fn unknown_override_options() {
integration_test(
"unknown_override_options",
&["--set", "foo", "bar", "a", "b", "--set", "baz", "bob", "--set", "a", "b"],
"foo:
echo hello
echo {{`exit 111`}}
a = `exit 222`",
255,
"",
"baz and foo set on the command line but not present in justfile\n",
);
}
#[test]
fn unknown_override_args() {
integration_test(
"unknown_override_args",
&["foo=bar", "baz=bob", "a=b", "a", "b"],
"foo:
echo hello
echo {{`exit 111`}}
a = `exit 222`",
255,
"",
"baz and foo set on the command line but not present in justfile\n",
);
}
#[test]
fn overrides_first() {
integration_test(
"unknown_override_args",
&["foo=bar", "a=b", "recipe", "baz=bar"],
r#"
foo = "foo"
a = "a"
baz = "baz"
recipe arg:
echo arg={{arg}}
echo {{foo + a + baz}}"#,
0,
"arg=baz=bar\nbarbbaz\n",
"echo arg=baz=bar\necho barbbaz\n",
);
}

View file

@ -197,6 +197,7 @@ impl<'a> Recipe<'a> {
evaluated: BTreeMap::new(),
scope: scope,
assignments: &BTreeMap::new(),
overrides: &BTreeMap::new(),
};
if self.shebang {
@ -494,11 +495,13 @@ impl<'a: 'b, 'b> AssignmentResolver<'a, 'b> {
fn evaluate_assignments<'a>(
assignments: &BTreeMap<&'a str, Expression<'a>>,
overrides: &BTreeMap<&str, &str>,
) -> Result<BTreeMap<&'a str, String>, RunError<'a>> {
let mut evaluator = Evaluator {
evaluated: BTreeMap::new(),
scope: &BTreeMap::new(),
assignments: assignments,
evaluated: BTreeMap::new(),
scope: &BTreeMap::new(),
assignments: assignments,
overrides: overrides,
};
for name in assignments.keys() {
@ -512,6 +515,7 @@ struct Evaluator<'a: 'b, 'b> {
evaluated: BTreeMap<&'a str, String>,
scope: &'b BTreeMap<&'a str, String>,
assignments: &'b BTreeMap<&'a str, Expression<'a>>,
overrides: &'b BTreeMap<&'b str, &'b str>,
}
impl<'a, 'b> Evaluator<'a, 'b> {
@ -538,8 +542,12 @@ impl<'a, 'b> Evaluator<'a, 'b> {
}
if let Some(expression) = self.assignments.get(name) {
let value = try!(self.evaluate_expression(expression, &BTreeMap::new()));
self.evaluated.insert(name, value);
if let Some(value) = self.overrides.get(name) {
self.evaluated.insert(name, value.to_string());
} else {
let value = try!(self.evaluate_expression(expression, &BTreeMap::new()));
self.evaluated.insert(name, value);
}
} else {
return Err(RunError::InternalError {
message: format!("attempted to evaluated unknown assignment {}", name)
@ -635,26 +643,41 @@ fn mixed_whitespace(text: &str) -> bool {
!(text.chars().all(|c| c == ' ') || text.chars().all(|c| c == '\t'))
}
struct Or<'a, T: 'a + Display>(&'a [T]);
struct And<'a, T: 'a + Display>(&'a [T]);
struct Or <'a, T: 'a + Display>(&'a [T]);
impl<'a, T: Display> Display for And<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
conjoin(f, self.0, "and")
}
}
impl<'a, T: Display> Display for Or<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self.0.len() {
conjoin(f, self.0, "or")
}
}
fn conjoin<T: Display>(
f: &mut fmt::Formatter,
values: &[T],
conjunction: &str,
) -> Result<(), fmt::Error> {
match values.len() {
0 => {},
1 => try!(write!(f, "{}", self.0[0])),
2 => try!(write!(f, "{} or {}", self.0[0], self.0[1])),
_ => for (i, item) in self.0.iter().enumerate() {
1 => try!(write!(f, "{}", values[0])),
2 => try!(write!(f, "{} {} {}", values[0], conjunction, values[1])),
_ => for (i, item) in values.iter().enumerate() {
try!(write!(f, "{}", item));
if i == self.0.len() - 1 {
} else if i == self.0.len() - 2 {
try!(write!(f, ", or "));
if i == values.len() - 1 {
} else if i == values.len() - 2 {
try!(write!(f, ", {} ", conjunction));
} else {
try!(write!(f, ", "))
}
},
}
Ok(())
}
}
impl<'a> Display for Error<'a> {
@ -783,8 +806,20 @@ impl<'a, 'b> Justfile<'a> where 'a: 'b {
self.recipes.keys().cloned().collect()
}
fn run(&'a self, arguments: &[&'a str]) -> Result<(), RunError<'a>> {
let scope = try!(evaluate_assignments(&self.assignments));
fn run(
&'a self,
overrides: &BTreeMap<&'a str, &'a str>,
arguments: &[&'a str]
) -> Result<(), RunError<'a>> {
let unknown_overrides = overrides.keys().cloned()
.filter(|name| !self.assignments.contains_key(name))
.collect::<Vec<_>>();
if !unknown_overrides.is_empty() {
return Err(RunError::UnknownOverrides{overrides: unknown_overrides});
}
let scope = try!(evaluate_assignments(&self.assignments, overrides));
let mut ran = HashSet::new();
for (i, argument) in arguments.iter().enumerate() {
@ -878,6 +913,7 @@ enum RunError<'a> {
TmpdirIoError{recipe: &'a str, io_error: io::Error},
UnknownFailure{recipe: &'a str},
UnknownRecipes{recipes: Vec<&'a str>},
UnknownOverrides{overrides: Vec<&'a str>},
BacktickCode{code: i32, token: Token<'a>},
BacktickIoError{io_error: io::Error},
BacktickSignal{signal: i32},
@ -895,6 +931,10 @@ impl<'a> Display for RunError<'a> {
try!(write!(f, "Justfile does not contain recipes: {}", recipes.join(" ")));
};
},
RunError::UnknownOverrides{ref overrides} => {
try!(write!(f, "{} set on the command line but not present in justfile",
And(overrides)))
},
RunError::NonLeadingRecipeWithArguments{recipe} => {
try!(write!(f, "Recipe `{}` takes arguments and so must be the first and only recipe specified on the command line", recipe));
},

View file

@ -1,8 +1,8 @@
extern crate tempdir;
use super::{Token, Error, ErrorKind, Justfile};
use super::{Token, Error, ErrorKind, Justfile, RunError};
use super::TokenKind::*;
use std::collections::BTreeMap;
fn tokenize_success(text: &str, expected_summary: &str) {
let tokens = super::tokenize(text).unwrap();
@ -562,18 +562,26 @@ fn mixed_leading_whitespace() {
}
#[test]
fn write_or() {
fn conjoin_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 conjoin_and() {
assert_eq!("1", super::And(&[1 ]).to_string());
assert_eq!("1 and 2", super::And(&[1,2 ]).to_string());
assert_eq!("1, 2, and 3", super::And(&[1,2,3 ]).to_string());
assert_eq!("1, 2, 3, and 4", super::And(&[1,2,3,4]).to_string());
}
#[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),
match parse_success("a:\nb:\nc:").run(&BTreeMap::new(), &["a", "x", "y", "z"]).unwrap_err() {
RunError::UnknownRecipes{recipes} => assert_eq!(recipes, &["x", "y", "z"]),
other => panic!("expected an unknown recipe error, but got: {}", other),
}
}
@ -739,8 +747,8 @@ a:
x
";
match parse_success(text).run(&["a"]).unwrap_err() {
super::RunError::Code{recipe, code} => {
match parse_success(text).run(&BTreeMap::new(), &["a"]).unwrap_err() {
RunError::Code{recipe, code} => {
assert_eq!(recipe, "a");
assert_eq!(code, 200);
},
@ -750,12 +758,12 @@ a:
#[test]
fn code_error() {
match parse_success("fail:\n @function x { return 100; }; x").run(&["fail"]).unwrap_err() {
super::RunError::Code{recipe, code} => {
match parse_success("fail:\n @function x { return 100; }; x").run(&BTreeMap::new(), &["fail"]).unwrap_err() {
RunError::Code{recipe, code} => {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
},
other @ _ => panic!("expected a code run error, but got: {}", other),
other => panic!("expected a code run error, but got: {}", other),
}
}
@ -765,8 +773,8 @@ fn run_args() {
a return code:
@function x { {{return}} {{code + "0"}}; }; x"#;
match parse_success(text).run(&["a", "return", "15"]).unwrap_err() {
super::RunError::Code{recipe, code} => {
match parse_success(text).run(&BTreeMap::new(), &["a", "return", "15"]).unwrap_err() {
RunError::Code{recipe, code} => {
assert_eq!(recipe, "a");
assert_eq!(code, 150);
},
@ -776,8 +784,8 @@ a return code:
#[test]
fn missing_args() {
match parse_success("a b c d:").run(&["a", "b", "c"]).unwrap_err() {
super::RunError::ArgumentCountMismatch{recipe, found, expected} => {
match parse_success("a b c d:").run(&BTreeMap::new(), &["a", "b", "c"]).unwrap_err() {
RunError::ArgumentCountMismatch{recipe, found, expected} => {
assert_eq!(recipe, "a");
assert_eq!(found, 2);
assert_eq!(expected, 3);
@ -788,8 +796,8 @@ fn missing_args() {
#[test]
fn missing_default() {
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}").run(&["a"]).unwrap_err() {
super::RunError::ArgumentCountMismatch{recipe, found, expected} => {
match parse_success("a b c d:\n echo {{b}}{{c}}{{d}}").run(&BTreeMap::new(), &["a"]).unwrap_err() {
RunError::ArgumentCountMismatch{recipe, found, expected} => {
assert_eq!(recipe, "a");
assert_eq!(found, 0);
assert_eq!(expected, 3);
@ -800,11 +808,25 @@ fn missing_default() {
#[test]
fn backtick_code() {
match parse_success("a:\n echo {{`function f { return 100; }; f`}}").run(&["a"]).unwrap_err() {
super::RunError::BacktickCode{code, token} => {
match parse_success("a:\n echo {{`function f { return 100; }; f`}}").run(&BTreeMap::new(), &["a"]).unwrap_err() {
RunError::BacktickCode{code, token} => {
assert_eq!(code, 100);
assert_eq!(token.lexeme, "`function f { return 100; }; f`");
},
other => panic!("expected an code run error, but got: {}", other),
}
}
#[test]
fn unknown_overrides() {
let mut overrides = BTreeMap::new();
overrides.insert("foo", "bar");
overrides.insert("baz", "bob");
match parse_success("a:\n echo {{`function f { return 100; }; f`}}")
.run(&overrides, &["a"]).unwrap_err() {
RunError::UnknownOverrides{overrides} => {
assert_eq!(overrides, &["baz", "foo"]);
},
other => panic!("expected an code run error, but got: {}", other),
}
}