Report line number in recipe failure messages (#125)

The grammar now permits blank lines in recipes.

Note that inside of recipes, the token `NEWLINE` is used instead of the
non-terminal `eol`. This is because an `eol` optionally includes a
comment, whereas inside recipes bodies comments get no special
treatment.
This commit is contained in:
Casey Rodarmor 2016-11-16 22:18:55 -08:00 committed by GitHub
parent 07634d9390
commit cef117f8bd
5 changed files with 65 additions and 28 deletions

View File

@ -61,7 +61,8 @@ dependencies : NAME+
body : INDENT line+ DEDENT body : INDENT line+ DEDENT
line : LINE (TEXT | interpolation)+ eol line : LINE (TEXT | interpolation)+ NEWLINE
| NEWLINE
interpolation : '{{' expression '}}' interpolation : '{{' expression '}}'
``` ```

View File

@ -46,6 +46,11 @@ push GITHUB-BRANCH:
git diff --no-ext-diff --quiet --exit-code git diff --no-ext-diff --quiet --exit-code
git push github master:refs/heads/{{GITHUB-BRANCH}} git push github master:refs/heads/{{GITHUB-BRANCH}}
push-f GITHUB-BRANCH:
git branch | grep '* master'
git diff --no-ext-diff --quiet --exit-code
git push github -f master:refs/heads/{{GITHUB-BRANCH}}
# install just from crates.io # install just from crates.io
install: install:
cargo install -f just cargo install -f just

View File

@ -283,14 +283,17 @@ recipe:
fn status_passthrough() { fn status_passthrough() {
let text = let text =
" "
hello:
recipe: recipe:
@exit 100"; @exit 100";
integration_test( integration_test(
&[], &["recipe"],
text, text,
100, 100,
"", "",
"error: Recipe `recipe` failed with exit code 100\n", "error: Recipe `recipe` failed on line 6 with exit code 100\n",
); );
} }
@ -1028,7 +1031,8 @@ recipe:
text, text,
100, 100,
"", "",
"\u{1b}[1;31merror:\u{1b}[0m \u{1b}[1mRecipe `recipe` failed with exit code 100\u{1b}[0m\n", "\u{1b}[1;31merror:\u{1b}[0m \u{1b}[1m\
Recipe `recipe` failed on line 3 with exit code 100\u{1b}[0m\n",
); );
} }

View File

