Repository: casey/just
Pull request #1704 (2023-10-17 15:17:29)

Feat/issue 1698 global quiet
Hi,

Here's a first pass at introducing `quiet recipes`. I'm happy to take unboard any thoughts you may have. Not quite sure about the name `set quiet-recipes`, but was struggling to come up with something better (maybe `set globally-quiet`?) :-)

Tests pass (with my additional ones added).

Thank you for your consideration.

#Fixes 1698 

-=david=-

casey: Thanks for the PR!

Some thoughts:

- Let's call the setting `quiet`, so you can do `set quiet`.
- Let's call the attribute `[noquiet]`
- We could also not have an attribute, and invert the meaning of `@` if `set quiet` is used. So if you `set quiet`, you can do `@recipe:`, which would make that recipe not quiet`. `@` is a bit confusing though, so I'm not convinced either way on this.
dharrigan: Hi,

Thank you for your helpful feedback. I'll make the changes within the next few days :-)

With regards the inversion of the `@`. I thought about this and I decided on the principle of least surprise (for the user). They are used to `@` to signify that the recipe (or line) should be quiet, so I didn't want to break that by introducing an inverted case. I would rather accrete changes than break expectations. I then thought that by introducing the attribute of `[no-quiet-recipe]` (now `[no-quiet]`), they could then choose to make it explicit if they want an already demarked recipe of `@` to become non-quiet when the global attribute is used.

So, no surprises was my thoughts behind that one :-)

-=david=-
dharrigan: A question...

You've recommended the attribute to be renamed to `noquiet`. However, there is the precedent of `no-exit-message`, with a hyphen between the `no` and the action. Would it be better to be consisent and call the attribute `no-quiet`?

Thank you.
casey: Ah, yeah, good catch. In that case it should be `no-quiet`.
dharrigan: Hi!

I was wondering if you have had a chance to consider to merge in this PR? Anything else I can do?

Thank you.

-=david=-
dharrigan: Hi!

All changes asked for applied. When you can spare the time, have a looksee and see what you think :-)

Thank you.

-=david=-
dharrigan: Hi,

Additional integration tests done. I hope this is looking better.

-=david=-

Patch:
diff --git a/GRAMMAR.md b/GRAMMAR.md
index 920ad849..4c4a25ca 100644
--- a/GRAMMAR.md
+++ b/GRAMMAR.md
@@ -69,6 +69,7 @@ setting       : 'set' 'allow-duplicate-recipes' boolean?
               | 'set' 'fallback' boolean?
               | 'set' 'ignore-comments' boolean?
               | 'set' 'positional-arguments' boolean?
+              | 'set' 'quiet' boolean?
               | 'set' 'shell' ':=' '[' string (',' string)* ','? ']'
               | 'set' 'tempdir ':=' string
               | 'set' 'windows-powershell' boolean?
diff --git a/README.md b/README.md
index c854c565..0020dabd 100644
--- a/README.md
+++ b/README.md
@@ -1460,6 +1460,7 @@ Recipes may be annotated with attributes that change their behavior.
 | `[macos]`<sup>1.8.0</sup> | Enable recipe on MacOS. |
 | `[no-cd]`<sup>1.9.0</sup> | Don't change directory before executing recipe. |
 | `[no-exit-message]`<sup>1.7.0</sup> | Don't print an error message if recipe fails. |
+| `[no-quiet]`<sup>master</sup> | Override globally quiet recipes and always echo out the recipe. |
 | `[private]`<sup>1.10.0</sup> | See [Private Recipes](#private-recipes). |
 | `[unix]`<sup>1.8.0</sup> | Enable recipe on Unixes. (Includes MacOS). |
 | `[windows]`<sup>1.8.0</sup> | Enable recipe on Windows. |
@@ -2496,6 +2497,31 @@ goodbye
 # all done!
 ```
 
