Add [no-exit-message] recipe annotation (#1354)

When a recipe wraps cli tool and the tool exits with a non-zero code,
just adds its own extra exit error message along with the messages
from the tool. Introduce the `[no-exit-message]` attribute to suppress
this additional message.
This commit is contained in:
Gökhan Karabulut 2022-10-26 02:32:36 +03:00 committed by GitHub
parent dc9f458937
commit 8b7640b633
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 326 additions and 21 deletions

View File

@ -94,7 +94,9 @@ string : STRING
sequence : expression ',' sequence
| expression ','?
recipe : '@'? NAME parameter* variadic? ':' dependency* body?
recipe : attribute? '@'? NAME parameter* variadic? ':' dependency* body?
attribute : '[' NAME ']' eol
parameter : '$'? NAME
| '$'? NAME '=' value

View File

@ -1987,6 +1987,35 @@ echo 'Bar!'
Bar!
```
`just` normally prints error messages when a recipe line fails. These error
messages can be suppressed using the `[no-exit-message]` attribute. You may find
this especially useful with a recipe that recipe wraps a tool:
```make
git *args:
@git {{args}}
```
```sh
$ just git status
fatal: not a git repository (or any of the parent directories): .git
error: Recipe `git` failed on line 2 with exit code 128
```
Add the attribute to suppress the exit error message when the tool exits with a
non-zero code:
```make
[no-exit-message]
git *args:
@git {{args}}
```
```sh
$ just git status
fatal: not a git repository (or any of the parent directories): .git
```
### Selecting Recipes to Run With an Interactive Chooser
The `--choose` subcommand makes `just` invoke a chooser to select which recipes to run. Choosers should read lines containing recipe names from standard input and print one or more of those names separated by spaces to standard output.

13
src/attribute.rs Normal file
View File

@ -0,0 +1,13 @@
use super::*;
#[derive(EnumString)]
#[strum(serialize_all = "kebab_case")]
pub(crate) enum Attribute {
NoExitMessage,
}
impl Attribute {
pub(crate) fn from_name(name: Name) -> Option<Attribute> {
name.lexeme().parse().ok()
}
}

View File

@ -247,6 +247,9 @@ impl Display for CompileError<'_> {
UnknownAliasTarget { alias, target } => {
write!(f, "Alias `{}` has an unknown target `{}`", alias, target)?;
}
UnknownAttribute { attribute } => {
write!(f, "Unknown attribute `{}`", attribute)?;
}
UnknownDependency { recipe, unknown } => {
write!(
f,

View File

@ -98,6 +98,9 @@ pub(crate) enum CompileErrorKind<'src> {
alias: &'src str,
target: &'src str,
},
UnknownAttribute {
attribute: &'src str,
},
UnknownDependency {
recipe: &'src str,
unknown: &'src str,

View File

@ -35,6 +35,7 @@ pub(crate) enum Error<'src> {
recipe: &'src str,
line_number: Option<usize>,
code: i32,
suppress_message: bool,
},
CommandInvoke {
binary: OsString,
@ -167,6 +168,16 @@ impl<'src> Error<'src> {
message: message.into(),
}
}
pub(crate) fn suppress_message(&self) -> bool {
matches!(
self,
Error::Code {
suppress_message: true,
..
}
)
}
}
impl<'src> From<CompileError<'src>> for Error<'src> {
@ -323,6 +334,7 @@ impl<'src> ColorDisplay for Error<'src> {
recipe,
line_number,
code,
..
} => {
if let Some(n) = line_number {
write!(

View File

@ -472,11 +472,13 @@ mod tests {
recipe,
line_number,
code,
suppress_message,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 200);
assert_eq!(line_number, None);
assert!(!suppress_message);
}
}
@ -491,11 +493,13 @@ mod tests {
recipe,
line_number,
code,
suppress_message,
},
check: {
assert_eq!(recipe, "fail");
assert_eq!(code, 100);
assert_eq!(line_number, Some(2));
assert!(!suppress_message);
}
}
@ -510,11 +514,13 @@ mod tests {
recipe,
line_number,
code,
suppress_message,
},
check: {
assert_eq!(recipe, "a");
assert_eq!(code, 150);
assert_eq!(line_number, Some(2));
assert!(!suppress_message);
}
}
@ -664,13 +670,15 @@ mod tests {
"#,
args: ["--quiet", "wut"],
error: Code {
line_number,
recipe,
line_number,
suppress_message,
..
},
check: {
assert_eq!(recipe, "wut");
assert_eq!(line_number, Some(7));
assert!(!suppress_message);
}
}

View File

@ -16,19 +16,19 @@
pub(crate) use {
crate::{
alias::Alias, analyzer::Analyzer, assignment::Assignment,
assignment_resolver::AssignmentResolver, ast::Ast, binding::Binding, color::Color,
color_display::ColorDisplay, command_ext::CommandExt, compile_error::CompileError,
compile_error_kind::CompileErrorKind, conditional_operator::ConditionalOperator,
config::Config, config_error::ConfigError, count::Count, delimiter::Delimiter,
dependency::Dependency, dump_format::DumpFormat, enclosure::Enclosure, error::Error,
evaluator::Evaluator, expression::Expression, fragment::Fragment, function::Function,
function_context::FunctionContext, interrupt_guard::InterruptGuard,
interrupt_handler::InterruptHandler, item::Item, justfile::Justfile, keyed::Keyed,
keyword::Keyword, lexer::Lexer, line::Line, list::List, load_dotenv::load_dotenv,
loader::Loader, name::Name, ordinal::Ordinal, output::output, output_error::OutputError,
parameter::Parameter, parameter_kind::ParameterKind, parser::Parser, platform::Platform,
platform_interface::PlatformInterface, position::Position, positional::Positional,
range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
assignment_resolver::AssignmentResolver, ast::Ast, attribute::Attribute, binding::Binding,
color::Color, color_display::ColorDisplay, command_ext::CommandExt,
compile_error::CompileError, compile_error_kind::CompileErrorKind,
conditional_operator::ConditionalOperator, config::Config, config_error::ConfigError,
count::Count, delimiter::Delimiter, dependency::Dependency, dump_format::DumpFormat,
enclosure::Enclosure, error::Error, evaluator::Evaluator, expression::Expression,
fragment::Fragment, function::Function, function_context::FunctionContext,
interrupt_guard::InterruptGuard, interrupt_handler::InterruptHandler, item::Item,
justfile::Justfile, keyed::Keyed, keyword::Keyword, lexer::Lexer, line::Line, list::List,
load_dotenv::load_dotenv, loader::Loader, name::Name, ordinal::Ordinal, output::output,
output_error::OutputError, parameter::Parameter, parameter_kind::ParameterKind, parser::Parser,
platform::Platform, platform_interface::PlatformInterface, position::Position,
positional::Positional, range_ext::RangeExt, recipe::Recipe, recipe_context::RecipeContext,
recipe_resolver::RecipeResolver, scope::Scope, search::Search, search_config::SearchConfig,
search_error::SearchError, set::Set, setting::Setting, settings::Settings, shebang::Shebang,
shell::Shell, show_whitespace::ShowWhitespace, string_kind::StringKind,
@ -116,6 +116,7 @@ mod analyzer;
mod assignment;
mod assignment_resolver;
mod ast;
mod attribute;
mod binding;
mod color;
mod color_display;

View File

@ -345,13 +345,20 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
items.push(Item::Assignment(self.parse_assignment(false)?));
} else {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, false)?));
items.push(Item::Recipe(self.parse_recipe(doc, false, false)?));
}
}
}
} else if self.accepted(At)? {
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, true)?));
items.push(Item::Recipe(self.parse_recipe(doc, true, false)?));
} else if self.accepted(BracketL)? {
let Attribute::NoExitMessage = self.parse_attribute_name()?;
self.expect(BracketR)?;
self.expect_eol()?;
let quiet = self.accepted(At)?;
let doc = pop_doc_comment(&mut items, eol_since_last_comment);
items.push(Item::Recipe(self.parse_recipe(doc, quiet, true)?));
} else {
return Err(self.unexpected_token()?);
}
@ -595,6 +602,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
&mut self,
doc: Option<&'src str>,
quiet: bool,
suppress_exit_error_messages: bool,
) -> CompileResult<'src, UnresolvedRecipe<'src>> {
let name = self.parse_name()?;
@ -658,6 +666,7 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
parameters: positional.into_iter().chain(variadic).collect(),
private: name.lexeme().starts_with('_'),
shebang: body.first().map_or(false, Line::is_shebang),
suppress_exit_error_messages,
priors,
body,
dependencies,
@ -816,6 +825,16 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
Ok(Shell { arguments, command })
}
/// Parse a recipe attribute name
fn parse_attribute_name(&mut self) -> CompileResult<'src, Attribute> {
let name = self.parse_name()?;
Attribute::from_name(name).ok_or_else(|| {
name.error(CompileErrorKind::UnknownAttribute {
attribute: name.lexeme(),
})
})
}
}
#[cfg(test)]
@ -1966,7 +1985,7 @@ mod tests {
column: 0,
width: 1,
kind: UnexpectedToken {
expected: vec![At, Comment, Eof, Eol, Identifier],
expected: vec![At, BracketL, Comment, Eof, Eol, Identifier],
found: BraceL,
},
}
@ -2144,6 +2163,29 @@ mod tests {
},
}
error! {
name: empty_attribute,
input: "[]\nsome_recipe:\n @exit 3",
offset: 1,
line: 0,
column: 1,
width: 1,
kind: UnexpectedToken {
expected: vec![Identifier],
found: BracketR,
},
}
error! {
name: unknown_attribute,
input: "[unknown]\nsome_recipe:\n @exit 3",
offset: 1,
line: 0,
column: 1,
width: 7,
kind: UnknownAttribute { attribute: "unknown" },
}
error! {
name: set_unknown,
input: "set shall := []",

View File

@ -30,6 +30,7 @@ pub(crate) struct Recipe<'src, D = Dependency<'src>> {
pub(crate) private: bool,
pub(crate) quiet: bool,
pub(crate) shebang: bool,
pub(crate) suppress_exit_error_messages: bool,
}
impl<'src, D> Recipe<'src, D> {
@ -191,10 +192,12 @@ impl<'src, D> Recipe<'src, D> {
Ok(exit_status) => {
if let Some(code) = exit_status.code() {
if code != 0 && !infallible_command {
let suppress_message = self.suppress_exit_error_messages;
return Err(Error::Code {
recipe: self.name(),
line_number: Some(line_number),
code,
suppress_message,
});
}
} else {
@ -322,10 +325,12 @@ impl<'src, D> Recipe<'src, D> {
if code == 0 {
Ok(())
} else {
let suppress_message = self.suppress_exit_error_messages;
Err(Error::Code {
recipe: self.name(),
line_number: None,
code,
suppress_message,
})
}
},

View File

@ -29,7 +29,7 @@ pub fn run() -> Result<(), i32> {
config
.and_then(|config| config.run(&loader))
.map_err(|error| {
if !verbosity.quiet() {
if !verbosity.quiet() && !error.suppress_message() {
eprintln!("{}", error.color_display(color.stderr()));
}
error.code().unwrap_or(EXIT_FAILURE)

View File

@ -53,6 +53,7 @@ impl<'src> UnresolvedRecipe<'src> {
quiet: self.quiet,
shebang: self.shebang,
priors: self.priors,
suppress_exit_error_messages: self.suppress_exit_error_messages,
dependencies,
})
}

View File

@ -26,7 +26,7 @@ fn non_leading_byte_order_mark_produces_error() {
)
.stderr(
"
error: Expected \'@\', comment, end of file, end of line, or identifier, but found byte order mark
error: Expected \'@\', \'[\', comment, end of file, end of line, or identifier, but found byte order mark
|
3 | \u{feff}
| ^
@ -41,7 +41,7 @@ fn dont_mention_byte_order_mark_in_errors() {
.justfile("{")
.stderr(
"
error: Expected '@', comment, end of file, end of line, or identifier, but found '{'
error: Expected '@', '[', comment, end of file, end of line, or identifier, but found '{'
|
1 | {
| ^

View File

@ -36,6 +36,7 @@ fn alias() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -111,6 +112,7 @@ fn body() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -154,6 +156,7 @@ fn dependencies() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"foo": {
"body": [],
@ -165,6 +168,7 @@ fn dependencies() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -238,6 +242,7 @@ fn dependency_argument() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"foo": {
"body": [],
@ -256,6 +261,7 @@ fn dependency_argument() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -311,6 +317,7 @@ fn duplicate_recipes() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -348,6 +355,7 @@ fn doc_comment() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -417,6 +425,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"b": {
"body": [],
@ -435,6 +444,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"c": {
"body": [],
@ -453,6 +463,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"d": {
"body": [],
@ -471,6 +482,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"e": {
"body": [],
@ -489,6 +501,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"f": {
"body": [],
@ -507,6 +520,7 @@ fn parameters() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
},
"settings": {
@ -548,6 +562,7 @@ fn priors() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
},
"b": {
"body": [],
@ -566,6 +581,7 @@ fn priors() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
"parameters": [],
"priors": 1,
},
@ -578,6 +594,7 @@ fn priors() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
"parameters": [],
"priors": 0,
},
@ -617,6 +634,7 @@ fn private() {
"private": true,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -654,6 +672,7 @@ fn quiet() {
"private": false,
"quiet": true,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -711,6 +730,7 @@ fn settings() {
"private": false,
"quiet": false,
"shebang": true,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -754,6 +774,7 @@ fn shebang() {
"private": false,
"quiet": false,
"shebang": true,
"suppress_exit_error_messages": false,
}
},
"settings": {
@ -791,6 +812,48 @@ fn simple() {
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": false,
}
},
"settings": {
"allow_duplicate_recipes": false,
"dotenv_load": null,
"export": false,
"fallback": false,
"ignore_comments": false,
"positional_arguments": false,
"shell": null,
"windows_powershell": false,
"windows_shell": null,
},
"warnings": [],
}),
);
}
#[test]
fn attribute() {
test(
"
[no-exit-message]
foo:
",
json!({
"aliases": {},
"assignments": {},
"first": "foo",
"recipes": {
"foo": {
"body": [],
"dependencies": [],
"doc": null,
"name": "foo",
"parameters": [],
"priors": 0,
"private": false,
"quiet": false,
"shebang": false,
"suppress_exit_error_messages": true,
}
},
"settings": {

View File

@ -59,6 +59,7 @@ mod json;
mod line_prefixes;
mod misc;
mod multibyte_char;
mod no_exit_message;
mod parser;
mod positional_arguments;
mod quiet;

122
tests/no_exit_message.rs Normal file
View File

@ -0,0 +1,122 @@
use libc::EXIT_FAILURE;
test! {
name: recipe_exit_message_suppressed,
justfile: r#"
# This is a doc comment
[no-exit-message]
hello:
@echo "Hello, World!"
@exit 100
"#,
stdout: "Hello, World!\n",
stderr: "",
status: 100,
}
test! {
name: silent_recipe_exit_message_suppressed,
justfile: r#"
# This is a doc comment
[no-exit-message]
@hello:
echo "Hello, World!"
exit 100
"#,
stdout: "Hello, World!\n",
stderr: "",
status: 100,
}
test! {
name: recipe_has_doc_comment,
justfile: r#"
# This is a doc comment
[no-exit-message]
hello:
@exit 100
"#,
args: ("--list"),
stdout: "
Available recipes:
hello # This is a doc comment
",
}
test! {
name: unknown_attribute,
justfile: r#"
# This is a doc comment
[unknown-attribute]
hello:
@exit 100
"#,
stderr: r#"
error: Unknown attribute `unknown-attribute`
|
2 | [unknown-attribute]
| ^^^^^^^^^^^^^^^^^
"#,
status: EXIT_FAILURE,
}
test! {
name: empty_attribute,
justfile: r#"
# This is a doc comment
[]
hello:
@exit 100
"#,
stderr: r#"
error: Expected identifier, but found ']'
|
2 | []
| ^
"#,
status: EXIT_FAILURE,
}
test! {
name: unattached_attribute_before_comment,
justfile: r#"
[no-exit-message]
# This is a doc comment
hello:
@exit 100
"#,
stderr: r#"
error: Expected '@' or identifier, but found comment
|
2 | # This is a doc comment
| ^^^^^^^^^^^^^^^^^^^^^^^
"#,
status: EXIT_FAILURE,
}
test! {
name: unattached_attribute_before_empty_line,
justfile: r#"
[no-exit-message]
hello:
@exit 100
"#,
stderr: "error: Expected '@' or identifier, but found end of line\n |\n2 | \n | ^\n",
status: EXIT_FAILURE,
}
test! {
name: shebang_exit_message_suppressed,
justfile: r#"
[no-exit-message]
hello:
#!/usr/bin/env bash
echo 'Hello, World!'
exit 100
"#,
stdout: "Hello, World!\n",
stderr: "",
status: 100,
}