From 7e5ad64b52431e4d7d50450e134bdecbeae95708 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 22 Dec 2024 21:54:04 -0800 Subject: [PATCH 1/2] Add `?` sigil --- README.md | 52 +++++++++++++++++++++++++++++++++++++++---------- src/keyword.rs | 1 + src/lexer.rs | 2 +- src/lib.rs | 2 ++ src/line.rs | 33 +++++++++++++++++++------------ src/node.rs | 1 + src/parser.rs | 5 +++-- src/recipe.rs | 37 ++++++++++++++++++----------------- src/setting.rs | 2 ++ src/settings.rs | 4 ++++ src/sigil.rs | 6 ++++++ tests/guards.rs | 32 ++++++++++++++++++++++++++++++ tests/json.rs | 1 + tests/lib.rs | 1 + 14 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 src/sigil.rs create mode 100644 tests/guards.rs diff --git a/README.md b/README.md index a8c1559817..8a58913dde 100644 --- a/README.md +++ b/README.md @@ -1542,24 +1542,56 @@ exported `just` variables cannot be used. However, this allows shell expanded strings to be used in places like settings and import paths, which cannot depend on `just` variables and `.env` files. -### Ignoring Errors +### Sigils -Normally, if a command returns a non-zero exit status, execution will stop. To -continue execution after a command, even if it fails, prefix the command with -`-`: +Commands in linewise recipes may be prefixed with any combination of sigils +`-`, `@`, and `?`. + +The `@` sigil toggles command echoing: ```just foo: - -cat foo - echo 'Done!' + @echo "This line won't be echoed!" + echo "This line will be echoed!" + +@bar: + @echo "This line will be echoed!" + echo "This line won't be echoed!" +``` + +The `-` sigil cause recipe execution to continue even if the command returns a +nonzero exit status: + +```just +# execution will continue, even if bar doesn't exist +foo: + -rmdir bar +``` + +The `?` sigilmaster causes the current recipe to stop executing if +the command returns a nonzero exit status, but execution of other recipes will +continue. + +If the `guards` settings is unset or false, `?` sigils are ignored. + +```just +set guards + +@foo: bar + echo FOO + +@bar: + ?[[ -f baz ]] + echo BAR ``` ```console $ just foo -cat foo -cat: foo: No such file or directory -echo 'Done!' -Done! +FOO +$ touch baz +$ just foo +BAR +FOO ``` ### Functions diff --git a/src/keyword.rs b/src/keyword.rs index ff06c39d27..e146f62a45 100644 --- a/src/keyword.rs +++ b/src/keyword.rs @@ -15,6 +15,7 @@ pub(crate) enum Keyword { Export, Fallback, False, + Guards, If, IgnoreComments, Import, diff --git a/src/lexer.rs b/src/lexer.rs index afef81c21e..93bad7e096 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -259,7 +259,7 @@ impl<'src> Lexer<'src> { /// True if `text` could be an identifier pub(crate) fn is_identifier(text: &str) -> bool { - if !text.chars().next().map_or(false, Self::is_identifier_start) { + if !text.chars().next().is_some_and(Self::is_identifier_start) { return false; } diff --git a/src/lib.rs b/src/lib.rs index 30777e513d..5f765a2f86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ pub(crate) use { settings::Settings, shebang::Shebang, show_whitespace::ShowWhitespace, + sigil::Sigil, source::Source, string_delimiter::StringDelimiter, string_kind::StringKind, @@ -255,6 +256,7 @@ mod setting; mod settings; mod shebang; mod show_whitespace; +mod sigil; mod source; mod string_delimiter; mod string_kind; diff --git a/src/line.rs b/src/line.rs index 8a58604676..5ad8532984 100644 --- a/src/line.rs +++ b/src/line.rs @@ -18,6 +18,27 @@ impl Line<'_> { } } + pub(crate) fn sigils(&self, settings: &Settings) -> BTreeSet { + let mut sigils = BTreeSet::new(); + + if let Some(first) = self.first() { + for c in first.chars() { + let sigil = match c { + '-' => Sigil::Infallible, + '?' if settings.guards => Sigil::Guard, + '@' => Sigil::Quiet, + _ => break, + }; + + if !sigils.insert(sigil) { + break; + } + } + } + + sigils + } + pub(crate) fn is_comment(&self) -> bool { self.first().is_some_and(|text| text.starts_with('#')) } @@ -33,18 +54,6 @@ impl Line<'_> { self.fragments.is_empty() } - pub(crate) fn is_infallible(&self) -> bool { - self - .first() - .is_some_and(|text| text.starts_with('-') || text.starts_with("@-")) - } - - pub(crate) fn is_quiet(&self) -> bool { - self - .first() - .is_some_and(|text| text.starts_with('@') || text.starts_with("-@")) - } - pub(crate) fn is_shebang(&self) -> bool { self.first().is_some_and(|text| text.starts_with("#!")) } diff --git a/src/node.rs b/src/node.rs index 6bfb042af8..99dca5df34 100644 --- a/src/node.rs +++ b/src/node.rs @@ -291,6 +291,7 @@ impl<'src> Node<'src> for Set<'src> { | Setting::DotenvRequired(value) | Setting::Export(value) | Setting::Fallback(value) + | Setting::Guards(value) | Setting::PositionalArguments(value) | Setting::Quiet(value) | Setting::Unstable(value) diff --git a/src/parser.rs b/src/parser.rs index 2258c13b3b..82e969cf83 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -917,7 +917,7 @@ impl<'run, 'src> Parser<'run, 'src> { let body = self.parse_body()?; - let shebang = body.first().map_or(false, Line::is_shebang); + let shebang = body.first().is_some_and(Line::is_shebang); let script = attributes.contains(AttributeDiscriminant::Script); if shebang && script { @@ -1016,7 +1016,7 @@ impl<'run, 'src> Parser<'run, 'src> { } } - while lines.last().map_or(false, Line::is_empty) { + while lines.last().is_some_and(Line::is_empty) { lines.pop(); } @@ -1067,6 +1067,7 @@ impl<'run, 'src> Parser<'run, 'src> { Keyword::DotenvRequired => Some(Setting::DotenvRequired(self.parse_set_bool()?)), Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)), Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)), + Keyword::Guards => Some(Setting::Guards(self.parse_set_bool()?)), Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)), Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)), Keyword::Quiet => Some(Setting::Quiet(self.parse_set_bool()?)), diff --git a/src/recipe.rs b/src/recipe.rs index d53a44bb40..6c93c2583d 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -195,20 +195,19 @@ impl<'src, D> Recipe<'src, D> { mut evaluator: Evaluator<'src, 'run>, ) -> RunResult<'src, ()> { let config = &context.config; + let settings = &context.module.settings; let mut lines = self.body.iter().peekable(); let mut line_number = self.line_number() + 1; loop { - if lines.peek().is_none() { + let Some(line) = lines.peek() else { return Ok(()); - } + }; + let mut evaluated = String::new(); let mut continued = false; - let quiet_line = lines.peek().map_or(false, |line| line.is_quiet()); - let infallible_line = lines.peek().map_or(false, |line| line.is_infallible()); - - let comment_line = context.module.settings.ignore_comments - && lines.peek().map_or(false, |line| line.is_comment()); + let comment_line = settings.ignore_comments && line.is_comment(); + let sigils = line.sigils(settings); loop { if lines.peek().is_none() { @@ -233,9 +232,7 @@ impl<'src, D> Recipe<'src, D> { let mut command = evaluated.as_str(); - let sigils = usize::from(infallible_line) + usize::from(quiet_line); - - command = &command[sigils..]; + command = &command[sigils.len()..]; if command.is_empty() { continue; @@ -243,7 +240,7 @@ impl<'src, D> Recipe<'src, D> { if config.dry_run || config.verbosity.loquacious() - || !((quiet_line ^ self.quiet) + || !((sigils.contains(&Sigil::Quiet) ^ self.quiet) || (context.module.settings.quiet && !self.no_quiet()) || config.verbosity.quiet()) { @@ -299,13 +296,17 @@ impl<'src, D> Recipe<'src, D> { match InterruptHandler::guard(|| cmd.status()) { Ok(exit_status) => { if let Some(code) = exit_status.code() { - if code != 0 && !infallible_line { - return Err(Error::Code { - recipe: self.name(), - line_number: Some(line_number), - code, - print_message: self.print_exit_message(), - }); + if code != 0 { + if sigils.contains(&Sigil::Guard) { + return Ok(()); + } else if !sigils.contains(&Sigil::Infallible) { + return Err(Error::Code { + recipe: self.name(), + line_number: Some(line_number), + code, + print_message: self.print_exit_message(), + }); + } } } else { return Err(error_from_signal( diff --git a/src/setting.rs b/src/setting.rs index ec45f30144..a2862d989f 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -10,6 +10,7 @@ pub(crate) enum Setting<'src> { DotenvRequired(bool), Export(bool), Fallback(bool), + Guards(bool), IgnoreComments(bool), PositionalArguments(bool), Quiet(bool), @@ -31,6 +32,7 @@ impl Display for Setting<'_> { | Self::DotenvRequired(value) | Self::Export(value) | Self::Fallback(value) + | Self::Guards(value) | Self::IgnoreComments(value) | Self::PositionalArguments(value) | Self::Quiet(value) diff --git a/src/settings.rs b/src/settings.rs index 4d0f9ea249..4cfc56ad9c 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -15,6 +15,7 @@ pub(crate) struct Settings<'src> { pub(crate) dotenv_required: bool, pub(crate) export: bool, pub(crate) fallback: bool, + pub(crate) guards: bool, pub(crate) ignore_comments: bool, pub(crate) positional_arguments: bool, pub(crate) quiet: bool, @@ -58,6 +59,9 @@ impl<'src> Settings<'src> { Setting::Fallback(fallback) => { settings.fallback = fallback; } + Setting::Guards(guards) => { + settings.guards = guards; + } Setting::IgnoreComments(ignore_comments) => { settings.ignore_comments = ignore_comments; } diff --git a/src/sigil.rs b/src/sigil.rs new file mode 100644 index 0000000000..80f7202f0c --- /dev/null +++ b/src/sigil.rs @@ -0,0 +1,6 @@ +#[derive(Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum Sigil { + Guard, + Infallible, + Quiet, +} diff --git a/tests/guards.rs b/tests/guards.rs new file mode 100644 index 0000000000..923026a48e --- /dev/null +++ b/tests/guards.rs @@ -0,0 +1,32 @@ +use super::*; + +#[test] +fn guard_lines_halt_executation() { + Test::new() + .justfile( + " + set guards + + @foo: + ?[[ 'foo' == 'bar' ]] + echo baz + ", + ) + .run(); +} + +#[test] +fn guard_lines_have_no_effect_if_successful() { + Test::new() + .justfile( + " + set guards + + @foo: + ?[[ 'foo' == 'foo' ]] + echo baz + ", + ) + .stdout("baz\n") + .run(); +} diff --git a/tests/json.rs b/tests/json.rs index 4e740e52e7..2d849baabf 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -83,6 +83,7 @@ struct Settings<'a> { dotenv_required: bool, export: bool, fallback: bool, + guards: bool, ignore_comments: bool, positional_arguments: bool, quiet: bool, diff --git a/tests/lib.rs b/tests/lib.rs index a86a3da769..c4f973286d 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -71,6 +71,7 @@ mod functions; #[cfg(unix)] mod global; mod groups; +mod guards; mod ignore_comments; mod imports; mod init; From f3196aa6c8ef6172491a1e0598fe5dcfdb11a96d Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Sun, 22 Dec 2024 22:01:18 -0800 Subject: [PATCH 2/2] Adapt --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a58913dde..1c4ec69699 100644 --- a/README.md +++ b/README.md @@ -1544,7 +1544,7 @@ depend on `just` variables and `.env` files. ### Sigils -Commands in linewise recipes may be prefixed with any combination of sigils +Commands in linewise recipes may be prefixed with any combination of the sigils `-`, `@`, and `?`. The `@` sigil toggles command echoing: @@ -1566,6 +1566,8 @@ nonzero exit status: # execution will continue, even if bar doesn't exist foo: -rmdir bar + mkdir bar + echo 'so much good stuff' > bar/stuff.txt ``` The `?` sigilmaster causes the current recipe to stop executing if