@ -163,17 +163,25 @@ impl<'a> Display for Expression<'a> {
} }
#[cfg(unix)] #[cfg(unix)]
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError { fn error_from_signal(
recipe: &str,
line_number: Option<usize>,
exit_status: process::ExitStatus
) -> RunError {
use std::os::unix::process::ExitStatusExt; use std::os::unix::process::ExitStatusExt;
match exit_status.signal() { match exit_status.signal() {
Some(signal) => RunError::Signal{recipe: recipe, signal: signal}, Some(signal) => RunError::Signal{recipe: recipe, line_number: line_number, signal: signal},
None => RunError::UnknownFailure{recipe: recipe}, None => RunError::UnknownFailure{recipe: recipe, line_number: line_number},
} }
} }
#[cfg(windows)] #[cfg(windows)]
fn error_from_signal(recipe: &str, exit_status: process::ExitStatus) -> RunError { fn error_from_signal(
RunError::UnknownFailure{recipe: recipe} recipe: &str,
line_number: Option<usize>,
exit_status: process::ExitStatus
) -> RunError {
RunError::UnknownFailure{recipe: recipe, line_number: line_number}
} }
#[cfg(unix)] #[cfg(unix)]
@ -362,16 +370,17 @@ impl<'a> Recipe<'a> {
match command.status() { match command.status() {
Ok(exit_status) => if let Some(code) = exit_status.code() { Ok(exit_status) => if let Some(code) = exit_status.code() {
if code != 0 { if code != 0 {
return Err(RunError::Code{recipe: self.name, code: code}) return Err(RunError::Code{recipe: self.name, line_number: None, code: code})
} }
} else { } else {
return Err(error_from_signal(self.name, exit_status)) return Err(error_from_signal(self.name, None, exit_status))
}, },
Err(io_error) => return Err(RunError::TmpdirIoError{ Err(io_error) => return Err(RunError::TmpdirIoError{
recipe: self.name, io_error: io_error}) recipe: self.name, io_error: io_error})
}; };
} else { } else {
let mut lines = self.lines.iter().peekable(); let mut lines = self.lines.iter().peekable();
let mut line_number = self.line_number + 1;
loop { loop {
if lines.peek().is_none() { if lines.peek().is_none() {
break; break;
@ -382,6 +391,7 @@ impl<'a> Recipe<'a> {
break; break;
} }
let line = lines.next().unwrap(); let line = lines.next().unwrap();
line_number += 1;
evaluated += &evaluator.evaluate_line(line, &argument_map)?; evaluated += &evaluator.evaluate_line(line, &argument_map)?;
if line.last().map(Fragment::continuation).unwrap_or(false) { if line.last().map(Fragment::continuation).unwrap_or(false) {
evaluated.pop(); evaluated.pop();
@ -422,10 +432,12 @@ impl<'a> Recipe<'a> {
match cmd.status() { match cmd.status() {
Ok(exit_status) => if let Some(code) = exit_status.code() { Ok(exit_status) => if let Some(code) = exit_status.code() {
if code != 0 { if code != 0 {
return Err(RunError::Code{recipe: self.name, code: code}); return Err(RunError::Code{
recipe: self.name, line_number: Some(line_number), code: code
});
} }
} else { } else {
return Err(error_from_signal(self.name, exit_status)); return Err(error_from_signal(self.name, Some(line_number), exit_status));
}, },
Err(io_error) => return Err(RunError::IoError{ Err(io_error) => return Err(RunError::IoError{
recipe: self.name, io_error: io_error}), recipe: self.name, io_error: io_error}),
@ -1263,13 +1275,13 @@ impl<'a> Display for Justfile<'a> {
#[derive(Debug)] #[derive(Debug)]
enum RunError<'a> { enum RunError<'a> {
ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize}, ArgumentCountMismatch{recipe: &'a str, found: usize, min: usize, max: usize},
Code{recipe: &'a str, code: i32}, Code{recipe: &'a str, line_number: Option<usize>, code: i32},
InternalError{message: String}, InternalError{message: String},
IoError{recipe: &'a str, io_error: io::Error}, IoError{recipe: &'a str, io_error: io::Error},
NonLeadingRecipeWithParameters{recipe: &'a str}, NonLeadingRecipeWithParameters{recipe: &'a str},
Signal{recipe: &'a str, signal: i32}, Signal{recipe: &'a str, line_number: Option<usize>, signal: i32},
TmpdirIoError{recipe: &'a str, io_error: io::Error}, TmpdirIoError{recipe: &'a str, io_error: io::Error},
UnknownFailure{recipe: &'a str}, UnknownFailure{recipe: &'a str, line_number: Option<usize>},
UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>}, UnknownRecipes{recipes: Vec<&'a str>, suggestion: Option<&'a str>},
UnknownOverrides{overrides: Vec<&'a str>}, UnknownOverrides{overrides: Vec<&'a str>},
BacktickCode{token: Token<'a>, code: i32}, BacktickCode{token: Token<'a>, code: i32},
@ -1319,14 +1331,25 @@ impl<'a> Display for RunError<'a> {
recipe, found, maybe_s(found), max)?; recipe, found, maybe_s(found), max)?;
} }
}, },
Code{recipe, code} => { Code{recipe, line_number, code} => {
if let Some(n) = line_number {
write!(f, "Recipe `{}` failed on line {} with exit code {}", recipe, n, code)?;
} else {
write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?; write!(f, "Recipe `{}` failed with exit code {}", recipe, code)?;
}
}, },
Signal{recipe, signal} => { Signal{recipe, line_number, signal} => {
if let Some(n) = line_number {
write!(f, "Recipe `{}` was terminated on line {} by signal {}", recipe, n, signal)?;
} else {
write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?; write!(f, "Recipe `{}` was terminated by signal {}", recipe, signal)?;
} }
UnknownFailure{recipe} => { }
write!(f, "Recipe `{}` failed for an unknown reason", recipe)?; UnknownFailure{recipe, line_number} => {
if let Some(n) = line_number {
write!(f, "Recipe `{}` failed on line {} for an unknown reason", recipe, n)?;
} else {
}
}, },
IoError{recipe, ref io_error} => { IoError{recipe, ref io_error} => {
match io_error.kind() { match io_error.kind() {

View File

@ -862,9 +862,10 @@ a:
"; ";
match parse_success(text).run(&["a"], &Default::default()).unwrap_err() { match parse_success(text).run(&["a"], &Default::default()).unwrap_err() {
RunError::Code{recipe, code} => { RunError::Code{recipe, line_number, code} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 200); assert_eq!(code, 200);
assert_eq!(line_number, None);
}, },
other => panic!("expected an code run error, but got: {}", other), other => panic!("expected an code run error, but got: {}", other),
} }
@ -874,9 +875,10 @@ a:
fn code_error() { fn code_error() {
match parse_success("fail:\n @exit 100") match parse_success("fail:\n @exit 100")
.run(&["fail"], &Default::default()).unwrap_err() { .run(&["fail"], &Default::default()).unwrap_err() {
RunError::Code{recipe, code} => { RunError::Code{recipe, line_number, code} => {
assert_eq!(recipe, "fail"); assert_eq!(recipe, "fail");
assert_eq!(code, 100); assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
}, },
other => panic!("expected a code run error, but got: {}", other), other => panic!("expected a code run error, but got: {}", other),
} }
@ -889,9 +891,10 @@ a return code:
@x() { {{return}} {{code + "0"}}; }; x"#; @x() { {{return}} {{code + "0"}}; }; x"#;
match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() { match parse_success(text).run(&["a", "return", "15"], &Default::default()).unwrap_err() {
RunError::Code{recipe, code} => { RunError::Code{recipe, line_number, code} => {
assert_eq!(recipe, "a"); assert_eq!(recipe, "a");
assert_eq!(code, 150); assert_eq!(code, 150);
assert_eq!(line_number, Some(3));
}, },
other => panic!("expected an code run error, but got: {}", other), other => panic!("expected an code run error, but got: {}", other),
} }
@ -1017,8 +1020,9 @@ wut:
}; };
match parse_success(text).run(&["wut"], &options).unwrap_err() { match parse_success(text).run(&["wut"], &options).unwrap_err() {
RunError::Code{code: _, recipe} => { RunError::Code{code: _, line_number, recipe} => {
assert_eq!(recipe, "wut"); assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(8));
}, },
other => panic!("expected a recipe code errror, but got: {}", other), other => panic!("expected a recipe code errror, but got: {}", other),
} }