variable=value overrides done
This commit is contained in:
parent
25c6432fa3
commit
9a368fb351
2
justfile
2
justfile
@ -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
15
notes
@ -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
|
||||
|
35
src/app.rs
35
src/app.rs
@ -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),
|
||||
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
72
src/lib.rs
72
src/lib.rs
@ -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));
|
||||
},
|
||||
|
60
src/unit.rs
60
src/unit.rs
@ -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),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user