From 3d37e1caa3c2a65e06dcd684e990cee14b276540 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 29 Dec 2025 18:29:45 +0900 Subject: [PATCH 1/5] fix: Defer printing `wc` I/O errors from word counting until after statistics output, matching GNU behavior. --- src/uu/wc/src/wc.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 1f4b67c2047..b7dc1fc9d60 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -950,11 +950,11 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { } }; - let word_count = match word_count_from_input(&input, settings) { - CountResult::Success(word_count) => word_count, + // Store any I/O error from reading to print AFTER stats (matches GNU wc behavior) + let (word_count, deferred_error) = match word_count_from_input(&input, settings) { + CountResult::Success(word_count) => (word_count, None), CountResult::Interrupted(word_count, err) => { - show!(err.map_err_context(|| input.path_display())); - word_count + (word_count, Some(err.map_err_context(|| input.path_display()))) } CountResult::Failure(err) => { show!(err.map_err_context(|| input.path_display())); @@ -970,6 +970,10 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { show!(err.map_err_context(|| translate!("wc-error-failed-to-print-result", "title" => title.to_string_lossy()))); } } + // Print deferred error after stats to match GNU wc output order + if let Some(err) = deferred_error { + show!(err); + } } if settings.total_when.is_total_row_visible(num_inputs) { From e9070a9e7bf8d17e9839f5e88f5f9a54e16805a8 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 29 Dec 2025 18:30:10 +0900 Subject: [PATCH 2/5] refactor: reformat `CountResult::Interrupted` arm for improved readability --- src/uu/wc/src/wc.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index b7dc1fc9d60..9c984baaa5a 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -953,9 +953,10 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { // Store any I/O error from reading to print AFTER stats (matches GNU wc behavior) let (word_count, deferred_error) = match word_count_from_input(&input, settings) { CountResult::Success(word_count) => (word_count, None), - CountResult::Interrupted(word_count, err) => { - (word_count, Some(err.map_err_context(|| input.path_display()))) - } + CountResult::Interrupted(word_count, err) => ( + word_count, + Some(err.map_err_context(|| input.path_display())), + ), CountResult::Failure(err) => { show!(err.map_err_context(|| input.path_display())); continue; From a2fd722bb7e51f16133a96a041a9e5047bc70660 Mon Sep 17 00:00:00 2001 From: mattsu Date: Mon, 29 Dec 2025 18:46:05 +0900 Subject: [PATCH 3/5] fix(wc): flush stdout before printing deferred error Ensure deferred error messages are printed after all stats output, matching GNU wc behavior. Added io::stdout().flush() to guarantee buffered output is written before showing the error. --- src/uu/wc/src/wc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index 9c984baaa5a..4ae07fe2b8f 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -973,6 +973,7 @@ fn wc(inputs: &Inputs, settings: &Settings) -> UResult<()> { } // Print deferred error after stats to match GNU wc output order if let Some(err) = deferred_error { + let _ = io::stdout().flush(); show!(err); } } From de4a98f1b190cf876127f6c448a6494c8a465a54 Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 30 Dec 2025 08:20:49 +0900 Subject: [PATCH 4/5] test: add test for wc error handling with stderr redirected to stdout Add a new Unix-specific test case to verify that `wc` correctly handles reading from a directory when stderr is redirected to stdout, ensuring error messages appear in the expected order alongside the output. This improves test coverage for edge cases in error redirection. --- tests/by-util/test_wc.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index d1266e09d5c..f5a39bb0310 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -449,6 +449,23 @@ fn test_read_from_directory_error() { .stdout_is(STDOUT); } +#[cfg(unix)] +#[test] +fn test_read_error_order_with_stderr_to_stdout() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("ioerrdir"); + + let expected = format!( + "{:>7} {:>7} {:>7} ioerrdir\nwc: ioerrdir: Is a directory\n", + 0, 0, 0 + ); + + ucmd.arg("ioerrdir") + .stderr_to_stdout() + .fails() + .stdout_only(expected); +} + /// Test that getting counts from nonexistent file is an error. #[test] fn test_read_from_nonexistent_file() { From da2dbc7759dcee90e0eda3a5f04f2693e7ba560e Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Tue, 30 Dec 2025 21:58:54 +0100 Subject: [PATCH 5/5] Update spell-checker ignore list in test_wc.rs --- tests/by-util/test_wc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index f5a39bb0310..be23742838f 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -8,7 +8,7 @@ use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::vec_of_size; -// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword weirdchars +// spell-checker:ignore (flags) lwmcL clmwL ; (path) bogusfile emptyfile manyemptylines moby notrailingnewline onelongemptyline onelongword weirdchars ioerrdir #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1);