diff --git a/src/cmp.rs b/src/cmp.rs index d1121ce..1f5829c 100644 --- a/src/cmp.rs +++ b/src/cmp.rs @@ -36,6 +36,15 @@ pub struct Params { max_bytes: Option, verbose: bool, quiet: bool, + stdout_is_dev_null: bool, +} + +impl Params { + /// Whether the difference and EOF reports are skipped. Error messages + /// are only hidden by `quiet`. + fn no_report(&self) -> bool { + self.quiet || self.stdout_is_dev_null + } } #[inline] @@ -223,21 +232,21 @@ pub fn parse_params>(mut opts: Peekable) -> Resu } } - // Do as GNU cmp, and completely disable printing if we are + if params.quiet && params.verbose { + return Err(format!( + "{executable_str}: options -l and -s are incompatible" + )); + } + + // Do as GNU cmp, and disable reporting the differences if we are // outputting to /dev/null. #[cfg(not(target_os = "windows"))] if is_stdout_dev_null() { - params.quiet = true; + params.stdout_is_dev_null = true; params.verbose = false; params.print_bytes = false; } - if params.quiet && params.verbose { - return Err(format!( - "{executable_str}: options -l and -s are incompatible" - )); - } - params.from = if let Some(from) = from { from } else if let Some(param) = opts.next() { @@ -316,7 +325,7 @@ pub fn cmp(params: &Params) -> Result { // If the files have different sizes, we already know they are not identical. If we have not // been asked to show even the first difference, we can quit early. - if params.quiet && a_size != b_size { + if params.no_report() && a_size != b_size { return Ok(Cmp::Different); } @@ -630,7 +639,7 @@ fn report_eof( eof_on: &str, params: &Params, ) { - if params.quiet { + if params.no_report() { return; } @@ -674,7 +683,7 @@ fn report_difference( at_line: u64, params: &Params, ) { - if params.quiet { + if params.no_report() { return; } diff --git a/tests/by-util/test_cmp.rs b/tests/by-util/test_cmp.rs index d7ad3a1..f4c0792 100644 --- a/tests/by-util/test_cmp.rs +++ b/tests/by-util/test_cmp.rs @@ -61,6 +61,52 @@ fn cmp_stdin(#[case] input: &str, #[case] code: i32, #[case] expected: &str) { .stdout_only(expected); } +// cmp turns the difference output off when its stdout is /dev/null. +#[cfg(unix)] +fn dev_null() -> std::fs::File { + OpenOptions::new().write(true).open("/dev/null").unwrap() +} + +// Unlike -s, sending stdout to /dev/null must not hide error messages. +#[test] +#[cfg(unix)] +fn cmp_dev_null_stdout_still_reports_errors() { + let (at, ts) = at_and_ts!(); + at.write("a", "abc"); + + ts.ucmd() + .args(&["a", "missing"]) + .set_stdout(dev_null()) + .fails_with_code(2) + .stderr_contains("No such file or directory"); + ts.ucmd() + .args(&["-l", "-s", "a"]) + .set_stdout(dev_null()) + .fails_with_code(2) + .stderr_str_check(|s| s.ends_with(": options -l and -s are incompatible\n")); +} + +// The difference and EOF reports stay off when stdout is /dev/null. +#[test] +#[cfg(unix)] +fn cmp_dev_null_stdout_reports_no_difference() { + let (at, ts) = at_and_ts!(); + at.write("a", "abcdef"); + at.write("shorter", "abc"); + at.write("changed", "abcdeX"); + + for other in ["shorter", "changed"] { + for option in [&[][..], &["-l"], &["-b"]] { + ts.ucmd() + .args(option) + .args(&["a", other]) + .set_stdout(dev_null()) + .fails_with_code(1) + .no_stderr(); + } + } +} + #[test] fn cmp_equal_files() { let (at, ts) = at_and_ts!();