From fa25c846c7fcf75bb582352cb95ad9c64289c9be Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 6 Oct 2016 17:43:30 -0700 Subject: [PATCH] More validation of recipes, allow leading shebang --- notes | 25 +++++++++++------- src/lib.rs | 73 +++++++++++++++++++++++++++++++++++++--------------- src/tests.rs | 16 ++++++++---- 3 files changed, 78 insertions(+), 36 deletions(-) diff --git a/notes b/notes index 5c4724d..2475a3c 100644 --- a/notes +++ b/notes @@ -1,24 +1,29 @@ notes ----- +polyglot: +- recipes can have shebangs on first line +- complain if there is extra leading whitespace in a non-shebang recipe +- extract and run script +- preserve line numbers + +- change name to "a polyglot command runner" - comment code - fix docs (note that shell is invoked with -cu) - publish to github and cargo +- spam facebook, reddit -polyglot: -- recipes can have shebangs -- extract and run script -- preserve line numbers -- special 'prelude recipe" - . allow launching binaries from cargo - . script until -- - . all recipes are then in that language? - -extras: +wishlist: +- preludes: + may be nice to allow all recipes in a given langauge to share + functions, variables, etc. could have a "prelude" recipe + which was included as a prefix to other recipes +- windows support: currently calling 'sh', which won't work on windows - args can be passed after --, or with some special syntax: a: 1 2 3 : - should also add an annotation for recipes a FOO BAR, export variables FOO and BAR with args + fail if doesn't get two arguments - indent for line continuation - use launch recipes asyncronously - ~/.justfile: diff --git a/src/lib.rs b/src/lib.rs index 3afe90c..a316854 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,21 +42,23 @@ fn re(pattern: &str) -> Regex { } pub struct Recipe<'a> { - line: usize, + line_number: usize, + label: &'a str, name: &'a str, leading_whitespace: &'a str, - commands: Vec<&'a str>, + lines: Vec<&'a str>, dependencies: BTreeSet<&'a str>, + shebang: bool, } impl<'a> Display for Recipe<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - try!(writeln!(f, "{}:", self.name)); - for (i, command) in self.commands.iter().enumerate() { - if i + 1 < self.commands.len() { - try!(writeln!(f, " {}", command)); + try!(writeln!(f, "{}", self.label)); + for (i, line) in self.lines.iter().enumerate() { + if i + 1 < self.lines.len() { + try!(writeln!(f, " {}", line)); } { - try!(write!(f, " {}", command)); + try!(write!(f, " {}", line)); } } Ok(()) @@ -79,7 +81,8 @@ fn error_from_signal<'a>(recipe: &'a str, exit_status: process::ExitStatus) -> R impl<'a> Recipe<'a> { fn run(&self) -> Result<(), RunError<'a>> { - for command in &self.commands { + // TODO: if shebang, run as script + for command in &self.lines { let mut command = *command; if !command.starts_with("@") { warn!("{}", command); @@ -126,7 +129,7 @@ fn resolve<'a>( if seen.contains(dependency.name) { let first = stack[0]; stack.push(first); - return Err(error(text, recipe.line, ErrorKind::CircularDependency { + return Err(error(text, recipe.line_number, ErrorKind::CircularDependency { circle: stack.iter() .skip_while(|name| **name != dependency.name) .cloned().collect() @@ -134,7 +137,7 @@ fn resolve<'a>( } return resolve(text, recipes, resolved, seen, stack, dependency); }, - None => return Err(error(text, recipe.line, ErrorKind::UnknownDependency { + None => return Err(error(text, recipe.line_number, ErrorKind::UnknownDependency { name: recipe.name, unknown: dependency_name })), @@ -160,8 +163,10 @@ enum ErrorKind<'a> { DuplicateRecipe{first: usize, name: &'a str}, TabAfterSpace{whitespace: &'a str}, MixedLeadingWhitespace{whitespace: &'a str}, + ExtraLeadingWhitespace, InconsistentLeadingWhitespace{expected: &'a str, found: &'a str}, - Shebang, + OuterShebang, + NonLeadingShebang{recipe: &'a str}, UnknownDependency{name: &'a str, unknown: &'a str}, Unparsable, UnparsableDependencies, @@ -228,14 +233,20 @@ impl<'a> Display for Error<'a> { show_whitespace(whitespace) )); } + ErrorKind::ExtraLeadingWhitespace => { + try!(writeln!(f, "line has extra leading whitespace")); + } ErrorKind::InconsistentLeadingWhitespace{expected, found} => { try!(writeln!(f, "inconsistant leading whitespace: recipe started with {} but found line with {}:", show_whitespace(expected), show_whitespace(found) )); } - ErrorKind::Shebang => { - try!(writeln!(f, "shebang \"#!\" is reserved syntax")) + ErrorKind::OuterShebang => { + try!(writeln!(f, "a shebang \"#!\" is reserved syntax outside of recipes")) + } + ErrorKind::NonLeadingShebang{..} => { + try!(writeln!(f, "a shebang \"#!\" may only appear on the first line of a recipe")) } ErrorKind::UnknownDependency{name, unknown} => { try!(writeln!(f, "recipe {} has unknown dependency {}", name, unknown)); @@ -266,7 +277,7 @@ impl<'a> Justfile<'a> { let mut first: Option<&Recipe<'a>> = None; for (_, recipe) in self.recipes.iter() { if let Some(first_recipe) = first { - if recipe.line < first_recipe.line { + if recipe.line_number < first_recipe.line_number { first = Some(recipe) } } else { @@ -374,8 +385,6 @@ pub fn parse<'a>(text: &'a str) -> Result { for (i, line) in text.lines().enumerate() { if blank_re.is_match(line) { continue; - } else if shebang_re.is_match(line) { - return Err(error(text, i, ErrorKind::Shebang)); } if let Some(mut recipe) = current_recipe { @@ -399,7 +408,7 @@ pub fn parse<'a>(text: &'a str) -> Result { found: leading_whitespace, })); } - recipe.commands.push(line.split_at(recipe.leading_whitespace.len()).1); + recipe.lines.push(line.split_at(recipe.leading_whitespace.len()).1); current_recipe = Some(recipe); continue; }, @@ -412,6 +421,8 @@ pub fn parse<'a>(text: &'a str) -> Result { if comment_re.is_match(line) { // ignore + } else if shebang_re.is_match(line) { + return Err(error(text, i, ErrorKind::OuterShebang)); } else if let Some(captures) = label_re.captures(line) { let name = captures.at(1).unwrap(); if !name_re.is_match(name) { @@ -421,7 +432,7 @@ pub fn parse<'a>(text: &'a str) -> Result { } if let Some(recipe) = recipes.get(name) { return Err(error(text, i, ErrorKind::DuplicateRecipe { - first: recipe.line, + first: recipe.line_number, name: name, })); } @@ -442,11 +453,13 @@ pub fn parse<'a>(text: &'a str) -> Result { } current_recipe = Some(Recipe{ - line: i, + line_number: i, + label: line, name: name, leading_whitespace: "", - commands: vec![], - dependencies: dependencies, + lines: vec![], + dependencies: dependencies, + shebang: false, }); } else { return Err(error(text, i, ErrorKind::Unparsable)); @@ -457,6 +470,24 @@ pub fn parse<'a>(text: &'a str) -> Result { recipes.insert(recipe.name, recipe); } + let leading_whitespace_re = re(r"^\s+"); + + for recipe in recipes.values_mut() { + for (i, line) in recipe.lines.iter().enumerate() { + let line_number = recipe.line_number + 1 + i; + if shebang_re.is_match(line) { + if i == 0 { + recipe.shebang = true; + } else { + return Err(error(text, line_number, ErrorKind::NonLeadingShebang{recipe: recipe.name})); + } + } + if !recipe.shebang && leading_whitespace_re.is_match(line) { + return Err(error(text, line_number, ErrorKind::ExtraLeadingWhitespace)); + } + } + } + let mut resolved = HashSet::new(); let mut seen = HashSet::new(); let mut stack = vec![]; diff --git a/src/tests.rs b/src/tests.rs index fd3602f..6b5c542 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -22,7 +22,7 @@ fn check_recipe( name: &str, line: usize, leading_whitespace: &str, - commands: &[&str], + lines: &[&str], dependencies: &[&str] ) { let recipe = match justfile.recipes.get(name) { @@ -30,9 +30,9 @@ fn check_recipe( None => panic!("Justfile had no recipe \"{}\"", name), }; assert_eq!(recipe.name, name); - assert_eq!(recipe.line, line); + assert_eq!(recipe.line_number, line); assert_eq!(recipe.leading_whitespace, leading_whitespace); - assert_eq!(recipe.commands, commands); + assert_eq!(recipe.lines, lines); assert_eq!(recipe.dependencies.iter().cloned().collect::>(), dependencies); } @@ -87,8 +87,8 @@ fn inconsistent_leading_whitespace() { #[test] fn shebang() { - expect_error("#!/bin/sh", 0, ErrorKind::Shebang); - expect_error("a:\n #!/bin/sh", 1, ErrorKind::Shebang); + expect_error("#!/bin/sh", 0, ErrorKind::OuterShebang); + expect_error("a:\n echo hello\n #!/bin/sh", 2, ErrorKind::NonLeadingShebang{recipe:"a"}); } #[test] @@ -96,6 +96,12 @@ fn unknown_dependency() { expect_error("a: b", 0, ErrorKind::UnknownDependency{name: "a", unknown: "b"}); } +#[test] +fn extra_whitespace() { + expect_error("a:\n blah\n blarg", 2, ErrorKind::ExtraLeadingWhitespace); + expect_success("a:\n #!\n print(1)"); +} + #[test] fn unparsable() { expect_error("hello", 0, ErrorKind::Unparsable);