+All recipes in a Justfile can be made quiet with `set quiet`:
+
+```just
+set quiet
+
+foo:
+  echo "This is quiet"
+
+@foo2:
+  echo "This is also quiet"
+```
+
+The `[no-quiet]` attribute overrides this setting:
+
+```just
+set quiet
+
+foo:
+  echo "This is quiet"
+
+[no-quiet]
+foo2:
+  echo "This is not quiet"
+```
+
 Shebang recipes are quiet by default:
 
 ```just
diff --git a/src/attribute.rs b/src/attribute.rs
index b4c71665..0c361e3b 100644
--- a/src/attribute.rs
+++ b/src/attribute.rs
@@ -12,6 +12,7 @@ pub(crate) enum Attribute {
   NoCd,
   NoExitMessage,
   Private,
+  NoQuiet,
   Unix,
   Windows,
 }
diff --git a/src/keyword.rs b/src/keyword.rs
index 8bde1a8c..830207e5 100644
--- a/src/keyword.rs
+++ b/src/keyword.rs
@@ -17,6 +17,7 @@ pub(crate) enum Keyword {
   Import,
   Mod,
   PositionalArguments,
+  Quiet,
   Set,
   Shell,
   Tempdir,
diff --git a/src/node.rs b/src/node.rs
index 77309d64..1d98b991 100644
--- a/src/node.rs
+++ b/src/node.rs
@@ -269,6 +269,7 @@ impl<'src> Node<'src> for Set<'src> {
       | Setting::Export(value)
       | Setting::Fallback(value)
       | Setting::PositionalArguments(value)
+      | Setting::Quiet(value)
       | Setting::WindowsPowerShell(value)
       | Setting::IgnoreComments(value) => {
         set.push_mut(value.to_string());
diff --git a/src/parser.rs b/src/parser.rs
index 6608c5f1..3f7deda7 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -852,6 +852,7 @@ impl<'run, 'src> Parser<'run, 'src> {
       Keyword::Fallback => Some(Setting::Fallback(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()?)),
       Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
       _ => None,
     };
@@ -1927,6 +1928,24 @@ mod tests {
     tree: (justfile (set positional_arguments true)),
   }
 
+  test! {
+    name: set_quiet_implicit,
+    text: "set quiet",
+    tree: (justfile (set quiet true)),
+  }
+
+  test! {
+    name: set_quiet_true,
+    text: "set quiet := true",
+    tree: (justfile (set quiet true)),
+  }
+
+  test! {
+    name: set_quiet_false,
+    text: "set quiet := false",
+    tree: (justfile (set quiet false)),
+  }
+
   test! {
     name: set_positional_arguments_false,
     text: "set positional-arguments := false",
diff --git a/src/recipe.rs b/src/recipe.rs
index 99f90e1f..bc06cf46 100644
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -134,6 +134,10 @@ impl<'src, D> Recipe<'src, D> {
     }
   }
 
+  fn no_quiet(&self) -> bool {
+    self.attributes.contains(&Attribute::NoQuiet)
+  }
+
   pub(crate) fn run<'run>(
     &self,
     context: &RecipeContext<'src, 'run>,
@@ -181,8 +185,8 @@ impl<'src, D> Recipe<'src, D> {
       }
       let mut evaluated = String::new();
       let mut continued = false;
-      let quiet_command = lines.peek().map_or(false, |line| line.is_quiet());
-      let infallible_command = lines.peek().map_or(false, |line| line.is_infallible());
+      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.settings.ignore_comments && lines.peek().map_or(false, |line| line.is_comment());
@@ -210,7 +214,7 @@ impl<'src, D> Recipe<'src, D> {
 
       let mut command = evaluated.as_str();
 
-      let sigils = usize::from(infallible_command) + usize::from(quiet_command);
+      let sigils = usize::from(infallible_line) + usize::from(quiet_line);
 
       command = &command[sigils..];
 
@@ -220,7 +224,9 @@ impl<'src, D> Recipe<'src, D> {
 
       if config.dry_run
         || config.verbosity.loquacious()
-        || !((quiet_command ^ self.quiet) || config.verbosity.quiet())
+        || !((quiet_line ^ self.quiet)
+          || (context.settings.quiet && !self.no_quiet())
+          || config.verbosity.quiet())
       {
         let color = if config.highlight {
           config.color.command(config.command_color)
@@ -257,7 +263,7 @@ 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_command {
+            if code != 0 && !infallible_line {
               return Err(Error::Code {
                 recipe: self.name(),
                 line_number: Some(line_number),
diff --git a/src/setting.rs b/src/setting.rs
index 9b2a9fe6..0256ff94 100644
--- a/src/setting.rs
+++ b/src/setting.rs
@@ -10,6 +10,7 @@ pub(crate) enum Setting<'src> {
   Fallback(bool),
   IgnoreComments(bool),
   PositionalArguments(bool),
+  Quiet(bool),
   Shell(Shell<'src>),
   Tempdir(String),
   WindowsPowerShell(bool),
@@ -25,6 +26,7 @@ impl<'src> Display for Setting<'src> {
       | Setting::Fallback(value)
       | Setting::IgnoreComments(value)
       | Setting::PositionalArguments(value)
+      | Setting::Quiet(value)
       | Setting::WindowsPowerShell(value) => write!(f, "{value}"),
       Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"),
       Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
diff --git a/src/settings.rs b/src/settings.rs
index 5c5e64ce..f72cb546 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -15,6 +15,7 @@ pub(crate) struct Settings<'src> {
   pub(crate) fallback: bool,
   pub(crate) ignore_comments: bool,
   pub(crate) positional_arguments: bool,
+  pub(crate) quiet: bool,
   pub(crate) shell: Option<Shell<'src>>,
   pub(crate) tempdir: Option<String>,
   pub(crate) windows_powershell: bool,
@@ -51,6 +52,9 @@ impl<'src> Settings<'src> {
         Setting::PositionalArguments(positional_arguments) => {
           settings.positional_arguments = positional_arguments;
         }
+        Setting::Quiet(quiet) => {
+          settings.quiet = quiet;
+        }
         Setting::Shell(shell) => {
           settings.shell = Some(shell);
         }


Test patch:
diff --git a/tests/json.rs b/tests/json.rs
index c70063f2..4c71b430 100644
--- a/tests/json.rs
+++ b/tests/json.rs
@@ -50,6 +50,7 @@ fn alias() {
         "export": false,
         "fallback": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "ignore_comments": false,
@@ -86,6 +87,7 @@ fn assignment() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -136,6 +138,7 @@ fn body() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -198,6 +201,7 @@ fn dependencies() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -297,6 +301,7 @@ fn dependency_argument() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -359,6 +364,7 @@ fn duplicate_recipes() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -402,6 +408,7 @@ fn doc_comment() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -431,6 +438,7 @@ fn empty_justfile() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -581,6 +589,7 @@ fn parameters() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -664,6 +673,7 @@ fn priors() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -707,6 +717,7 @@ fn private() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -750,6 +761,7 @@ fn quiet() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "windows_powershell": false,
@@ -770,6 +782,7 @@ fn settings() {
       set export
       set fallback
       set positional-arguments
+      set quiet
       set ignore-comments
       set shell := ['a', 'b', 'c']
       foo:
@@ -804,6 +817,7 @@ fn settings() {
         "fallback": true,
         "ignore_comments": true,
         "positional_arguments": true,
+        "quiet": true,
         "shell": {
           "arguments": ["b", "c"],
           "command": "a",
@@ -853,6 +867,7 @@ fn shebang() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir": null,
         "windows_powershell": false,
@@ -896,6 +911,7 @@ fn simple() {
         "fallback": false,
         "ignore_comments": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir": null,
         "windows_powershell": false,
@@ -941,6 +957,7 @@ fn attribute() {
         "export": false,
         "fallback": false,
         "positional_arguments": false,
+        "quiet": false,
         "shell": null,
         "tempdir" : null,
         "ignore_comments": false,
@@ -1000,6 +1017,7 @@ fn module() {
               "export": false,
               "fallback": false,
               "positional_arguments": false,
+              "quiet": false,
               "shell": null,
               "tempdir" : null,
               "ignore_comments": false,
@@ -1018,6 +1036,7 @@ fn module() {
           "export": false,
           "fallback": false,
           "positional_arguments": false,
+          "quiet": false,
           "shell": null,
           "tempdir" : null,
           "ignore_comments": false,
diff --git a/tests/quiet.rs b/tests/quiet.rs
index 8fc0e4f6..59ad41e4 100644
--- a/tests/quiet.rs
+++ b/tests/quiet.rs
@@ -129,3 +129,111 @@ test! {
   ",
   args: ("--quiet"),
 }
+
+#[test]
+fn no_quiet_setting() {
+  Test::new()
+    .justfile(
+      "
+        foo:
+          echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .stderr("echo FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      foo:
+        echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting_with_no_quiet_attribute() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      [no-quiet]
+      foo:
+        echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .stderr("echo FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting_with_quiet_recipe() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      @foo:
+        echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting_with_quiet_line() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      foo:
+        @echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting_with_no_quiet_attribute_and_quiet_recipe() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      [no-quiet]
+      @foo:
+        echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .run();
+}
+
+#[test]
+fn quiet_setting_with_no_quiet_attribute_and_quiet_line() {
+  Test::new()
+    .justfile(
+      "
+      set quiet
+
+      [no-quiet]
+      foo:
+        @echo FOO
+      ",
+    )
+    .stdout("FOO\n")
+    .run();
+}