mirror of
https://github.com/casey/just
synced 2024-09-06 00:39:13 +00:00
More validation of recipes, allow leading shebang
This commit is contained in:
parent
d503b37fb3
commit
fa25c846c7
25
notes
25
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:
|
||||
|
|
73
src/lib.rs
73
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<Justfile, Error> {
|
|||
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<Justfile, Error> {
|
|||
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<Justfile, Error> {
|
|||
|
||||
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<Justfile, Error> {
|
|||
}
|
||||
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<Justfile, Error> {
|
|||
}
|
||||
|
||||
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<Justfile, Error> {
|
|||
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![];
|
||||
|
|
16
src/tests.rs
16
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::<Vec<_>>(), 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);
|
||||
|
|
Loading…
Reference in a new issue