diff --git a/src/uu/wc/src/wc.rs b/src/uu/wc/src/wc.rs index a7fea0519e9..ddd548417a2 100644 --- a/src/uu/wc/src/wc.rs +++ b/src/uu/wc/src/wc.rs @@ -707,14 +707,15 @@ fn word_count_from_input(input: &Input<'_>, settings: &Settings) -> CountResult /// Compute the number of digits needed to represent all counts in all inputs. /// -/// For [`Inputs::Stdin`], [`MINIMUM_WIDTH`] is returned, unless there is only one counter number -/// to be printed, in which case 1 is returned. +/// For [`Inputs::Stdin`], the width is derived from the size of stdin when stdin is a regular +/// file, and [`MINIMUM_WIDTH`] otherwise; if there is only one counter number to be printed, +/// 1 is returned. /// /// For [`Inputs::Files0From`], [`MINIMUM_WIDTH`] is returned. /// /// An [`Inputs::Paths`] may include zero or more "-" entries, each of which represents reading -/// from `stdin`. The presence of any such entry causes this function to return a width that is at -/// least [`MINIMUM_WIDTH`]. +/// from `stdin`. Such an entry contributes the size of stdin when stdin is a regular file, and +/// otherwise causes this function to return a width that is at least [`MINIMUM_WIDTH`]. /// /// If an [`Inputs::Paths`] contains only one path and only one number needs to be printed then /// this function is optimized to return 1 without making any calls to get file metadata. @@ -725,7 +726,10 @@ fn word_count_from_input(input: &Input<'_>, settings: &Settings) -> CountResult fn compute_number_width(inputs: &Inputs, settings: &Settings) -> usize { match inputs { Inputs::Stdin if settings.number_enabled() == 1 => 1, - Inputs::Stdin => MINIMUM_WIDTH, + Inputs::Stdin => match stdin_size() { + Some(size) => width_of(size, 1), + None => MINIMUM_WIDTH, + }, Inputs::Files0From(_) => 1, Inputs::Paths(inputs) => { if settings.number_enabled() == 1 && inputs.len() == 1 { @@ -736,7 +740,10 @@ fn compute_number_width(inputs: &Inputs, settings: &Settings) -> usize { let mut total: u64 = 0; for input in inputs { match input { - Input::Stdin(_) => minimum_width = MINIMUM_WIDTH, + Input::Stdin(_) => match stdin_size() { + Some(size) => total += size, + None => minimum_width = MINIMUM_WIDTH, + }, Input::Path(path) => { if let Ok(meta) = fs::metadata(path) { if meta.is_file() { @@ -749,18 +756,46 @@ fn compute_number_width(inputs: &Inputs, settings: &Settings) -> usize { } } - if total == 0 { - minimum_width - } else { - let total_width = (1 + total.ilog10()) - .try_into() - .expect("ilog of a u64 should fit into a usize"); - max(total_width, minimum_width) - } + width_of(total, minimum_width) } } } +/// The width in digits of `total`, but never less than `minimum_width`. +/// +/// A `total` of zero carries no information about the counts, so it leaves +/// `minimum_width` untouched. +fn width_of(total: u64, minimum_width: usize) -> usize { + if total == 0 { + minimum_width + } else { + let total_width = (1 + total.ilog10()) + .try_into() + .expect("ilog of a u64 should fit into a usize"); + max(total_width, minimum_width) + } +} + +/// The size of stdin, if stdin refers to a regular file. +/// +/// Counts read from a stream of unknown length are formatted with +/// [`MINIMUM_WIDTH`], but when stdin is a regular file its size is known up +/// front and dictates the width the same way a named file's size does. +fn stdin_size() -> Option { + #[cfg(unix)] + use std::os::fd::AsFd; + #[cfg(windows)] + use std::os::windows::io::AsHandle; + + let stdin = io::stdin(); + #[cfg(unix)] + let cloned = stdin.as_fd().try_clone_to_owned().ok()?; + #[cfg(windows)] + let cloned = stdin.as_handle().try_clone_to_owned().ok()?; + let metadata = File::from(cloned).metadata().ok()?; + metadata.is_file().then_some(metadata.len()) +} + type InputIterItem<'a> = Result, Box>; /// To be used with `--files0-from=-`, this applies a filter on the results of [`files0_iter`] to diff --git a/tests/by-util/test_wc.rs b/tests/by-util/test_wc.rs index 00a542fe0d9..599d0605fd4 100644 --- a/tests/by-util/test_wc.rs +++ b/tests/by-util/test_wc.rs @@ -375,6 +375,45 @@ fn test_file_one_long_word() { .stdout_is(" 1 1 10001 10001 10000 onelongword.txt\n"); } +/// Test that the size of stdin dictates the display width when stdin is a +/// regular file. +/// +/// A stream of unknown length falls back to the minimum width, but a regular +/// file redirected onto stdin has a size known up front, so it dictates the +/// width just like a named file does. +#[test] +// `at_and_ucmd!` is only imported on unix in this file, and the wasm targets +// have no meaningful stdin file to redirect. +#[cfg(unix)] +fn test_stdin_size_dictates_width() { + use std::fs::File; + + // lorem_ipsum.txt contains 772 bytes, a width of 3. + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.set_stdin(File::open(at.plus("lorem_ipsum.txt")).unwrap()) + .succeeds() + .stdout_is(" 13 109 772\n"); + + // The same content piped in has no size known in advance, so the counts + // are padded to the minimum width instead. + new_ucmd!() + .pipe_in_fixture("lorem_ipsum.txt") + .succeeds() + .stdout_is(" 13 109 772\n"); + + // "-" contributes the size of stdin to the total, so 772 + 302 = 1074 + // bytes give a width of 4. + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-", "alice_in_wonderland.txt"]) + .set_stdin(File::open(at.plus("lorem_ipsum.txt")).unwrap()) + .succeeds() + .stdout_is(concat!( + " 13 109 772 -\n", + " 5 57 302 alice_in_wonderland.txt\n", + " 18 166 1074 total\n", + )); +} + /// Test that the total size of all the files in the input dictates /// the display width. ///