From 275483d95572686fd0d2e1faa176ad5a22e02105 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Fri, 30 Oct 2026 04:53:49 +0800 Subject: [PATCH 1/5] paste: include file name in open errors --- src/uu/paste/locales/en-US.ftl | 1 + src/uu/paste/src/paste.rs | 11 ++++++++++- tests/by-util/test_paste.rs | 8 ++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/uu/paste/locales/en-US.ftl b/src/uu/paste/locales/en-US.ftl index 8dc74ad2d90..50aeb4a7d93 100644 --- a/src/uu/paste/locales/en-US.ftl +++ b/src/uu/paste/locales/en-US.ftl @@ -10,3 +10,4 @@ paste-help-zero-terminated = line delimiter is NUL, not newline # Error messages paste-error-delimiter-unescaped-backslash = delimiter list ends with an unescaped backslash: { $delimiters } paste-error-stdin-borrow = failed to access standard input: { $error } +paste-error-file = { $file }: { $error } diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 00e62cab2e8..50c93b77bf4 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -107,7 +107,16 @@ fn paste( ) } else { let path = Path::new(&filename); - let file = File::open(path)?; + let file = File::open(path).map_err(|error| { + USimpleError::new( + 1, + translate!( + "paste-error-file", + "file" => filename.to_string_lossy(), + "error" => error + ), + ) + })?; InputSource::File(BufReader::new(file)) }; diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 582d70e5ab3..7792d71105c 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -609,3 +609,11 @@ fn test_crlf_delimiter_not_eaten_by_an_empty_line() { .succeeds() .stdout_is(expected); } + +#[test] +fn test_missing_file_error_includes_filename() { + new_ucmd!() + .arg("missing.txt") + .fails_with_code(1) + .stderr_contains("missing.txt"); +} From 7509850f52f6d3a2e036853bd1faab5b97f69a0d Mon Sep 17 00:00:00 2001 From: Logic769 Date: Tue, 3 Nov 2026 08:10:29 +0800 Subject: [PATCH 2/5] paste: report file names for all input errors --- src/uu/paste/locales/en-US.ftl | 1 - src/uu/paste/src/paste.rs | 58 ++++++++++++++++++++++------------ tests/by-util/test_paste.rs | 14 +++++++- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/uu/paste/locales/en-US.ftl b/src/uu/paste/locales/en-US.ftl index 50aeb4a7d93..8dc74ad2d90 100644 --- a/src/uu/paste/locales/en-US.ftl +++ b/src/uu/paste/locales/en-US.ftl @@ -10,4 +10,3 @@ paste-help-zero-terminated = line delimiter is NUL, not newline # Error messages paste-error-delimiter-unescaped-backslash = delimiter list ends with an unescaped backslash: { $delimiters } paste-error-stdin-borrow = failed to access standard input: { $error } -paste-error-file = { $file }: { $error } diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 50c93b77bf4..7ede4b78684 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -12,7 +12,8 @@ use std::iter::Cycle; use std::path::Path; use std::rc::Rc; use std::slice::Iter; -use uucore::error::{UResult, USimpleError}; +use uucore::display::Quotable; +use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::format_usage; use uucore::i18n::charmap::mb_char_len; use uucore::line_ending::LineEnding; @@ -107,17 +108,9 @@ fn paste( ) } else { let path = Path::new(&filename); - let file = File::open(path).map_err(|error| { - USimpleError::new( - 1, - translate!( - "paste-error-file", - "file" => filename.to_string_lossy(), - "error" => error - ), - ) - })?; - InputSource::File(BufReader::new(file)) + let file = File::open(path) + .map_err_context(|| filename.as_os_str().maybe_quote().to_string())?; + InputSource::File(BufReader::new(file), filename) }; input_source_vec.push(input_source); @@ -145,14 +138,24 @@ fn paste( let mut delimiter_state = DelimiterState::new(&unescaped_and_encoded_delimiters); let mut output = Vec::new(); + let mut read_error: Option> = None; if serial { for input_source in &mut input_source_vec { output.clear(); - while let read @ 1.. = input_source.read_until(line_ending_byte, &mut output)? { - remove_trailing_line_ending_byte(line_ending_byte, read, &mut output); - delimiter_state.write_delimiter(&mut output); + loop { + match input_source.read_until(line_ending_byte, &mut output) { + Ok(0) => break, + Ok(read) => { + remove_trailing_line_ending_byte(line_ending_byte, read, &mut output); + delimiter_state.write_delimiter(&mut output); + } + Err(error) => { + read_error.get_or_insert(error); + break; + } + } } delimiter_state.remove_trailing_delimiter(&mut output); @@ -177,14 +180,19 @@ fn paste( if eof[i] { eof_count += 1; } else { - match input_source.read_until(line_ending_byte, &mut output)? { - 0 => { + match input_source.read_until(line_ending_byte, &mut output) { + Ok(0) => { eof[i] = true; eof_count += 1; } - read => { + Ok(read) => { remove_trailing_line_ending_byte(line_ending_byte, read, &mut output); } + Err(error) => { + read_error.get_or_insert(error); + eof[i] = true; + eof_count += 1; + } } } @@ -209,6 +217,10 @@ fn paste( } } + if let Some(error) = read_error { + return Err(error); + } + Ok(()) } @@ -385,14 +397,16 @@ impl<'a> DelimiterState<'a> { } enum InputSource { - File(BufReader), + File(BufReader, OsString), StandardInput(Rc>), } impl InputSource { fn read(&mut self, buf: &mut [u8]) -> UResult { let us = match self { - Self::File(bu) => bu.read(buf)?, + Self::File(reader, name) => reader + .read(buf) + .map_err_context(|| name.as_os_str().maybe_quote().to_string())?, Self::StandardInput(rc) => rc .try_borrow() .map_err(|bo| { @@ -407,7 +421,9 @@ impl InputSource { fn read_until(&mut self, byte: u8, buf: &mut Vec) -> UResult { let us = match self { - Self::File(bu) => bu.read_until(byte, buf)?, + Self::File(reader, name) => reader + .read_until(byte, buf) + .map_err_context(|| name.as_os_str().maybe_quote().to_string())?, Self::StandardInput(rc) => rc .try_borrow() .map_err(|bo| { diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 7792d71105c..5cfe93b5e8b 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -615,5 +615,17 @@ fn test_missing_file_error_includes_filename() { new_ucmd!() .arg("missing.txt") .fails_with_code(1) - .stderr_contains("missing.txt"); + .stderr_is("paste: missing.txt: No such file or directory\n"); +} + +#[test] +fn test_directory_error_includes_filename() { + let (at, mut ucmd) = at_and_ucmd!(); + at.write("a.txt", "a\n"); + at.mkdir("d"); + + ucmd.args(&["a.txt", "d"]) + .fails_with_code(1) + .stdout_is("a\t\n") + .stderr_is("paste: d: Is a directory\n"); } From bfb870643f5ab65976501493dd061ae30d3cc2d3 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Wed, 4 Nov 2026 01:58:12 +0800 Subject: [PATCH 3/5] paste: make directory error test portable --- tests/by-util/test_paste.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 5cfe93b5e8b..e20dc2fc6cd 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -624,8 +624,9 @@ fn test_directory_error_includes_filename() { at.write("a.txt", "a\n"); at.mkdir("d"); - ucmd.args(&["a.txt", "d"]) - .fails_with_code(1) - .stdout_is("a\t\n") - .stderr_is("paste: d: Is a directory\n"); + let result = ucmd.args(&["a.txt", "d"]).fails_with_code(1); + result.stderr_contains("paste: d:"); + if !cfg!(windows) { + result.stdout_is("a\t\n"); + } } From cd2fd8915ff98ba68d2288d815de5f20ed4813b2 Mon Sep 17 00:00:00 2001 From: Logic769 Date: Wed, 4 Nov 2026 18:22:38 +0800 Subject: [PATCH 4/5] paste: report each input read error and dedup name quoting Use show!() for read errors in both serial and parallel modes so every failing input is reported, as GNU does. Store the quoted file name once in InputSource instead of re-quoting it on each read. --- src/uu/paste/src/paste.rs | 26 ++++++++++---------------- tests/by-util/test_paste.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 7ede4b78684..208f8389582 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -13,10 +13,11 @@ use std::path::Path; use std::rc::Rc; use std::slice::Iter; use uucore::display::Quotable; -use uucore::error::{FromIo, UError, UResult, USimpleError}; +use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::i18n::charmap::mb_char_len; use uucore::line_ending::LineEnding; +use uucore::show; use uucore::translate; mod options { @@ -108,9 +109,9 @@ fn paste( ) } else { let path = Path::new(&filename); - let file = File::open(path) - .map_err_context(|| filename.as_os_str().maybe_quote().to_string())?; - InputSource::File(BufReader::new(file), filename) + let name = filename.as_os_str().maybe_quote().to_string(); + let file = File::open(path).map_err_context(|| name.clone())?; + InputSource::File(BufReader::new(file), name) }; input_source_vec.push(input_source); @@ -138,7 +139,6 @@ fn paste( let mut delimiter_state = DelimiterState::new(&unescaped_and_encoded_delimiters); let mut output = Vec::new(); - let mut read_error: Option> = None; if serial { for input_source in &mut input_source_vec { @@ -152,7 +152,7 @@ fn paste( delimiter_state.write_delimiter(&mut output); } Err(error) => { - read_error.get_or_insert(error); + show!(error); break; } } @@ -189,7 +189,7 @@ fn paste( remove_trailing_line_ending_byte(line_ending_byte, read, &mut output); } Err(error) => { - read_error.get_or_insert(error); + show!(error); eof[i] = true; eof_count += 1; } @@ -217,10 +217,6 @@ fn paste( } } - if let Some(error) = read_error { - return Err(error); - } - Ok(()) } @@ -397,16 +393,14 @@ impl<'a> DelimiterState<'a> { } enum InputSource { - File(BufReader, OsString), + File(BufReader, String), StandardInput(Rc>), } impl InputSource { fn read(&mut self, buf: &mut [u8]) -> UResult { let us = match self { - Self::File(reader, name) => reader - .read(buf) - .map_err_context(|| name.as_os_str().maybe_quote().to_string())?, + Self::File(reader, name) => reader.read(buf).map_err_context(|| name.clone())?, Self::StandardInput(rc) => rc .try_borrow() .map_err(|bo| { @@ -423,7 +417,7 @@ impl InputSource { let us = match self { Self::File(reader, name) => reader .read_until(byte, buf) - .map_err_context(|| name.as_os_str().maybe_quote().to_string())?, + .map_err_context(|| name.clone())?, Self::StandardInput(rc) => rc .try_borrow() .map_err(|bo| { diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index e20dc2fc6cd..1fdcb88135a 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -630,3 +630,29 @@ fn test_directory_error_includes_filename() { result.stdout_is("a\t\n"); } } + +#[test] +#[cfg(not(windows))] +fn test_all_directory_errors_are_reported() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("d1"); + at.mkdir("d2"); + + let result = ucmd.args(&["d1", "d2"]).fails_with_code(1); + result.stderr_contains("paste: d1:"); + result.stderr_contains("paste: d2:"); + result.stdout_is(""); +} + +#[test] +#[cfg(not(windows))] +fn test_all_directory_errors_are_reported_in_serial_mode() { + let (at, mut ucmd) = at_and_ucmd!(); + at.mkdir("d1"); + at.mkdir("d2"); + + let result = ucmd.args(&["-s", "d1", "d2"]).fails_with_code(1); + result.stderr_contains("paste: d1:"); + result.stderr_contains("paste: d2:"); + result.stdout_is("\n\n"); +} From d869199c11d1bdb3632e07fce023af92ef0bf0ea Mon Sep 17 00:00:00 2001 From: Logic769 Date: Wed, 4 Nov 2026 21:52:21 +0800 Subject: [PATCH 5/5] paste: use no_stdout in directory error test --- tests/by-util/test_paste.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 1fdcb88135a..49b25ebb766 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -641,7 +641,7 @@ fn test_all_directory_errors_are_reported() { let result = ucmd.args(&["d1", "d2"]).fails_with_code(1); result.stderr_contains("paste: d1:"); result.stderr_contains("paste: d2:"); - result.stdout_is(""); + result.no_stdout(); } #[test]