diff --git a/justfile b/justfile index b31cb9f..150cbe5 100644 --- a/justfile +++ b/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 diff --git a/notes b/notes index e9f6178..83789de 100644 --- a/notes +++ b/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 diff --git a/src/app.rs b/src/app.rs index b9d1580..10d39d9 100644 --- a/src/app.rs +++ b/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 ")) + .arg(Arg::with_name("set") + .long("set") + .takes_value(true) + .number_of_values(2) + .value_names(&["variable", "value"]) + .multiple(true) + .help("set to ")) .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::>() + 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), diff --git a/src/integration.rs b/src/integration.rs index efa0ee5..027a451 100644 --- a/src/integration.rs +++ b/src/integration.rs @@ -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", + ); +} diff --git a/src/lib.rs b/src/lib.rs index 781fb7f..6b8330d 100644 --- a/src/lib.rs +++ b/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, 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( + 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::>(); + + 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)); }, diff --git a/src/unit.rs b/src/unit.rs index dbaf8a5..0f11795 100644 --- a/src/unit.rs +++ b/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), + } +}