diff --git a/src/config.rs b/src/config.rs index 654e4f7..684fe1b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -483,14 +483,14 @@ impl Config { } if let Completions { shell } = self.subcommand { - return Subcommand::completions(&shell); + return Subcommand::completions(self.verbosity, &shell); } let search = Search::find(&self.search_config, &self.invocation_directory).eprint(self.color)?; if self.subcommand == Edit { - return Self::edit(&search); + return self.edit(&search); } let src = fs::read_to_string(&search.justfile) @@ -502,11 +502,13 @@ impl Config { let justfile = Compiler::compile(&src).eprint(self.color)?; - for warning in &justfile.warnings { - if self.color.stderr().active() { - eprintln!("{:#}", warning); - } else { - eprintln!("{}", warning); + if self.verbosity.loud() { + for warning in &justfile.warnings { + if self.color.stderr().active() { + eprintln!("{:#}", warning); + } else { + eprintln!("{}", warning); + } } } @@ -520,7 +522,7 @@ impl Config { arguments, overrides, } => self.run(justfile, &search, overrides, arguments)?, - Show { ref name } => Self::show(&name, justfile)?, + Show { ref name } => self.show(&name, justfile)?, Summary => self.summary(justfile), Variables => Self::variables(justfile), Completions { .. } | Edit | Init => unreachable!(), @@ -544,7 +546,9 @@ impl Config { .collect::>>(); if recipes.is_empty() { - eprintln!("Justfile contains no choosable recipes."); + if self.verbosity.loud() { + eprintln!("Justfile contains no choosable recipes."); + } return Err(EXIT_FAILURE); } @@ -565,11 +569,13 @@ impl Config { let mut child = match result { Ok(child) => child, Err(error) => { - eprintln!( - "Chooser `{}` invocation failed: {}", - chooser.to_string_lossy(), - error - ); + if self.verbosity.loud() { + eprintln!( + "Chooser `{}` invocation failed: {}", + chooser.to_string_lossy(), + error + ); + } return Err(EXIT_FAILURE); }, }; @@ -581,11 +587,13 @@ impl Config { .expect("Child was created with piped stdio") .write_all(format!("{}\n", recipe.name).as_bytes()) { - eprintln!( - "Failed to write to chooser `{}`: {}", - chooser.to_string_lossy(), - error - ); + if self.verbosity.loud() { + eprintln!( + "Failed to write to chooser `{}`: {}", + chooser.to_string_lossy(), + error + ); + } return Err(EXIT_FAILURE); } } @@ -593,21 +601,25 @@ impl Config { let output = match child.wait_with_output() { Ok(output) => output, Err(error) => { - eprintln!( - "Failed to read output from chooser `{}`: {}", - chooser.to_string_lossy(), - error - ); + if self.verbosity.loud() { + eprintln!( + "Failed to read output from chooser `{}`: {}", + chooser.to_string_lossy(), + error + ); + } return Err(EXIT_FAILURE); }, }; if !output.status.success() { - eprintln!( - "Chooser `{}` returned error: {}", - chooser.to_string_lossy(), - output.status - ); + if self.verbosity.loud() { + eprintln!( + "Chooser `{}` returned error: {}", + chooser.to_string_lossy(), + output.status + ); + } return Err(output.status.code().unwrap_or(EXIT_FAILURE)); } @@ -626,7 +638,7 @@ impl Config { println!("{}", justfile); } - pub(crate) fn edit(search: &Search) -> Result<(), i32> { + pub(crate) fn edit(&self, search: &Search) -> Result<(), i32> { let editor = env::var_os("VISUAL") .or_else(|| env::var_os("EDITOR")) .unwrap_or_else(|| "vim".into()); @@ -641,15 +653,19 @@ impl Config { if status.success() { Ok(()) } else { - eprintln!("Editor `{}` failed: {}", editor.to_string_lossy(), status); + if self.verbosity.loud() { + eprintln!("Editor `{}` failed: {}", editor.to_string_lossy(), status); + } Err(status.code().unwrap_or(EXIT_FAILURE)) }, Err(error) => { - eprintln!( - "Editor `{}` invocation failed: {}", - editor.to_string_lossy(), - error - ); + if self.verbosity.loud() { + eprintln!( + "Editor `{}` invocation failed: {}", + editor.to_string_lossy(), + error + ); + } Err(EXIT_FAILURE) }, } @@ -660,17 +676,23 @@ impl Config { Search::init(&self.search_config, &self.invocation_directory).eprint(self.color)?; if search.justfile.exists() { - eprintln!("Justfile `{}` already exists", search.justfile.display()); + if self.verbosity.loud() { + eprintln!("Justfile `{}` already exists", search.justfile.display()); + } Err(EXIT_FAILURE) } else if let Err(err) = fs::write(&search.justfile, INIT_JUSTFILE) { - eprintln!( - "Failed to write justfile to `{}`: {}", - search.justfile.display(), - err - ); + if self.verbosity.loud() { + eprintln!( + "Failed to write justfile to `{}`: {}", + search.justfile.display(), + err + ); + } Err(EXIT_FAILURE) } else { - eprintln!("Wrote justfile to `{}`", search.justfile.display()); + if self.verbosity.loud() { + eprintln!("Wrote justfile to `{}`", search.justfile.display()); + } Ok(()) } } @@ -766,7 +788,7 @@ impl Config { overrides: &BTreeMap, arguments: &[String], ) -> Result<(), i32> { - if let Err(error) = InterruptHandler::install() { + if let Err(error) = InterruptHandler::install(self.verbosity) { warn!("Failed to set CTRL-C handler: {}", error) } @@ -779,7 +801,7 @@ impl Config { } } - fn show(name: &str, justfile: Justfile) -> Result<(), i32> { + fn show(&self, name: &str, justfile: Justfile) -> Result<(), i32> { if let Some(alias) = justfile.get_alias(name) { let recipe = justfile.get_recipe(alias.target.name.lexeme()).unwrap(); println!("{}", alias); @@ -789,9 +811,11 @@ impl Config { println!("{}", recipe); Ok(()) } else { - eprintln!("Justfile does not contain recipe `{}`.", name); - if let Some(suggestion) = justfile.suggest(name) { - eprintln!("{}", suggestion); + if self.verbosity.loud() { + eprintln!("Justfile does not contain recipe `{}`.", name); + if let Some(suggestion) = justfile.suggest(name) { + eprintln!("{}", suggestion); + } } Err(EXIT_FAILURE) } @@ -799,7 +823,9 @@ impl Config { fn summary(&self, justfile: Justfile) { if justfile.count() == 0 { - eprintln!("Justfile contains no recipes."); + if self.verbosity.loud() { + eprintln!("Justfile contains no recipes."); + } } else { let summary = justfile .public_recipes(self.unsorted) diff --git a/src/interrupt_handler.rs b/src/interrupt_handler.rs index 65e40a8..a0d161e 100644 --- a/src/interrupt_handler.rs +++ b/src/interrupt_handler.rs @@ -3,10 +3,13 @@ use crate::common::*; pub(crate) struct InterruptHandler { blocks: u32, interrupted: bool, + verbosity: Verbosity, } impl InterruptHandler { - pub(crate) fn install() -> Result<(), ctrlc::Error> { + pub(crate) fn install(verbosity: Verbosity) -> Result<(), ctrlc::Error> { + let mut instance = Self::instance(); + instance.verbosity = verbosity; ctrlc::set_handler(|| Self::instance().interrupt()) } @@ -30,6 +33,7 @@ impl InterruptHandler { Self { blocks: 0, interrupted: false, + verbosity: Verbosity::default(), } } @@ -53,9 +57,11 @@ impl InterruptHandler { pub(crate) fn unblock(&mut self) { if self.blocks == 0 { - eprintln!("{}", RuntimeError::Internal { - message: "attempted to unblock interrupt handler, but handler was not blocked".to_owned(), - }); + if self.verbosity.loud() { + eprintln!("{}", RuntimeError::Internal { + message: "attempted to unblock interrupt handler, but handler was not blocked".to_owned(), + }); + } std::process::exit(EXIT_FAILURE); } diff --git a/src/recipe.rs b/src/recipe.rs index 48767cb..625e8ba 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -96,7 +96,7 @@ impl<'src, D> Recipe<'src, D> { evaluated_lines.push(evaluator.evaluate_line(line, false)?); } - if config.dry_run || self.quiet { + if config.verbosity.loud() && (config.dry_run || self.quiet) { for line in &evaluated_lines { eprintln!("{}", line); } diff --git a/src/subcommand.rs b/src/subcommand.rs index 287e1de..53e7399 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -203,18 +203,25 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[( )]; impl Subcommand { - pub(crate) fn completions(shell: &str) -> Result<(), i32> { + pub(crate) fn completions(verbosity: Verbosity, shell: &str) -> Result<(), i32> { use clap::Shell; - fn replace(haystack: &mut String, needle: &str, replacement: &str) -> Result<(), i32> { + fn replace( + verbosity: Verbosity, + haystack: &mut String, + needle: &str, + replacement: &str, + ) -> Result<(), i32> { if let Some(index) = haystack.find(needle) { haystack.replace_range(index..index + needle.len(), replacement); Ok(()) } else { - eprintln!("Failed to find text:"); - eprintln!("{}", needle); - eprintln!("…in completion script:"); - eprintln!("{}", haystack); + if verbosity.loud() { + eprintln!("Failed to find text:"); + eprintln!("{}", needle); + eprintln!("…in completion script:"); + eprintln!("{}", haystack); + } Err(EXIT_FAILURE) } } @@ -232,19 +239,19 @@ impl Subcommand { match shell { Shell::Bash => for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; + replace(verbosity, &mut script, needle, replacement)?; }, Shell::Fish => { script.insert_str(0, FISH_RECIPE_COMPLETIONS); }, Shell::PowerShell => for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; + replace(verbosity, &mut script, needle, replacement)?; }, Shell::Zsh => for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS { - replace(&mut script, needle, replacement)?; + replace(verbosity, &mut script, needle, replacement)?; }, Shell::Elvish => {}, } diff --git a/src/verbosity.rs b/src/verbosity.rs index c4fadff..51b9b9c 100644 --- a/src/verbosity.rs +++ b/src/verbosity.rs @@ -21,6 +21,10 @@ impl Verbosity { matches!(self, Quiet) } + pub(crate) fn loud(self) -> bool { + !self.quiet() + } + pub(crate) fn loquacious(self) -> bool { match self { Quiet | Taciturn => false, @@ -35,3 +39,9 @@ impl Verbosity { } } } + +impl Default for Verbosity { + fn default() -> Self { + Self::Taciturn + } +} diff --git a/tests/lib.rs b/tests/lib.rs index ea2abbf..dbdf149 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -15,6 +15,7 @@ mod init; mod interrupts; mod invocation_directory; mod misc; +mod quiet; mod readme; mod search; mod shell; diff --git a/tests/misc.rs b/tests/misc.rs index 36e2e08..989fdf2 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -666,71 +666,6 @@ test! { status: EXIT_FAILURE, } -test! { - name: quiet_flag_no_stdout, - justfile: r#" -default: - @echo hello -"#, - args: ("--quiet"), - stdout: "", -} - -test! { - name: quiet_flag_no_stderr, - justfile: r#" -default: - @echo hello 1>&2 -"#, - args: ("--quiet"), - stdout: "", -} - -test! { - name: quiet_flag_no_command_echoing, - justfile: r#" -default: - exit -"#, - args: ("--quiet"), - stdout: "", -} - -test! { - name: quiet_flag_no_error_messages, - justfile: r#" -default: - exit 100 -"#, - args: ("--quiet"), - stdout: "", - status: 100, -} - -test! { - name: quiet_flag_no_assignment_backtick_stderr, - justfile: r#" -a := `echo hello 1>&2` -default: - exit 100 -"#, - args: ("--quiet"), - stdout: "", - status: 100, -} - -test! { - name: quiet_flag_no_interpolation_backtick_stderr, - justfile: r#" -default: - echo `echo hello 1>&2` - exit 100 -"#, - args: ("--quiet"), - stdout: "", - status: 100, -} - test! { name: argument_single, justfile: " diff --git a/tests/quiet.rs b/tests/quiet.rs new file mode 100644 index 0000000..1f3058e --- /dev/null +++ b/tests/quiet.rs @@ -0,0 +1,147 @@ +use crate::common::*; + +test! { + name: no_stdout, + justfile: r#" +default: + @echo hello +"#, + args: ("--quiet"), + stdout: "", +} + +test! { + name: stderr, + justfile: r#" +default: + @echo hello 1>&2 +"#, + args: ("--quiet"), + stdout: "", +} + +test! { + name: command_echoing, + justfile: r#" +default: + exit +"#, + args: ("--quiet"), + stdout: "", +} + +test! { + name: error_messages, + justfile: r#" +default: + exit 100 +"#, + args: ("--quiet"), + stdout: "", + status: 100, +} + +test! { + name: assignment_backtick_stderr, + justfile: r#" +a := `echo hello 1>&2` +default: + exit 100 +"#, + args: ("--quiet"), + stdout: "", + status: 100, +} + +test! { + name: interpolation_backtick_stderr, + justfile: r#" +default: + echo `echo hello 1>&2` + exit 100 +"#, + args: ("--quiet"), + stdout: "", + status: 100, +} + +test! { + name: warning, + justfile: " + foo = 'bar' + + baz: + ", + args: ("--quiet"), +} + +test! { + name: choose_none, + justfile: "", + args: ("--choose", "--quiet"), + status: EXIT_FAILURE, +} + +test! { + name: choose_invocation, + justfile: "foo:", + args: ("--choose", "--quiet", "--shell", "asdfasdfasfdasdfasdfadsf"), + status: EXIT_FAILURE, + shell: false, +} + +test! { + name: choose_status, + justfile: "foo:", + args: ("--choose", "--quiet", "--chooser", "/usr/bin/env false"), + status: EXIT_FAILURE, +} + +test! { + name: edit_invocation, + justfile: "foo:", + args: ("--edit", "--quiet"), + env: { + "VISUAL": "adsfasdfasdfadsfadfsaf", + }, + status: EXIT_FAILURE, +} + +test! { + name: edit_status, + justfile: "foo:", + args: ("--edit", "--quiet"), + env: { + "VISUAL": "false", + }, + status: EXIT_FAILURE, +} + +test! { + name: init_exists, + justfile: "foo:", + args: ("--init", "--quiet"), + status: EXIT_FAILURE, +} + +test! { + name: show_missing, + justfile: "foo:", + args: ("--show", "bar", "--quiet"), + status: EXIT_FAILURE, +} + +test! { + name: summary_none, + justfile: "", + args: ("--summary", "--quiet"), +} + +test! { + name: quiet_shebang, + justfile: " + @foo: + #!/bin/sh + ", + args: ("--quiet"), +}