From 4b897fc187cc87e4664cf13fc1dd98ccf822713f Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:40 +0100 Subject: [PATCH 01/29] uucore: share stdbuf-aware stdout buffering --- src/uucore/Cargo.toml | 5 +- src/uucore/src/lib/lib.rs | 8 +- src/uucore/src/lib/mods.rs | 3 +- src/uucore/src/lib/mods/display.rs | 14 ++- src/uucore/src/lib/mods/io.rs | 4 +- src/uucore/src/lib/mods/output.rs | 143 +++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 src/uucore/src/lib/mods/output.rs diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 764c5b0c6cb..76652ffad81 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -102,8 +102,8 @@ bstr = { workspace = true, optional = true } [target.'cfg(any(target_vendor = "apple", target_os = "cygwin", target_os = "freebsd", target_os = "linux", target_os = "netbsd"))'.dependencies] dns-lookup = { workspace = true, optional = true } -[target.'cfg(any(unix, windows, target_os = "wasi"))'.dependencies] -rustix = { workspace = true, optional = true } +[target.'cfg(any(unix, target_os = "wasi"))'.dependencies] +rustix = { workspace = true, features = ["std"] } [target.'cfg(unix)'.dependencies] # utmpx is unix-only, so its dependencies must not be pulled into other targets @@ -120,6 +120,7 @@ walkdir = { workspace = true, optional = true } procfs = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] +rustix = { workspace = true, optional = true } wild = { workspace = true } windows-sys = { workspace = true, optional = true, default-features = false, features = [ "Wdk_System_SystemInformation", diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index dbb055df37d..9ce476aaa19 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind +// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind ohos //! library ~ (core/bundler file) @@ -39,9 +39,11 @@ mod mods; // core cross-platform modules pub use uucore_procs::*; // * cross-platform modules -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(any(unix, windows, target_os = "wasi"))] pub use crate::mods::io; -pub use crate::mods::{clap_localization, display, error, line_ending, locale, os, panic, posix}; +pub use crate::mods::{ + clap_localization, display, error, line_ending, locale, os, output, panic, posix, +}; // * feature-gated modules #[cfg(feature = "backup-control")] diff --git a/src/uucore/src/lib/mods.rs b/src/uucore/src/lib/mods.rs index 2d5c94a4be6..fb2c10f62a3 100644 --- a/src/uucore/src/lib/mods.rs +++ b/src/uucore/src/lib/mods.rs @@ -8,10 +8,11 @@ pub mod clap_localization; pub mod display; pub mod error; -#[cfg(all(feature = "fs", any(unix, windows, target_os = "wasi")))] +#[cfg(any(unix, windows, target_os = "wasi"))] pub mod io; pub mod line_ending; pub mod locale; pub mod os; +pub mod output; pub mod panic; pub mod posix; diff --git a/src/uucore/src/lib/mods/display.rs b/src/uucore/src/lib/mods/display.rs index 50174af6f13..13309ec1361 100644 --- a/src/uucore/src/lib/mods/display.rs +++ b/src/uucore/src/lib/mods/display.rs @@ -27,7 +27,11 @@ use std::ffi::OsStr; use std::fs::File; +#[cfg(any(unix, windows))] +use std::io::LineWriter; use std::io::{self, BufWriter, Stdout, StdoutLock, Write as _}; +#[cfg(any(unix, target_os = "wasi"))] +use std::os::fd::AsFd; use std::{env, fmt}; // These used to be defined here, but they live in their own crate now. @@ -108,9 +112,17 @@ impl OsWrite for StdoutLock<'_> {} // doing a raw write. impl OsWrite for BufWriter {} +#[cfg(any(unix, windows))] +// Keep OS-string output available through the line-buffering wrapper. +impl OsWrite for LineWriter {} + +#[cfg(any(unix, target_os = "wasi"))] +impl OsWrite for crate::io::RawWriter {} + impl OsWrite for Box { fn write_all_os(&mut self, buf: &OsStr) -> io::Result<()> { - let this: &mut dyn OsWrite = self; + // Dereference the box before coercing, or dispatch would recurse here. + let this: &mut dyn OsWrite = &mut **self; this.write_all_os(buf) } } diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index b3b994d0426..20cddbbd07c 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -28,8 +28,8 @@ type NativeType = OwnedHandle; #[cfg(not(windows))] type NativeType = OwnedFd; -// create reader without buffering #[cfg(any(unix, target_os = "wasi"))] +/// Reads directly from the underlying file descriptor without a userspace buffer. pub struct RawReader(pub T); #[cfg(any(unix, target_os = "wasi"))] impl io::Read for RawReader { @@ -38,8 +38,8 @@ impl io::Read for RawReader { } } -// create writer without buffering #[cfg(any(unix, target_os = "wasi"))] +/// Writes directly to the underlying file descriptor without a userspace buffer. pub struct RawWriter(pub T); #[cfg(any(unix, target_os = "wasi"))] impl io::Write for RawWriter { diff --git a/src/uucore/src/lib/mods/output.rs b/src/uucore/src/lib/mods/output.rs new file mode 100644 index 00000000000..04a04b450fc --- /dev/null +++ b/src/uucore/src/lib/mods/output.rs @@ -0,0 +1,143 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Output buffering shared by utilities that write to standard output. + +#[cfg(unix)] +use std::io::LineWriter; +use std::io::{self, BufWriter, Write}; +#[cfg(any(unix, target_os = "wasi"))] +use std::os::fd::AsFd; + +#[cfg(any(unix, target_os = "wasi"))] +use crate::io::RawWriter; + +enum BufferMode { + Default, + #[cfg(unix)] + Unbuffered, + #[cfg(unix)] + Line, + #[cfg(unix)] + Block(usize), +} + +#[cfg(unix)] +fn requested_mode() -> BufferMode { + let Some(value) = std::env::var_os("_STDBUF_O") else { + return BufferMode::Default; + }; + + match value.to_str() { + Some("0") => BufferMode::Unbuffered, + Some("L") => BufferMode::Line, + // Invalid values, including an empty string, use the normal buffer. + Some(value) => value.parse().map_or(BufferMode::Default, BufferMode::Block), + None => BufferMode::Default, + } +} + +// `stdbuf` currently uses a preload library on Unix. Windows and WASI do not +// have a corresponding wrapper, so they keep the normal buffering behavior. +#[cfg(not(unix))] +fn requested_mode() -> BufferMode { + BufferMode::Default +} + +/// A buffered writer for stdout that honors the output mode requested by +/// `stdbuf` on supported platforms. +pub enum StdoutBuffer { + #[cfg(unix)] + Unbuffered(W), + #[cfg(unix)] + Line(LineWriter), + Block(BufWriter), +} + +impl StdoutBuffer { + /// Create a writer using the standard buffer capacity unless `stdbuf` + /// requested another output mode. + pub fn new(writer: W) -> Self { + // Resolve the mode once so callers can keep hot output loops generic. + match requested_mode() { + BufferMode::Default => Self::Block(BufWriter::new(writer)), + #[cfg(unix)] + BufferMode::Unbuffered => Self::Unbuffered(writer), + #[cfg(unix)] + BufferMode::Line => Self::Line(LineWriter::new(writer)), + #[cfg(unix)] + BufferMode::Block(size) => Self::Block(BufWriter::with_capacity(size, writer)), + } + } + + /// Create a writer with `capacity` as its normal buffer capacity unless + /// `stdbuf` requested another output mode. + pub fn with_capacity(capacity: usize, writer: W) -> Self { + match requested_mode() { + BufferMode::Default => Self::Block(BufWriter::with_capacity(capacity, writer)), + #[cfg(unix)] + BufferMode::Unbuffered => Self::Unbuffered(writer), + #[cfg(unix)] + BufferMode::Line => Self::Line(LineWriter::new(writer)), + #[cfg(unix)] + BufferMode::Block(size) => Self::Block(BufWriter::with_capacity(size, writer)), + } + } + + /// Use a raw writer for the unbuffered output mode where it is available. + #[cfg(any(unix, target_os = "wasi"))] + pub fn into_unbuffered(writer: W) -> RawWriter + where + W: AsFd, + { + RawWriter(writer) + } + + /// Keep the underlying writer on platforms without raw descriptor I/O. + #[cfg(not(any(unix, target_os = "wasi")))] + pub fn into_unbuffered(writer: W) -> W { + writer + } +} + +#[cfg(unix)] +impl Write for StdoutBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + Self::Unbuffered(writer) => RawWriter(&*writer).write(buf), + Self::Line(writer) => writer.write(buf), + Self::Block(writer) => writer.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + Self::Unbuffered(_) => Ok(()), + Self::Line(writer) => writer.flush(), + Self::Block(writer) => writer.flush(), + } + } +} + +#[cfg(not(unix))] +impl Write for StdoutBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + match self { + Self::Block(writer) => writer.write(buf), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self { + Self::Block(writer) => writer.flush(), + } + } +} + +#[cfg(unix)] +impl crate::display::OsWrite for StdoutBuffer {} + +#[cfg(not(unix))] +impl crate::display::OsWrite for StdoutBuffer {} From 2bf2391d0484b907e4ac1bb8fe4125fa63eacd0c Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:49 +0100 Subject: [PATCH 02/29] base32: respect stdbuf on stdout --- src/uu/base32/src/base_common.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index 0519f2ee0b1..64005e4c3e8 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -8,7 +8,7 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::io::{self, BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::encoding::{ @@ -196,7 +196,8 @@ pub fn handle_input(input: &mut R, format: Format, config: Config) - ), // Batch Base16's small encoded chunks to reduce write syscalls. (Format::Base16, false) => { - let mut output = BufWriter::with_capacity(DEFAULT_BUF_SIZE, &mut stdout_lock); + let mut output = + uucore::output::StdoutBuffer::with_capacity(DEFAULT_BUF_SIZE, &mut stdout_lock); let result = fast_encode::fast_encode_stream( input, &mut output, From 6359f357d079992222354810aa7b26f4a95db3da Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:51 +0100 Subject: [PATCH 03/29] cat: respect stdbuf on stdout --- src/uu/cat/src/cat.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index 81ad827368d..4ebbe8090b4 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -12,7 +12,7 @@ use clap::{Arg, ArgAction, Command}; use memchr::memchr2; use std::ffi::OsString; use std::fs::{File, metadata}; -use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write}; +use std::io::{self, ErrorKind, IsTerminal, Read, Write}; #[cfg(any(unix, target_os = "wasi"))] use std::os::fd::AsFd; #[cfg(unix)] @@ -542,7 +542,7 @@ fn print_lines( let stdout = io::stdout(); let stdout = stdout.lock(); // Add a 32K buffer for stdout - this greatly improves performance. - let mut writer = BufWriter::with_capacity(32 * 1024, stdout); + let mut writer = uucore::output::StdoutBuffer::with_capacity(32 * 1024, stdout); loop { let n = match handle.reader.read(&mut in_buf) { From b49f62fe0e833806a42c6c3308504a8ed750c90a Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:51 +0100 Subject: [PATCH 04/29] comm: respect stdbuf on stdout --- src/uu/comm/src/comm.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index 92dc6df3e9a..bba7e45d444 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -8,7 +8,7 @@ use std::cmp::Ordering; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Stdin, Write, stderr, stdin}; +use std::io::{self, BufRead, BufReader, Stdin, Write, stderr, stdin}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -199,7 +199,7 @@ fn comm( let delim_col_2 = delim.repeat(width_col_1); let delim_col_3 = delim.repeat(width_col_1 + width_col_2); - let mut writer = BufWriter::new(io::stdout().lock()); + let mut writer = uucore::output::StdoutBuffer::new(io::stdout().lock()); let ra = &mut Vec::new(); let mut na = a From 5ad7b4a9248780250433bdbc279a638b31c7a9cf Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:51 +0100 Subject: [PATCH 05/29] cp: respect stdbuf on stdout --- src/uu/cp/src/cp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 8fe2f0b1a14..88bb90118fc 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2530,7 +2530,7 @@ fn print_paths(parents: bool, source: &Path, dest: &Path) -> CopyResult<()> { // Buffer the output so a failed write (e.g. stdout redirected to a full // disk) surfaces as one error instead of panicking inside println!. - let mut out = io::BufWriter::new(io::stdout().lock()); + let mut out = uucore::output::StdoutBuffer::new(io::stdout().lock()); let write_err = |e| CpError::IoErrContext(e, translate!("cp-error-write")); if parents { // For example, if copying file `a/b/c` and its parents From 38bee6f07fd02580a24180bb910136d7992c6079 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:51 +0100 Subject: [PATCH 06/29] cut: respect stdbuf on stdout --- src/uu/cut/src/cut.rs | 46 +++++++++++++++++++++++++++++---------- tests/by-util/test_cut.rs | 10 +++++++++ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 3cb873732ea..418cb783969 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -10,13 +10,14 @@ use clap::builder::{PossibleValue, ValueParser}; use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, IsTerminal, Read, Write, stdin, stdout}; +use std::io::{BufRead, BufReader, IsTerminal, Read, Write, stdin, stdout}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError, set_exit_code, strip_errno}; use uucore::i18n::charmap::{Encoding, locale_encoding, mb_char_len}; use uucore::line_ending::LineEnding; use uucore::os_str_as_bytes; +use uucore::output::StdoutBuffer; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use self::searcher::Searcher; @@ -923,13 +924,32 @@ fn cut_fields( fn cut_files<'a, I>(filenames: I, mode: &Mode) where I: IntoIterator, +{ + if stdout().is_terminal() { + let mut out = stdout(); + cut_files_with_output(filenames, mode, &mut out); + return; + } + + let output = StdoutBuffer::new(stdout()); + match output { + StdoutBuffer::Block(mut out) => cut_files_with_output(filenames, mode, &mut out), + #[cfg(unix)] + StdoutBuffer::Unbuffered(out) => { + let mut out = StdoutBuffer::into_unbuffered(out); + cut_files_with_output(filenames, mode, &mut out); + } + #[cfg(unix)] + StdoutBuffer::Line(mut out) => cut_files_with_output(filenames, mode, &mut out), + } +} + +fn cut_files_with_output<'a, I, W>(filenames: I, mode: &Mode, out: &mut W) +where + I: IntoIterator, + W: Write, { let mut stdin_read = false; - let mut out: Box = if stdout().is_terminal() { - Box::new(stdout()) - } else { - Box::new(BufWriter::new(stdout())) as Box - }; for filename in filenames { if filename == "-" { @@ -938,9 +958,11 @@ where } show_if_err!(match mode { - Mode::Bytes(ranges, opts) => cut_chars(stdin(), &mut out, ranges, opts, false), - Mode::Characters(ranges, opts) => cut_chars(stdin(), &mut out, ranges, opts, true), - Mode::Fields(ranges, opts) => cut_fields(stdin(), &mut out, ranges, opts), + Mode::Bytes(ranges, opts) => cut_chars(stdin(), &mut *out, ranges, opts, false), + Mode::Characters(ranges, opts) => { + cut_chars(stdin(), &mut *out, ranges, opts, true) + } + Mode::Fields(ranges, opts) => cut_fields(stdin(), &mut *out, ranges, opts), }); stdin_read = true; @@ -963,12 +985,12 @@ where .and_then(|file| { match &mode { Mode::Bytes(ranges, opts) => { - cut_chars(file, &mut out, ranges, opts, false) + cut_chars(file, &mut *out, ranges, opts, false) } Mode::Characters(ranges, opts) => { - cut_chars(file, &mut out, ranges, opts, true) + cut_chars(file, &mut *out, ranges, opts, true) } - Mode::Fields(ranges, opts) => cut_fields(file, &mut out, ranges, opts), + Mode::Fields(ranges, opts) => cut_fields(file, &mut *out, ranges, opts), } }) ); diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 26ea8210787..9c19fc0d8ad 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -50,6 +50,16 @@ fn test_no_args() { .stderr_contains("cut: you must specify a list of bytes, characters, or fields"); } +#[test] +fn test_empty_stdbuf_mode() { + new_ucmd!() + .env("_STDBUF_O", "") + .args(&["-b", "1-3"]) + .pipe_in("abcdef\n") + .succeeds() + .stdout_is("abc\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); From e354bce201b835d9fd3be7c1ed04d200ba67a2cb Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:51 +0100 Subject: [PATCH 07/29] date: respect stdbuf on stdout --- src/uu/date/src/date.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index b5b210a6f44..f483a1ac6e7 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Read, Write, stderr}; +use std::io::{BufRead, BufReader, Read, Write, stderr}; use std::path::PathBuf; use std::sync::OnceLock; use uucore::display::Quotable; @@ -28,6 +28,7 @@ use uucore::error::{UError, UResult, strip_errno}; #[cfg(feature = "i18n-datetime")] use uucore::i18n::datetime::{localize_format_string, should_use_icu_locale}; use uucore::i18n::get_ctype_encoding; +use uucore::output::StdoutBuffer; use uucore::quoting_style::{QuotingStyle, escape_bytes}; use uucore::translate; use uucore::translate_text; @@ -663,7 +664,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let output = OutputContext::new(&settings); - let mut stdout = BufWriter::new(std::io::stdout().lock()); + let mut stdout = StdoutBuffer::new(std::io::stdout().lock()); // Format all the dates for date in dates { @@ -1135,7 +1136,7 @@ fn set_and_echo(input: &str, now: &Zoned, settings: &Settings) -> UResult<()> { ))); let output = OutputContext::new(settings); - let mut stdout = BufWriter::new(std::io::stdout().lock()); + let mut stdout = StdoutBuffer::new(std::io::stdout().lock()); write_formatted_date(&mut stdout, date, &output, settings)?; stdout.flush().map_err(DateError::Write)?; Ok(()) From ca37ada7a05eba2e6b6d53d6c04135753aad5b5b Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:52 +0100 Subject: [PATCH 08/29] df: respect stdbuf on stdout --- src/uu/df/src/df.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 022b61d1196..3d26325238d 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -25,7 +25,7 @@ use uucore::{format_usage, show, show_warning}; use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource}; use std::ffi::OsString; -use std::io::{BufWriter, Write, stdout}; +use std::io::{Write, stdout}; use std::path::Path; use crate::blocks::{BlockSize, read_block_size}; @@ -505,7 +505,7 @@ pub fn df(paths: Option<&[&Path]>, opt: &Options) -> UResult<()> { return Ok(()); } - let mut writer = BufWriter::new(stdout().lock()); + let mut writer = uucore::output::StdoutBuffer::new(stdout().lock()); write_table(&mut writer, filesystems, opt)?; // `BufWriter` swallows errors per drop, so flush explicitly. From 4fc4c061304a5eb59edf7aaf620c2b19fc170da8 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:52 +0100 Subject: [PATCH 09/29] expand: respect stdbuf on stdout --- src/uu/expand/src/expand.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index 1c44756e223..125ed2ecef7 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -8,13 +8,14 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; +use std::io::{BufRead, BufReader, Read, Write, stdin, stdout}; use std::num::IntErrorKind; use std::path::Path; use std::str::from_utf8; use uucore::char_width::char_width_at; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, set_exit_code}; +use uucore::output::StdoutBuffer; use uucore::{format_usage, show, translate}; pub mod options { @@ -366,9 +367,9 @@ fn write_spaces(output: &mut impl Write, mut n: usize) -> std::io::Result<()> { Ok(()) } -fn expand_buf( +fn expand_buf( buf: &[u8], - output: &mut BufWriter, + output: &mut W, tabstops: &[usize], options: &Options, col: &mut usize, @@ -444,11 +445,7 @@ fn expand_buf( Ok(()) } -fn expand_file( - file: &OsString, - output: &mut BufWriter, - options: &Options, -) -> UResult<()> { +fn expand_file(file: &OsString, output: &mut W, options: &Options) -> UResult<()> { let mut input = open(file)?; let ts = options.tabstops.as_ref(); let mut col = 0; @@ -467,11 +464,9 @@ fn expand_file( Ok(()) } -fn expand(options: &Options) -> UResult<()> { - let mut output = BufWriter::new(stdout()); - +fn expand_with_output(options: &Options, output: &mut W) -> UResult<()> { for file in &options.files { - if let Err(e) = expand_file(file, &mut output, options) { + if let Err(e) = expand_file(file, output, options) { show!(e); set_exit_code(1); } @@ -483,6 +478,20 @@ fn expand(options: &Options) -> UResult<()> { Ok(()) } +fn expand(options: &Options) -> UResult<()> { + let output = StdoutBuffer::new(stdout()); + match output { + StdoutBuffer::Block(mut output) => expand_with_output(options, &mut output), + #[cfg(unix)] + StdoutBuffer::Unbuffered(output) => { + let mut output = StdoutBuffer::into_unbuffered(output); + expand_with_output(options, &mut output) + } + #[cfg(unix)] + StdoutBuffer::Line(mut output) => expand_with_output(options, &mut output), + } +} + #[cfg(test)] mod tests { use crate::is_digit_or_comma; From 91b07e189f86a4c51105fceef55e2a02a61ec781 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:52 +0100 Subject: [PATCH 10/29] factor: respect stdbuf on stdout --- src/uu/factor/src/factor.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/uu/factor/src/factor.rs b/src/uu/factor/src/factor.rs index 9ebabf9ef08..d6ad8841c41 100644 --- a/src/uu/factor/src/factor.rs +++ b/src/uu/factor/src/factor.rs @@ -40,11 +40,7 @@ enum Number { BigUint(BigUint), } -fn write_factors_str( - num_str: &[u8], - w: &mut io::BufWriter, - print_exponents: bool, -) -> UResult<()> { +fn write_factors_str(num_str: &[u8], w: &mut impl Write, print_exponents: bool) -> UResult<()> { let parsed = parse_num(num_str); show_if_err!(&parsed); let Ok(x) = parsed else { @@ -128,7 +124,7 @@ impl Display for NumError<'_> { /// Writing out the prime factors for integers fn write_result( - w: &mut io::BufWriter, + w: &mut impl Write, x: &impl Display, factorization: BTreeMap, print_exponents: bool, @@ -158,7 +154,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let stdout = stdout(); // use a smaller buffer here to pass a GNU test. - let mut w = io::BufWriter::with_capacity(4 * 1024, stdout.lock()); + let mut w = uucore::output::StdoutBuffer::with_capacity(4 * 1024, stdout.lock()); if let Some(values) = matches.get_many::(options::NUMBER) { for number in values { From e23ecc73fd8873dd4b6a775c9f12fa7123f61607 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:52 +0100 Subject: [PATCH 11/29] fmt: respect stdbuf on stdout --- src/uu/fmt/src/fmt.rs | 6 +++--- src/uu/fmt/src/linebreak.rs | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index e8338c8c22b..2c237b0a535 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -8,7 +8,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; +use std::io::{BufReader, Read, Stdout, Write, stdin, stdout}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; @@ -203,7 +203,7 @@ impl FmtOptions { fn process_file( file_name: &OsString, fmt_opts: &FmtOptions, - ostream: &mut BufWriter, + ostream: &mut uucore::output::StdoutBuffer, ) -> UResult<()> { let mut fp = BufReader::new(if file_name == "-" { Box::new(stdin()) as Box @@ -404,7 +404,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let fmt_opts = FmtOptions::from_matches(&matches)?; - let mut ostream = BufWriter::new(stdout()); + let mut ostream = uucore::output::StdoutBuffer::new(stdout()); for file_name in &files { process_file(file_name, &fmt_opts, &mut ostream)?; diff --git a/src/uu/fmt/src/linebreak.rs b/src/uu/fmt/src/linebreak.rs index 4e6e336084f..bfb74e3d4eb 100644 --- a/src/uu/fmt/src/linebreak.rs +++ b/src/uu/fmt/src/linebreak.rs @@ -5,7 +5,7 @@ // spell-checker:ignore (ToDO) INFTY MULT accum breakwords linebreak linebreaking linebreaks linelen maxlength minlength nchars ostream overlen parasplit plass posn powf punct signum slen sstart tabwidth tlen underlen winfo wlen wordlen -use std::io::{BufWriter, Stdout, Write}; +use std::io::{Stdout, Write}; use std::mem; use crate::FmtOptions; @@ -17,7 +17,7 @@ struct BreakArgs<'a> { indent: &'a [u8], indent_len: usize, uniform: bool, - ostream: &'a mut BufWriter, + ostream: &'a mut uucore::output::StdoutBuffer, } impl BreakArgs<'_> { @@ -48,7 +48,7 @@ impl BreakArgs<'_> { pub fn break_lines( para: &Paragraph, opts: &FmtOptions, - ostream: &mut BufWriter, + ostream: &mut uucore::output::StdoutBuffer, ) -> std::io::Result<()> { // indent let p_indent = ¶.indent_str; @@ -510,7 +510,10 @@ fn slice_if_fresh<'a>( } /// Write a newline and add the indent. -fn write_newline(indent: &[u8], ostream: &mut BufWriter) -> std::io::Result<()> { +fn write_newline( + indent: &[u8], + ostream: &mut uucore::output::StdoutBuffer, +) -> std::io::Result<()> { ostream.write_all(b"\n")?; ostream.write_all(indent) } @@ -519,7 +522,7 @@ fn write_newline(indent: &[u8], ostream: &mut BufWriter) -> std::io::Res fn write_with_spaces( word: &[u8], slen: usize, - ostream: &mut BufWriter, + ostream: &mut uucore::output::StdoutBuffer, ) -> std::io::Result<()> { if slen == 2 { ostream.write_all(b" ")?; From de4eda2d40cb2cd90ca2a5d9638aa490d124f74d Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:53 +0100 Subject: [PATCH 12/29] fold: respect stdbuf on stdout --- src/uu/fold/src/fold.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/uu/fold/src/fold.rs b/src/uu/fold/src/fold.rs index 4a75bd2d5dc..271c00bb6ae 100644 --- a/src/uu/fold/src/fold.rs +++ b/src/uu/fold/src/fold.rs @@ -8,13 +8,14 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; +use std::io::{BufRead, BufReader, Read, Write, stdin, stdout}; use std::num::IntErrorKind; use std::path::Path; use unicode_width::UnicodeWidthChar; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; +use uucore::output::StdoutBuffer; use uucore::show; use uucore::translate; @@ -91,7 +92,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { None => vec![OsString::from("-")], }; - fold(&files, bytes, characters, spaces, width) + let output = StdoutBuffer::new(stdout()); + match output { + StdoutBuffer::Block(mut output) => { + fold(&files, bytes, characters, spaces, width, &mut output) + } + #[cfg(unix)] + StdoutBuffer::Unbuffered(output) => { + let mut output = StdoutBuffer::into_unbuffered(output); + fold(&files, bytes, characters, spaces, width, &mut output) + } + #[cfg(unix)] + StdoutBuffer::Line(mut output) => { + fold(&files, bytes, characters, spaces, width, &mut output) + } + } } pub fn uu_app() -> Command { @@ -217,15 +232,14 @@ fn handle_obsolete(args: &[OsString]) -> Vec { result } -fn fold( +fn fold( filenames: &[OsString], bytes: bool, characters: bool, spaces: bool, width: usize, + output: &mut W, ) -> UResult<()> { - let mut output = BufWriter::new(stdout()); - for filename in filenames { let mut stdin_buf; let mut file_buf; @@ -247,14 +261,14 @@ fn fold( }); if bytes { - fold_file_bytewise(buffer, spaces, width, &mut output)?; + fold_file_bytewise(buffer, spaces, width, output)?; } else { let mode = if characters { WidthMode::Characters } else { WidthMode::Columns }; - fold_file(buffer, spaces, width, mode, &mut output)?; + fold_file(buffer, spaces, width, mode, output)?; } } From 7987b2104651db601908e62a61a021d20e0fef26 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:53 +0100 Subject: [PATCH 13/29] head: respect stdbuf on stdout --- src/uu/head/src/head.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 8164d28f769..9bfad5dc496 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -9,7 +9,7 @@ use clap::ArgMatches; use memchr::memrchr_iter; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; +use std::io::{self, Read, Seek, SeekFrom, Write}; use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::AsFd; @@ -243,7 +243,7 @@ fn print_n_lines(input: &mut impl io::BufRead, n: u64, separator: u8) -> io::Res // Write those bytes to `stdout`. let stdout = io::stdout(); let stdout = stdout.lock(); - let mut writer = BufWriter::with_capacity(BUF_SIZE, stdout); + let mut writer = uucore::output::StdoutBuffer::with_capacity(BUF_SIZE, stdout); let bytes_written = io::copy(&mut reader, &mut writer).map_err(wrap_in_stdout_error)?; From 88a408c0d19a4b9daacf15f88a53125ccb0b1791 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:53 +0100 Subject: [PATCH 14/29] join: respect stdbuf on stdout --- src/uu/join/src/join.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index bfe58f11d8e..7c7acc427a3 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -11,7 +11,7 @@ use memchr::{Memchr3, memchr_iter, memmem::Finder}; use std::cmp::Ordering; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Split, Stdin, Write, stdin, stdout}; +use std::io::{self, BufRead, BufReader, Split, Stdin, Write, stdin, stdout}; use std::num::IntErrorKind; #[cfg(unix)] use std::os::unix::ffi::OsStrExt; @@ -1118,7 +1118,7 @@ fn exec( let repr = Repr::new(settings.line_ending, sep, format, &settings.empty); let stdout = stdout(); - let mut writer = BufWriter::new(stdout.lock()); + let mut writer = uucore::output::StdoutBuffer::new(stdout.lock()); if settings.headers { state1.write_headers(&mut writer, &state2, &repr)?; From bed71970deeb4bd367b99ca47dd8c2d13d85749b Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:53 +0100 Subject: [PATCH 15/29] kill: respect stdbuf on stdout --- src/uu/kill/src/kill.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uu/kill/src/kill.rs b/src/uu/kill/src/kill.rs index b40ed88ac14..6cc6a8a27bd 100644 --- a/src/uu/kill/src/kill.rs +++ b/src/uu/kill/src/kill.rs @@ -8,7 +8,7 @@ #![cfg(not(target_os = "fuchsia"))] use clap::{Arg, ArgAction, Command}; -use std::io::{self, BufWriter, Write}; +use std::io::{self, Write}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError, strip_errno}; use uucore::translate; @@ -172,7 +172,7 @@ fn handle_obsolete(args: &mut Vec) -> UResult> { fn table() -> UResult<()> { // Buffer the listing so a failed write surfaces as one clean error at flush // rather than the runtime's implicit-flush message on top of ours. - let mut out = BufWriter::new(io::stdout().lock()); + let mut out = uucore::output::StdoutBuffer::new(io::stdout().lock()); for signal_value in 0..=signal_number_upper_bound() { if let Some(signal_name) = signal_list_name_by_value(signal_value) { writeln!(out, "{signal_value: >#2} {signal_name}").map_err(KillError::Write)?; @@ -215,14 +215,14 @@ fn print_signal(signal_name_or_value: &str) -> UResult<()> { )); }; - let mut out = BufWriter::new(io::stdout().lock()); + let mut out = uucore::output::StdoutBuffer::new(io::stdout().lock()); writeln!(out, "{output}").map_err(KillError::Write)?; out.flush().map_err(KillError::Write)?; Ok(()) } fn print_signals() -> UResult<()> { - let mut out = BufWriter::new(io::stdout().lock()); + let mut out = uucore::output::StdoutBuffer::new(io::stdout().lock()); for signal_value in 0..=signal_number_upper_bound() { if let Some(signal_name) = signal_list_name_by_value(signal_value) { writeln!(out, "{signal_name}").map_err(KillError::Write)?; From 0f19c288575985c2dea5384833a30862fa0c5ad2 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:59 +0100 Subject: [PATCH 16/29] ls: respect stdbuf on stdout --- src/uu/ls/src/dired.rs | 8 ++++---- src/uu/ls/src/display.rs | 13 +++++++------ src/uu/ls/src/ls.rs | 6 +++--- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/uu/ls/src/dired.rs b/src/uu/ls/src/dired.rs index ce96dbd3b82..bcddac43806 100644 --- a/src/uu/ls/src/dired.rs +++ b/src/uu/ls/src/dired.rs @@ -37,7 +37,7 @@ /// use crate::{Config, LsError}; use std::fmt; -use std::io::{BufWriter, Stdout, Write}; +use std::io::{Stdout, Write}; use uucore::error::UResult; #[derive(Debug, Clone, PartialEq)] @@ -97,7 +97,7 @@ pub fn calculate_dired( (start, end) } -pub fn indent(out: &mut BufWriter) -> UResult<()> { +pub fn indent(out: &mut uucore::output::StdoutBuffer) -> UResult<()> { write!(out, " ")?; Ok(()) } @@ -113,7 +113,7 @@ pub fn calculate_subdired(dired: &mut DiredOutput, path_len: usize) { pub fn print_dired_output( config: &Config, dired: &DiredOutput, - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, ) -> UResult<()> { if !dired.dired_positions.is_empty() { print_positions(out, "//DIRED//", &dired.dired_positions)?; @@ -134,7 +134,7 @@ pub fn print_dired_output( /// Helper function to print positions with a given prefix. fn print_positions( - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, prefix: &str, positions: &[BytePosition], ) -> UResult<()> { diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs index 5bb1e773444..9273ff104c2 100644 --- a/src/uu/ls/src/display.rs +++ b/src/uu/ls/src/display.rs @@ -22,7 +22,7 @@ use std::{ ffi::{OsStr, OsString}, fmt::Write as FmtWrite, fs::{self, DirEntry, FileType, Metadata}, - io::{BufWriter, Stdout, Write}, + io::{Stdout, Write}, }; use ansi_width::ansi_width; @@ -161,7 +161,7 @@ enum SizeOrDeviceId { /// needs to place the header in `//SUBDIRED//`. pub fn show_dir_name( path_data: &PathData, - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, config: &Config, ) -> std::io::Result { let escaped_name = @@ -449,7 +449,7 @@ fn display_commas_format( names: impl Iterator, width: u16, line_ending: LineEnding, - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, ) -> UResult<()> { let mut names = names.peekable(); let mut current_col = 0; @@ -481,7 +481,7 @@ fn display_commas_format( fn display_oneline_format( names: impl Iterator, line_ending: LineEnding, - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, ) -> UResult<()> { for name in names { write_os_str(out, &name.displayed)?; @@ -494,7 +494,7 @@ fn display_grid( names: impl Iterator, width: u16, direction: Direction, - out: &mut BufWriter, + out: &mut uucore::output::StdoutBuffer, quoted: bool, tab_size: usize, ) -> UResult<()> { @@ -895,7 +895,8 @@ fn display_item_name( } } -/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. +/// This writes to the [`uucore::output::StdoutBuffer`] `state.out` a single +/// string of the output of `ls -l`. /// /// It writes the following keys, in order: /// * `inode` ([`display_inode`], config-optional) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c7e6fd48804..4b0fcf59b9c 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -22,7 +22,7 @@ use std::{ cell::OnceCell, ffi::{OsStr, OsString}, fs::{self, DirEntry, FileType, Metadata, ReadDir}, - io::{BufWriter, ErrorKind, Stdout, Write, stdout}, + io::{ErrorKind, Stdout, Write, stdout}, ops::RangeInclusive, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -1007,7 +1007,7 @@ impl Colorable for PathData<'_> { // A struct to encapsulate state that is passed around from `list` functions. #[cfg_attr(not(unix), allow(dead_code))] struct ListState<'a> { - out: BufWriter, + out: uucore::output::StdoutBuffer, style_manager: Option>, // TODO: More benchmarking with different use cases is required here. // From experiments, BTreeMap may be faster than HashMap, especially as the @@ -1039,7 +1039,7 @@ impl<'a> TextOutput<'a> { pub fn new(config: &'a Config) -> Self { Self { state: ListState { - out: BufWriter::new(stdout()), + out: uucore::output::StdoutBuffer::new(stdout()), style_manager: config.color.as_ref().map(StyleManager::new), #[cfg(unix)] uid_cache: FxHashMap::default(), From 72991b3ee0ff36ec4d9a8f43b8fab32a74120362 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:59 +0100 Subject: [PATCH 17/29] nl: respect stdbuf on stdout --- src/uu/nl/src/nl.rs | 88 ++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 32 deletions(-) diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 8f687ca5f15..76ddccd8a55 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -6,11 +6,12 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; +use std::io::{self, BufRead, BufReader, Read, Write, stdin, stdout}; use std::num::{IntErrorKind, ParseIntError}; use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; +use uucore::output::StdoutBuffer; use uucore::{format_usage, show_error, translate, translate_text}; mod helper; @@ -264,35 +265,17 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stats = Stats::new(settings.starting_line_number); - for file in &files { - if file == "-" { - let mut buffer = BufReader::new(stdin()); - nl(&mut buffer, &mut stats, &settings)?; - } else { - let path = Path::new(file); - - if path.is_dir() { - show_error!( - "{}", - translate_text!("nl-error-is-directory", "path" => path.maybe_quote()) - ); - set_exit_code(1); - } else { - match File::open(path) { - Ok(reader) => { - let mut buffer = BufReader::new(reader); - nl(&mut buffer, &mut stats, &settings)?; - } - Err(e) => { - show_error!("{}", e.map_err_context(|| file.maybe_quote().to_string())); - set_exit_code(1); - } - } - } + let output = StdoutBuffer::new(stdout()); + match output { + StdoutBuffer::Block(mut output) => nl_files(&files, &mut stats, &settings, &mut output), + #[cfg(unix)] + StdoutBuffer::Unbuffered(output) => { + let mut output = StdoutBuffer::into_unbuffered(output); + nl_files(&files, &mut stats, &settings, &mut output) } + #[cfg(unix)] + StdoutBuffer::Line(mut output) => nl_files(&files, &mut stats, &settings, &mut output), } - - Ok(()) } pub fn uu_app() -> Command { @@ -413,8 +396,12 @@ fn write_line(writer: &mut impl Write, line: &[u8]) -> io::Result<()> { } /// `nl` implements the main functionality for an individual buffer. -fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings) -> UResult<()> { - let mut writer = BufWriter::new(stdout()); +fn nl( + reader: &mut BufReader, + stats: &mut Stats, + settings: &Settings, + writer: &mut W, +) -> UResult<()> { let mut line = Vec::new(); // Written before every unnumbered line. Capped to avoid OOM on 32-bit targets // when -w i32::MAX is passed (allocating 2 GiB would panic). @@ -481,7 +468,7 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings })?; settings .number_format - .format_to(&mut writer, line_number, settings.number_width) + .format_to(&mut *writer, line_number, settings.number_width) .map_err_context(|| translate!("nl-error-could-not-write"))?; writer .write_all(settings.number_separator.as_encoded_bytes()) @@ -497,7 +484,7 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings remaining -= chunk; } } - write_line(&mut writer, &line) + write_line(&mut *writer, &line) .map_err_context(|| translate!("nl-error-could-not-write"))?; } } @@ -507,6 +494,43 @@ fn nl(reader: &mut BufReader, stats: &mut Stats, settings: &Settings Ok(()) } +fn nl_files( + files: &[OsString], + stats: &mut Stats, + settings: &Settings, + writer: &mut W, +) -> UResult<()> { + for file in files { + if file == "-" { + let mut buffer = BufReader::new(stdin()); + nl(&mut buffer, stats, settings, writer)?; + } else { + let path = Path::new(file); + + if path.is_dir() { + show_error!( + "{}", + translate_text!("nl-error-is-directory", "path" => path.maybe_quote()) + ); + set_exit_code(1); + } else { + match File::open(path) { + Ok(reader) => { + let mut buffer = BufReader::new(reader); + nl(&mut buffer, stats, settings, writer)?; + } + Err(e) => { + show_error!("{}", e.map_err_context(|| file.maybe_quote().to_string())); + set_exit_code(1); + } + } + } + } + } + + Ok(()) +} + #[cfg(test)] mod test { use super::*; From 4db78246bc52572b068e40f1c74ff14aeb7bc0e8 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:59 +0100 Subject: [PATCH 18/29] numfmt: respect stdbuf on stdout --- src/uu/numfmt/src/numfmt.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/numfmt/src/numfmt.rs b/src/uu/numfmt/src/numfmt.rs index c7e360a556a..de2470d6f80 100644 --- a/src/uu/numfmt/src/numfmt.rs +++ b/src/uu/numfmt/src/numfmt.rs @@ -19,12 +19,13 @@ use crate::options::{ use crate::units::{Result, Unit}; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, parser::ValueSource}; use std::ffi::OsString; -use std::io::{BufRead, BufWriter, IsTerminal, Write, stderr}; +use std::io::{BufRead, IsTerminal, Write, stderr}; use std::str::FromStr; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; use uucore::i18n::decimal::locale_grouping_separator; +use uucore::output::StdoutBuffer; use uucore::parser::parse_size::{IEC_BASES, SI_BASES}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::ranges::Range; @@ -134,7 +135,7 @@ fn with_stdout(body: impl FnOnce(&mut dyn Write) -> UResult) -> UResult let mut writer = stdout.lock(); finish(body(&mut writer), &mut writer) } else { - let mut writer = BufWriter::new(stdout.lock()); + let mut writer = StdoutBuffer::new(stdout.lock()); finish(body(&mut writer), &mut writer) } } From 91ab55e196bfeec77f5c3645e10d6aaa19b789cf Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:59 +0100 Subject: [PATCH 19/29] pr: stream stdin without waiting for EOF --- src/uu/pr/src/pr.rs | 189 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index 9aff3237966..b96bfcb5d68 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -10,7 +10,7 @@ use itertools::Itertools; use regex::Regex; use std::ffi::{OsStr, OsString}; use std::fs::metadata; -use std::io::{self, Read, Write, stderr, stdin, stdout}; +use std::io::{self, BufRead, BufReader, Read, Write, stderr, stdin, stdout}; use std::num::IntErrorKind; use std::path::PathBuf; use std::str::Utf8Error; @@ -1086,6 +1086,14 @@ fn apply_expand_tab(chunk: &mut Vec, byte: u8, expand_options: &ExpandTabsOp } fn pr(name: &OsStr, options: &OutputOptions) -> Result { + // Stream the simple stdin layout so output can begin before EOF; keep + // other layouts on the existing buffered path. + if name == FILE_STDIN && can_stream_stdin(options) { + let mut writer = uucore::output::StdoutBuffer::new(stdout().lock()); + stream_stdin(&mut writer, options)?; + return Ok(0); + } + // Read the entire contents of the file into a buffer. // // TODO Read incrementally. @@ -1115,6 +1123,185 @@ fn pr(name: &OsStr, options: &OutputOptions) -> Result { Ok(0) } +fn can_stream_stdin(options: &OutputOptions) -> bool { + options.number.is_none() + && options.column_mode_options.is_none() + && options.merge_files_print.is_none() + && !options.double_space + && options.start_page == 1 + && options.end_page.is_none() + && !options.form_feed_used + && options.offset_spaces == 0 + && options.line_separator == "\n" + && options.content_line_separator == "\n" + && options.page_separator_char == "\n" + && options.line_width.is_none() + && options.expand_tabs.is_none() +} + +fn stream_stdin(writer: &mut impl Write, options: &OutputOptions) -> Result<(), PrError> { + // Keep only the current record and page state so pipes produce output + // before EOF. + let mut reader = BufReader::new(stdin()); + let mut line = Vec::new(); + let mut page = 1; + let mut line_number = get_start_line_number(options); + let lines_per_page = lines_to_read_for_page(options); + let mut lines_on_page = 0; + let mut page_started = false; + let mut previous_delimiter = None; + + loop { + let delimiter = read_stream_record(&mut reader, &mut line).map_err(PrError::Read)?; + + let Some(delimiter) = delimiter else { + if !line.is_empty() { + write_stream_line(writer, options, page, line_number, &line, &mut page_started)?; + lines_on_page += 1; + if lines_on_page == lines_per_page { + finish_stream_page(writer, options, page, lines_on_page, &mut page_started)?; + page += 1; + lines_on_page = 0; + } + } + break; + }; + + let ignore_empty_line = line.is_empty() + && ((delimiter == FF && previous_delimiter == Some(NL)) + || (delimiter == NL && previous_delimiter == Some(FF))); + + // Adjacent newline/form-feed delimiters mark a page boundary rather + // than an additional empty input record. + if !ignore_empty_line { + write_stream_line(writer, options, page, line_number, &line, &mut page_started)?; + lines_on_page += 1; + if delimiter == NL { + line_number += 1; + } + } + + if delimiter == FF || lines_on_page == lines_per_page { + finish_stream_page(writer, options, page, lines_on_page, &mut page_started)?; + page += 1; + lines_on_page = 0; + } + + previous_delimiter = Some(delimiter); + } + + if lines_on_page > 0 { + finish_stream_page(writer, options, page, lines_on_page, &mut page_started)?; + } + + writer.flush().map_err(PrError::Write)?; + Ok(()) +} + +fn read_stream_record(reader: &mut impl BufRead, line: &mut Vec) -> io::Result> { + // Search each BufRead chunk for a delimiter, retaining only one record + // instead of reading the whole input before writing. + line.clear(); + loop { + let input = reader.fill_buf()?; + if input.is_empty() { + return Ok(None); + } + + if let Some(index) = memchr::memchr2(FF, NL, input) { + line.extend_from_slice(&input[..index]); + let delimiter = input[index]; + reader.consume(index + 1); + return Ok(Some(delimiter)); + } + + line.extend_from_slice(input); + let input_len = input.len(); + reader.consume(input_len); + } +} + +fn write_stream_line( + writer: &mut impl Write, + options: &OutputOptions, + page: usize, + line_number: usize, + line: &[u8], + page_started: &mut bool, +) -> Result<(), PrError> { + if !*page_started { + // Delay headers until a record arrives so empty input produces no page. + for header_line in header_content(options, page) { + writer + .write_all(header_line.as_bytes()) + .map_err(PrError::Write)?; + writer + .write_all(options.line_separator.as_bytes()) + .map_err(PrError::Write)?; + } + *page_started = true; + } + + let file_line = FileLine::from_buf(0, page - 1, line_number, line, options); + writer + .write_all( + get_line_for_printing(options, &file_line, 1, 0, options.line_width, 1).as_bytes(), + ) + .map_err(PrError::Write)?; + writer + .write_all(options.content_line_separator.as_bytes()) + .map_err(PrError::Write) +} + +fn finish_stream_page( + writer: &mut impl Write, + options: &OutputOptions, + page: usize, + lines_on_page: usize, + page_started: &mut bool, +) -> Result<(), PrError> { + if !*page_started { + // A form feed can create an empty page, so emit its header here. + for header_line in header_content(options, page) { + writer + .write_all(header_line.as_bytes()) + .map_err(PrError::Write)?; + writer + .write_all(options.line_separator.as_bytes()) + .map_err(PrError::Write)?; + } + *page_started = true; + } + + if options.display_header_and_trailer { + for _ in lines_on_page..options.content_lines_per_page { + writer + .write_all(options.content_line_separator.as_bytes()) + .map_err(PrError::Write)?; + } + } + + let trailer = trailer_content(options); + for (index, trailer_line) in trailer.iter().enumerate() { + writer + .write_all(trailer_line.as_bytes()) + .map_err(PrError::Write)?; + if index + 1 != trailer.len() { + writer + .write_all(options.line_separator.as_bytes()) + .map_err(PrError::Write)?; + } + } + if options.display_header_and_trailer || options.form_feed_used { + writer + .write_all(options.page_separator_char.as_bytes()) + .map_err(PrError::Write)?; + } + + *page_started = false; + Ok(()) +} + /// Group lines of a file into pages. /// /// Returns a list of the form `(page_num, lines)` and the total page count. From 0ceba6a5f125d93ad40335db556f3db7c25f7abf Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:25:59 +0100 Subject: [PATCH 20/29] ptx: respect stdbuf on stdout --- src/uu/ptx/src/ptx.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index 32bc4d32778..5dd23c18d44 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -791,14 +791,17 @@ fn write_traditional_output( words: &BTreeSet, output_filename: &OsStr, ) -> UResult<()> { - let mut writer: BufWriter> = - BufWriter::new(if output_filename == OsStr::new("-") { - Box::new(stdout()) + let mut writer: Box = if output_filename == OsStr::new("-") { + if std::env::var_os("_STDBUF_O").is_none() { + Box::new(BufWriter::new(stdout())) } else { - let file = File::create(output_filename) - .map_err_context(|| output_filename.quote().to_string())?; - Box::new(file) - }); + Box::new(uucore::output::StdoutBuffer::new(stdout())) + } + } else { + let file = File::create(output_filename) + .map_err_context(|| output_filename.quote().to_string())?; + Box::new(BufWriter::new(file)) + }; let context_reg = Regex::new(&config.context_regex).unwrap(); From 0b183cc7745f50a563e1d8f2f7d1672df4bca11c Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:00 +0100 Subject: [PATCH 21/29] seq: respect stdbuf on stdout --- src/uu/seq/src/seq.rs | 49 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/uu/seq/src/seq.rs b/src/uu/seq/src/seq.rs index b52f1504a4f..7a17f22f1a9 100644 --- a/src/uu/seq/src/seq.rs +++ b/src/uu/seq/src/seq.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) bigdecimal extendedbigdecimal numberparse hexadecimalfloat biguint use std::ffi::{OsStr, OsString}; -use std::io::{BufWriter, Write, stdout}; +use std::io::{Write, stdout}; use clap::{Arg, ArgAction, Command}; use num_bigint::BigUint; @@ -17,6 +17,7 @@ use uucore::error::{FromIo, UResult}; use uucore::extendedbigdecimal::ExtendedBigDecimal; use uucore::format::num_format::FloatVariant; use uucore::format::{Format, num_format}; +use uucore::output::StdoutBuffer; use uucore::{fast_inc::fast_inc, format_usage}; mod diagnostics; @@ -370,8 +371,50 @@ fn print_seq( fast_allowed: bool, padding: usize, // Used by fast path only ) -> std::io::Result<()> { - let stdout = stdout().lock(); - let mut stdout = BufWriter::new(stdout); + let output = StdoutBuffer::new(stdout().lock()); + + match output { + StdoutBuffer::Block(output) => print_seq_with_output( + range, + separator, + terminator, + format, + fast_allowed, + padding, + output, + ), + #[cfg(unix)] + StdoutBuffer::Unbuffered(output) => print_seq_with_output( + range, + separator, + terminator, + format, + fast_allowed, + padding, + StdoutBuffer::into_unbuffered(output), + ), + #[cfg(unix)] + StdoutBuffer::Line(output) => print_seq_with_output( + range, + separator, + terminator, + format, + fast_allowed, + padding, + output, + ), + } +} + +fn print_seq_with_output( + range: RangeFloat, + separator: &OsStr, + terminator: &OsStr, + format: &Format, + fast_allowed: bool, + padding: usize, + mut stdout: W, +) -> std::io::Result<()> { let (first, increment, last) = range; if fast_allowed { From 6f0b50a23bdeef456499c804ed360becb622bec2 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:00 +0100 Subject: [PATCH 22/29] shuf: respect stdbuf on stdout --- src/uu/shuf/src/shuf.rs | 87 ++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/src/uu/shuf/src/shuf.rs b/src/uu/shuf/src/shuf.rs index 0d4c6a08e5e..cb8537d0a53 100644 --- a/src/uu/shuf/src/shuf.rs +++ b/src/uu/shuf/src/shuf.rs @@ -7,7 +7,7 @@ use std::ffi::{OsStr, OsString}; use std::fs::File; -use std::io::{self, BufReader, BufWriter, Read, Write, stdin, stdout}; +use std::io::{self, BufReader, BufWriter, Read, stdin, stdout}; use std::num::IntErrorKind; use std::ops::RangeInclusive; use std::path::{Path, PathBuf}; @@ -161,24 +161,39 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { _ => None, }; - let mut output = open_output(options.output.as_deref())?; - - match mode { - Mode::Echo(args) => { - let mut evec: Vec<&OsStr> = args.iter().map(AsRef::as_ref).collect(); - shuf_exec(&mut evec, &options, &mut rng, &mut output)?; - } - Mode::InputRange(mut range) => { - shuf_exec(&mut range, &options, &mut rng, &mut output)?; + match options.output.as_deref() { + Some(s) => { + let file = File::create(s).map_err_context( + || translate!("shuf-error-failed-to-open-for-writing", "file" => s.quote()), + )?; + let writer: Box = Box::new(file); + let mut output = BufWriter::with_capacity(BUF_SIZE, writer); + shuf_with_output(mode, fdata, &options, &mut rng, &mut output) } - Mode::Default(_) => { - let fdata = fdata.unwrap_or_default(); - let mut items = split_seps(&fdata, options.sep); - shuf_exec(&mut items, &options, &mut rng, &mut output)?; + None => { + if std::env::var_os("_STDBUF_O").is_none() { + let writer: Box = Box::new(stdout()); + let mut output = BufWriter::with_capacity(BUF_SIZE, writer); + shuf_with_output(mode, fdata, &options, &mut rng, &mut output) + } else { + let output = uucore::output::StdoutBuffer::with_capacity(BUF_SIZE, stdout()); + match output { + uucore::output::StdoutBuffer::Block(mut output) => { + shuf_with_output(mode, fdata, &options, &mut rng, &mut output) + } + #[cfg(unix)] + uucore::output::StdoutBuffer::Unbuffered(output) => { + let mut output = uucore::output::StdoutBuffer::into_unbuffered(output); + shuf_with_output(mode, fdata, &options, &mut rng, &mut output) + } + #[cfg(unix)] + uucore::output::StdoutBuffer::Line(mut output) => { + shuf_with_output(mode, fdata, &options, &mut rng, &mut output) + } + } + } } } - - Ok(()) } pub fn uu_app() -> Command { @@ -264,17 +279,25 @@ pub fn uu_app() -> Command { ) } -fn open_output(output: Option<&Path>) -> UResult>> { - let writer: Box = match output { - None => Box::new(stdout()), - Some(s) => { - let file = File::create(s).map_err_context( - || translate!("shuf-error-failed-to-open-for-writing", "file" => s.quote()), - )?; - Box::new(file) +fn shuf_with_output( + mode: Mode, + fdata: Option>, + options: &Options, + rng: &mut WrappedRng, + output: &mut W, +) -> UResult<()> { + match mode { + Mode::Echo(args) => { + let mut evec: Vec<&OsStr> = args.iter().map(AsRef::as_ref).collect(); + shuf_exec(&mut evec, options, rng, output) } - }; - Ok(BufWriter::with_capacity(BUF_SIZE, writer)) + Mode::InputRange(mut range) => shuf_exec(&mut range, options, rng, output), + Mode::Default(_) => { + let fdata = fdata.unwrap_or_default(); + let mut items = split_seps(&fdata, options.sep); + shuf_exec(&mut items, options, rng, output) + } + } } fn read_input_file(filename: &Path) -> UResult> { @@ -385,23 +408,23 @@ impl Shufable for RangeInclusive { } trait Writable { - fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), io::Error>; + fn write_all_to(&self, output: &mut W) -> Result<(), io::Error>; } impl Writable for &[u8] { - fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), io::Error> { + fn write_all_to(&self, output: &mut W) -> Result<(), io::Error> { output.write_all(self) } } impl Writable for &OsStr { - fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), io::Error> { + fn write_all_to(&self, output: &mut W) -> Result<(), io::Error> { output.write_all_os(self) } } impl Writable for u64 { - fn write_all_to(&self, output: &mut impl OsWrite) -> Result<(), io::Error> { + fn write_all_to(&self, output: &mut W) -> Result<(), io::Error> { // The itoa crate is surprisingly much more efficient than a formatted write. // It speeds up `shuf -r -n1000000 -i1-1024` by 1.8×. let mut buf = itoa::Buffer::new(); @@ -417,11 +440,11 @@ fn handle_write_error(e: io::Error) -> Box { e.map_err_context(move || ctx) } -fn shuf_exec( +fn shuf_exec( input: &mut impl Shufable, opts: &Options, rng: &mut WrappedRng, - output: &mut BufWriter>, + output: &mut W, ) -> UResult<()> { let sep = [opts.sep]; if opts.repeat { From f39729374b2b82609ef665145606c6190c47cbd7 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:00 +0100 Subject: [PATCH 23/29] sort: respect stdbuf on stdout --- src/uu/sort/src/merge.rs | 35 +++++++++++++++++++++--- src/uu/sort/src/sort.rs | 58 ++++++++++++++++++++++++++++------------ 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index bfbd7fa64f1..e37320e9184 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -19,7 +19,7 @@ use std::{ fs::File, io::{self, BufWriter, Read, Write}, iter, - path::Path, + path::{Path, PathBuf}, process::{Child, ChildStdin, ChildStdout, Command, Stdio}, rc::Rc, sync::mpsc::{Receiver, Sender, SyncSender, TryRecvError, channel, sync_channel}, @@ -348,9 +348,36 @@ impl FileMerger<'_> { /// Write the merged contents to the output file. fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> { let ctx = output.write_failed_context(); - let mut out = output.into_write()?; - self.write_all_to(settings, &mut out, &ctx)?; - out.flush().map_err_context(ctx) + if let Some((name, file)) = output.file { + if file.metadata().is_ok_and(|meta| meta.is_file()) { + file.set_len(0).map_err(|error| SortError::TruncateFailed { + path: PathBuf::from(name), + error, + })?; + } + let mut out = BufWriter::new(file); + self.write_all_to(settings, &mut out, &ctx)?; + out.flush().map_err_context(ctx) + } else { + let out = uucore::output::StdoutBuffer::new(io::stdout()); + match out { + uucore::output::StdoutBuffer::Block(mut out) => { + self.write_all_to(settings, &mut out, &ctx)?; + out.flush().map_err_context(ctx) + } + #[cfg(unix)] + uucore::output::StdoutBuffer::Unbuffered(out) => { + let mut out = uucore::output::StdoutBuffer::into_unbuffered(out); + self.write_all_to(settings, &mut out, &ctx)?; + out.flush().map_err_context(ctx) + } + #[cfg(unix)] + uucore::output::StdoutBuffer::Line(mut out) => { + self.write_all_to(settings, &mut out, &ctx)?; + out.flush().map_err_context(ctx) + } + } + } } /// Write the merged contents to `out`, reporting write errors with `ctx`. diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index eb3a0959321..75738d810b5 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -262,22 +262,6 @@ impl Output { Ok(Self { file }) } - fn into_write(self) -> UResult>> { - Ok(BufWriter::new(match self.file { - Some((name, file)) => { - // Only regular files can be truncated; there a failure leaves stale bytes. - if file.metadata().is_ok_and(|meta| meta.is_file()) { - file.set_len(0).map_err(|error| SortError::TruncateFailed { - path: PathBuf::from(name), - error, - })?; - } - Box::new(file) - } - None => Box::new(stdout()), - })) - } - fn as_output_name(&self) -> Option<&OsStr> { match &self.file { Some((name, _file)) => Some(name.as_os_str()), @@ -3303,7 +3287,47 @@ fn print_sorted<'a, T: Iterator>>( output: Output, ) -> UResult<()> { let ctx = output.write_failed_context(); - let mut writer = output.into_write()?; + if let Some((name, file)) = output.file { + // Only regular files can be truncated; there a failure leaves stale bytes. + if file.metadata().is_ok_and(|meta| meta.is_file()) { + file.set_len(0).map_err(|error| SortError::TruncateFailed { + path: PathBuf::from(name), + error, + })?; + } + print_sorted_with_writer(iter, settings, BufWriter::new(file), ctx) + } else { + let writer = uucore::output::StdoutBuffer::new(stdout()); + match writer { + uucore::output::StdoutBuffer::Block(writer) => { + print_sorted_with_writer(iter, settings, writer, ctx) + } + #[cfg(unix)] + uucore::output::StdoutBuffer::Unbuffered(writer) => print_sorted_with_writer( + iter, + settings, + uucore::output::StdoutBuffer::into_unbuffered(writer), + ctx, + ), + #[cfg(unix)] + uucore::output::StdoutBuffer::Line(writer) => { + print_sorted_with_writer(iter, settings, writer, ctx) + } + } + } +} + +fn print_sorted_with_writer<'a, T, W, C>( + iter: T, + settings: &GlobalSettings, + mut writer: W, + ctx: C, +) -> UResult<()> +where + T: Iterator>, + W: Write, + C: Fn() -> String, +{ for line in iter { line.write(&mut writer, settings).map_err_context(&ctx)?; } From 8065cc8aae2e8726414cd53baa060ba4ffd8f2f7 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:00 +0100 Subject: [PATCH 24/29] tac: respect stdbuf on stdout --- src/uu/tac/src/tac.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 9d7fa815f9d..2cca51355e3 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -12,7 +12,7 @@ use memchr::memmem; #[cfg(not(target_os = "wasi"))] use memmap2::Mmap; use std::ffi::{OsStr, OsString}; -use std::io::{BufWriter, Read, Write, stdin, stdout}; +use std::io::{Read, Write, stdin, stdout}; use std::{fs::File, path::Path}; #[cfg(all(unix, not(target_os = "fuchsia")))] use uucore::error::UError; @@ -121,7 +121,7 @@ fn buffer_tac_regex( before: bool, ) -> std::io::Result<()> { let out = stdout(); - let mut out = BufWriter::new(out.lock()); + let mut out = uucore::output::StdoutBuffer::new(out.lock()); // The index of the line separator for the current line. // @@ -191,7 +191,7 @@ fn buffer_tac_regex( /// `"/abc/def"`. fn buffer_tac(data: &[u8], before: bool, separator: &OsStr) -> std::io::Result<()> { let out = stdout(); - let mut out = BufWriter::new(out.lock()); + let mut out = uucore::output::StdoutBuffer::new(out.lock()); // The number of bytes in the line separator. let slen = separator.len(); From 3672783aad63282d3cf2ea3fa6e177fd28adbcbe Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:00 +0100 Subject: [PATCH 25/29] tail: respect stdbuf on stdout --- src/uu/tail/src/follow/files.rs | 4 ++-- src/uu/tail/src/tail.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/uu/tail/src/follow/files.rs b/src/uu/tail/src/follow/files.rs index 19e6fc5a422..92a3658bbf2 100644 --- a/src/uu/tail/src/follow/files.rs +++ b/src/uu/tail/src/follow/files.rs @@ -12,7 +12,7 @@ use crate::text; use std::collections::HashMap; use std::collections::hash_map::Keys; use std::fs::{File, Metadata}; -use std::io::{BufRead, BufReader, BufWriter, Write, stdout}; +use std::io::{BufRead, BufReader, Write, stdout}; use std::path::{Path, PathBuf}; use uucore::error::UResult; @@ -148,7 +148,7 @@ impl FileHandling { self.header_printer.print(display_name.as_str())?; } - let mut writer = BufWriter::new(stdout().lock()); + let mut writer = uucore::output::StdoutBuffer::new(stdout().lock()); chunks.write(&mut writer)?; writer.flush()?; diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index ad794bfd392..aa1a987f95c 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -29,7 +29,7 @@ use memchr::{memchr_iter, memrchr_iter}; use paths::{FileExtTail, HeaderPrinter, Input, InputKind}; use std::cmp::Ordering; use std::fs::File; -use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout}; +use std::io::{self, BufReader, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, set_exit_code}; @@ -497,7 +497,7 @@ fn bounded_tail(file: &mut File, settings: &Settings) -> UResult<()> { } fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UResult<()> { - let mut writer = BufWriter::new(stdout().lock()); + let mut writer = uucore::output::StdoutBuffer::new(stdout().lock()); match &settings.mode { FilterMode::Lines(Signum::Negative(count), sep) => { let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count); From 65670210b51a99c54ece31d8708f35c43945dc14 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:01 +0100 Subject: [PATCH 26/29] tsort: respect stdbuf on stdout --- src/uu/tsort/src/tsort.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 39078d7f494..f063cf55f75 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -17,7 +17,7 @@ use std::collections::hash_map::Entry; use std::ffi::OsString; use std::fs::File; use std::fs::OpenOptions; -use std::io::{self, BufRead, BufReader, BufWriter, Write}; +use std::io::{self, BufRead, BufReader, Write}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::{format_usage, show, translate}; @@ -274,7 +274,7 @@ impl Graph { .make_contiguous() .sort_unstable_by(|a, b| self.get_node_name(*a).cmp(self.get_node_name(*b))); - let mut out = BufWriter::new(io::stdout().lock()); + let mut out = uucore::output::StdoutBuffer::new(io::stdout().lock()); while !self.nodes.is_empty() { let v = self.find_next_node(&mut independent_nodes_queue); From dab6c7c29e7e885af730458fa791953d008fade2 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:01 +0100 Subject: [PATCH 27/29] unexpand: respect stdbuf on stdout --- src/uu/unexpand/src/unexpand.rs | 34 +++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/uu/unexpand/src/unexpand.rs b/src/uu/unexpand/src/unexpand.rs index 93c13f634ea..cd20a0dd265 100644 --- a/src/uu/unexpand/src/unexpand.rs +++ b/src/uu/unexpand/src/unexpand.rs @@ -8,11 +8,12 @@ use clap::{Arg, ArgAction, Command}; use std::ffi::OsString; use std::fs::File; -use std::io::{self, BufRead, BufReader, BufWriter, Read, Stdin, Stdout, Write, stdin, stdout}; +use std::io::{self, BufRead, BufReader, Read, Stdin, Write, stdin, stdout}; use std::num::IntErrorKind; use uucore::char_width::char_info_at; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, set_exit_code}; +use uucore::output::StdoutBuffer; use uucore::translate; use uucore::{format_usage, show}; @@ -365,8 +366,8 @@ fn next_tabstop(tab_config: &TabConfig, col: usize) -> Option { } } -fn write_tabs( - output: &mut BufWriter, +fn write_tabs( + output: &mut W, tab_config: &TabConfig, print_state: &mut PrintState, amode: bool, @@ -501,9 +502,9 @@ impl PrintState { } #[allow(clippy::cognitive_complexity)] -fn unexpand_buf( +fn unexpand_buf( buf: &[u8], - output: &mut BufWriter, + output: &mut W, options: &Options, lastcol: usize, tab_config: &TabConfig, @@ -641,9 +642,9 @@ fn unexpand_buf( Ok(()) } -fn unexpand_file( +fn unexpand_file( file: &OsString, - output: &mut BufWriter, + output: &mut W, options: &Options, lastcol: usize, tab_config: &TabConfig, @@ -674,8 +675,7 @@ fn unexpand_file( write_tabs(output, tab_config, &mut print_state, options.aflag) } -fn unexpand(options: &Options) -> UResult<()> { - let mut output = BufWriter::new(stdout()); +fn unexpand_with_output(options: &Options, output: &mut W) -> UResult<()> { let tab_config = &options.tab_config; let lastcol = if tab_config.tabstops.len() > 1 && tab_config.increment_size.is_none() @@ -687,7 +687,7 @@ fn unexpand(options: &Options) -> UResult<()> { }; for file in &options.files { - if let Err(e) = unexpand_file(file, &mut output, options, lastcol, tab_config) { + if let Err(e) = unexpand_file(file, output, options, lastcol, tab_config) { show!(e); set_exit_code(1); } @@ -696,6 +696,20 @@ fn unexpand(options: &Options) -> UResult<()> { Ok(()) } +fn unexpand(options: &Options) -> UResult<()> { + let output = StdoutBuffer::new(stdout()); + match output { + StdoutBuffer::Block(mut output) => unexpand_with_output(options, &mut output), + #[cfg(unix)] + StdoutBuffer::Unbuffered(output) => { + let mut output = StdoutBuffer::into_unbuffered(output); + unexpand_with_output(options, &mut output) + } + #[cfg(unix)] + StdoutBuffer::Line(mut output) => unexpand_with_output(options, &mut output), + } +} + #[cfg(test)] mod tests { use crate::{ParseError, is_digit_or_comma, parse_tab_num, parse_tabstops}; From 48d200d4ea4d7eaae2bc13450b23ad00e3ee0540 Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Sat, 31 Oct 2026 21:26:01 +0100 Subject: [PATCH 28/29] uniq: emit groups as input arrives --- src/uu/uniq/src/uniq.rs | 62 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/uu/uniq/src/uniq.rs b/src/uu/uniq/src/uniq.rs index 4a7c86edf60..6799fcddc53 100644 --- a/src/uu/uniq/src/uniq.rs +++ b/src/uu/uniq/src/uniq.rs @@ -16,6 +16,7 @@ use std::num::IntErrorKind; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::format_usage; +use uucore::output::StdoutBuffer; use uucore::parser::shortcut_value_parser::ShortcutValueParser; use uucore::posix::{OBSOLETE, posix_version}; use uucore::translate; @@ -68,6 +69,12 @@ struct LineMeta { impl Uniq { pub fn write_uniq(&self, mut reader: impl BufRead, mut writer: impl Write) -> UResult<()> { + // Default output needs only one-record lookahead, so stream it before + // EOF; filtering and counting modes keep the existing path below. + if self.can_stream() { + return self.write_uniq_stream(&mut reader, &mut writer); + } + let mut first_line_printed = false; let mut group_count = 1; let line_terminator = self.get_line_terminator(); @@ -153,6 +160,55 @@ impl Uniq { read_result } + fn can_stream(&self) -> bool { + !self.repeats_only + && !self.uniques_only + && !self.all_repeated + && !self.show_counts + && self.delimiters == Delimiters::None + } + + fn write_uniq_stream(&self, reader: &mut impl BufRead, writer: &mut impl Write) -> UResult<()> { + // Retain the current line and one lookahead line; a changed key starts + // the next group. + let line_terminator = self.get_line_terminator(); + let mut current_buf = Vec::with_capacity(1024); + if !Self::read_line(reader, &mut current_buf, line_terminator)? { + return Ok(()); + } + + let mut current_meta = LineMeta::default(); + self.build_meta(¤t_buf, &mut current_meta); + let mut next_buf = Vec::with_capacity(1024); + let mut next_meta = LineMeta::default(); + let mut line_out = Vec::with_capacity(1024); + + self.write_line(writer, &mut line_out, ¤t_buf, 1, false)?; + + loop { + match Self::read_line(reader, &mut next_buf, line_terminator) { + Ok(true) => { + self.build_meta(&next_buf, &mut next_meta); + if !self.keys_are_equal(¤t_buf, ¤t_meta, &next_buf, &next_meta) { + // Swap buffers instead of copying the first line of + // the new group. + std::mem::swap(&mut current_buf, &mut next_buf); + std::mem::swap(&mut current_meta, &mut next_meta); + self.write_line(writer, &mut line_out, ¤t_buf, 1, true)?; + } + next_buf.clear(); + } + Ok(false) => break, + Err(error) => return Err(error), + } + } + + writer + .flush() + .map_err_context(|| translate!("uniq-error-write-error"))?; + Ok(()) + } + fn get_line_terminator(&self) -> u8 { if self.zero_terminated { 0 } else { b'\n' } } @@ -878,7 +934,11 @@ fn open_output_file(out_file_name: Option<&OsStr>) -> UResult> { let out_file = File::create(path).map_err_context(|| path.maybe_quote().to_string())?; Box::new(BufWriter::with_capacity(OUTPUT_BUFFER_CAPACITY, out_file)) } - _ => Box::new(BufWriter::with_capacity( + _ if std::env::var_os("_STDBUF_O").is_none() => Box::new(BufWriter::with_capacity( + OUTPUT_BUFFER_CAPACITY, + stdout().lock(), + )), + _ => Box::new(StdoutBuffer::with_capacity( OUTPUT_BUFFER_CAPACITY, stdout().lock(), )), From a436a11920ec7b5d7b32e4acc0eca94d4f879f6c Mon Sep 17 00:00:00 2001 From: "Tom D." <15268361+anastygnome@users.noreply.github.com> Date: Mon, 2 Nov 2026 22:28:29 +0100 Subject: [PATCH 29/29] pr: write streaming headers as bytes --- src/uu/pr/src/pr.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/uu/pr/src/pr.rs b/src/uu/pr/src/pr.rs index b96bfcb5d68..2e23091e6d3 100644 --- a/src/uu/pr/src/pr.rs +++ b/src/uu/pr/src/pr.rs @@ -1232,9 +1232,7 @@ fn write_stream_line( if !*page_started { // Delay headers until a record arrives so empty input produces no page. for header_line in header_content(options, page) { - writer - .write_all(header_line.as_bytes()) - .map_err(PrError::Write)?; + writer.write_all(&header_line).map_err(PrError::Write)?; writer .write_all(options.line_separator.as_bytes()) .map_err(PrError::Write)?; @@ -1263,9 +1261,7 @@ fn finish_stream_page( if !*page_started { // A form feed can create an empty page, so emit its header here. for header_line in header_content(options, page) { - writer - .write_all(header_line.as_bytes()) - .map_err(PrError::Write)?; + writer.write_all(&header_line).map_err(PrError::Write)?; writer .write_all(options.line_separator.as_bytes()) .map_err(PrError::Write)?;