From 74770af35eea4e052fa9ec060f85bcf8eee645f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:31:09 -0400 Subject: [PATCH 01/10] make filesystem data available to crate users --- src/uu/df/src/filesystem.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/df/src/filesystem.rs b/src/uu/df/src/filesystem.rs index fe504b7d85a..1a672642de0 100644 --- a/src/uu/df/src/filesystem.rs +++ b/src/uu/df/src/filesystem.rs @@ -29,7 +29,7 @@ use uucore::fsext::{FsUsage, MountInfo}; /// [`Filesystem::usage`] field provides information on the amount of /// space available on the filesystem and the amount of space used. #[derive(Debug, Clone)] -pub(crate) struct Filesystem { +pub struct Filesystem { /// The file given on the command line if any. /// /// When invoking `df` with a positional argument, it displays From dd7c337fbb83cd13bc1db57889f9f820a32461ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:31:41 -0400 Subject: [PATCH 02/10] add output sink trait for df --- src/uu/df/src/output.rs | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/uu/df/src/output.rs diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs new file mode 100644 index 00000000000..3f2191abc3f --- /dev/null +++ b/src/uu/df/src/output.rs @@ -0,0 +1,63 @@ +// 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 traits and types for programmatic access to df functionality. +//! +//! This module separates filesystem discovery from output formatting so +//! consumers can use `df` without parsing text written to stdout. + +use crate::{Filesystem, Options}; +use uucore::error::UResult; + +/// Streaming mode for `DfOutput` sinks. +/// +/// `Batch` sinks receive all filesystems at once via +/// [`DfOutput::write_filesystems`]. `Streaming` sinks receive one filesystem at +/// a time via [`DfOutput::write_filesystem`]. +pub enum StreamMode { + Batch, + Streaming, +} + +/// Trait for receiving df filesystem entries. +/// +/// Implement this trait to receive structured data from `df`. Text-formatting +/// sinks can use [`DfOutput::write_filesystems`] to format aligned tables, while +/// programmatic consumers can use [`DfOutput::write_filesystem`] to receive +/// entries one at a time. +pub trait DfOutput { + /// Returns the preferred output mode for this sink. + fn stream_mode(&self) -> StreamMode { + StreamMode::Batch + } + + /// Called for each filesystem entry in streaming mode. + fn write_filesystem(&mut self, _filesystem: &Filesystem, _options: &Options) -> UResult<()> { + Ok(()) + } + + /// Called with all filesystem entries in batch mode. + fn write_filesystems(&mut self, filesystems: &[Filesystem], options: &Options) -> UResult<()> { + for filesystem in filesystems { + self.write_filesystem(filesystem, options)?; + } + Ok(()) + } + + /// Called to flush buffered output before diagnostics. + fn flush(&mut self) -> UResult<()> { + Ok(()) + } + + /// Called when all filesystems have been written. + fn finalize(&mut self, _options: &Options) -> UResult<()> { + Ok(()) + } + + /// Called before any filesystems are processed. + fn initialize(&mut self, _options: &Options) -> UResult<()> { + Ok(()) + } +} From cefa84f365f4d048027c05912538785bb07b0e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:32:01 -0400 Subject: [PATCH 03/10] add streaming output collector for df --- src/uu/df/src/output.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs index 3f2191abc3f..b59d966c347 100644 --- a/src/uu/df/src/output.rs +++ b/src/uu/df/src/output.rs @@ -61,3 +61,42 @@ pub trait DfOutput { Ok(()) } } + +/// A streaming sink that collects filesystem entries as they are emitted. +#[derive(Debug, Default)] +pub struct StreamingOutput { + filesystems: Vec, +} + +impl StreamingOutput { + /// Create a new empty streaming sink. + pub fn new() -> Self { + Self::default() + } + + /// Get all collected filesystem entries. + pub fn filesystems(&self) -> &[Filesystem] { + &self.filesystems + } + + /// Consume the collector and return all filesystem entries. + pub fn into_filesystems(self) -> Vec { + self.filesystems + } + + /// Clear all collected data. + pub fn clear(&mut self) { + self.filesystems.clear(); + } +} + +impl DfOutput for StreamingOutput { + fn stream_mode(&self) -> StreamMode { + StreamMode::Streaming + } + + fn write_filesystem(&mut self, filesystem: &Filesystem, _options: &Options) -> UResult<()> { + self.filesystems.push(filesystem.clone()); + Ok(()) + } +} From 135646587459cd698a19d7258e112887ef8e83ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:32:23 -0400 Subject: [PATCH 04/10] expose df output types from uu_df --- src/uu/df/src/df.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index e1531ce8af8..1a1bdae28e5 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -6,6 +6,7 @@ mod blocks; mod columns; mod filesystem; +pub mod output; mod table; use blocks::HumanReadable; @@ -27,8 +28,9 @@ use thiserror::Error; use crate::blocks::{BlockSize, read_block_size}; use crate::columns::{Column, ColumnError}; -use crate::filesystem::Filesystem; +pub use crate::filesystem::Filesystem; use crate::filesystem::FsError; +pub use crate::output::{DfOutput, StreamMode, StreamingOutput}; use crate::table::Table; static OPT_HELP: &str = "help"; @@ -58,7 +60,7 @@ static OUTPUT_FIELD_LIST: [&str; 12] = [ /// Most of these parameters control which rows and which columns are /// displayed. The `block_size` determines the units to use when /// displaying numbers of bytes or inodes. -struct Options { +pub struct Options { show_local_fs: bool, show_all_fs: bool, human_readable: Option, @@ -112,6 +114,11 @@ impl Default for Options { } impl Options { + /// Convert command-line arguments into [`Options`]. + pub fn from_matches(matches: &ArgMatches) -> UResult { + Ok(Self::from(matches).map_err(DfError::OptionsError)?) + } + /// Whether -a, -l, -t, or -x options require the mount table. fn requires_mount_table(&self) -> bool { self.show_all_fs || self.show_local_fs || self.include.is_some() || self.exclude.is_some() From 5516a1d68096da9038c26d0117da4e3a43b7369a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:32:36 -0400 Subject: [PATCH 05/10] add text output implementation for df --- src/uu/df/src/df.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 1a1bdae28e5..3bfd2d938aa 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -441,6 +441,16 @@ impl UError for DfError { } } +/// Text output implementation that formats filesystem data as the standard `df` table. +pub struct TextOutput; + +impl DfOutput for TextOutput { + fn write_filesystems(&mut self, filesystems: &[Filesystem], options: &Options) -> UResult<()> { + Table::new(options, filesystems.to_vec()).write_to(&mut stdout())?; + Ok(()) + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; From afdebd3ed49c741401188808c5bd1c9ab3ab8510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:33:14 -0400 Subject: [PATCH 06/10] add df_with_output entry point --- src/uu/df/src/df.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 3bfd2d938aa..3f8d8fecdc9 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -451,6 +451,72 @@ impl DfOutput for TextOutput { } } +/// Display filesystem usage information, sending output to a custom sink. +/// +/// This is the programmatic entry point for `df`. It gathers filesystems using +/// the provided options and sends the results to `output` without requiring +/// consumers to parse text output. +pub fn df_with_output(paths: Option<&[P]>, opt: &Options, output: &mut O) -> UResult<()> +where + P: AsRef, + O: DfOutput, +{ + output.initialize(opt)?; + + let filesystems = match paths { + None => { + let filesystems = get_all_filesystems(opt).map_err(|e| { + let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); + USimpleError::new(e.code(), format!("{context}: {e}")) + })?; + + if filesystems.is_empty() { + return Err(USimpleError::new( + 1, + translate!("df-error-no-file-systems-processed"), + )); + } + + filesystems + } + Some(paths) => { + let filesystems = get_named_filesystems(paths, opt).map_err(|e| { + let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); + USimpleError::new(e.code(), format!("{context}: {e}")) + })?; + + // This can happen if paths are given as command-line arguments + // but none of the paths exist. + if filesystems.is_empty() { + output.finalize(opt)?; + return Ok(()); + } + + filesystems + } + }; + + if matches!(output.stream_mode(), StreamMode::Streaming) { + for filesystem in &filesystems { + output.write_filesystem(filesystem, opt)?; + } + } else { + output.write_filesystems(&filesystems, opt)?; + } + + output.finalize(opt)?; + Ok(()) +} + +/// Display filesystem usage information as text on stdout. +pub fn df

(paths: Option<&[P]>, opt: &Options) -> UResult<()> +where + P: AsRef, +{ + let mut output = TextOutput; + df_with_output(paths, opt, &mut output) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; From 46ae2b40e211340279a59020dd78ed9cdf57f0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:33:41 -0400 Subject: [PATCH 07/10] route df cli through shared output path --- src/uu/df/src/df.rs | 44 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 3f8d8fecdc9..07ac9a1d696 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -23,7 +23,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource}; use std::ffi::OsString; use std::io::stdout; -use std::path::Path; +use std::path::{Path, PathBuf}; use thiserror::Error; use crate::blocks::{BlockSize, read_block_size}; @@ -532,44 +532,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - let opt = Options::from(&matches).map_err(DfError::OptionsError)?; - // Get the list of filesystems to display in the output table. - let filesystems: Vec = match matches.get_many::(OPT_PATHS) { - None => { - let filesystems = get_all_filesystems(&opt).map_err(|e| { - let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); - USimpleError::new(e.code(), format!("{context}: {e}")) - })?; - - if filesystems.is_empty() { - return Err(USimpleError::new( - 1, - translate!("df-error-no-file-systems-processed"), - )); - } - - filesystems - } - Some(paths) => { - let paths: Vec<_> = paths.collect(); - let filesystems = get_named_filesystems(&paths, &opt).map_err(|e| { - let context = translate!("df-error-cannot-read-table-of-mounted-filesystems"); - USimpleError::new(e.code(), format!("{context}: {e}")) - })?; + let opt = Options::from_matches(&matches)?; + let paths: Option> = matches + .get_many::(OPT_PATHS) + .map(|paths| paths.map(PathBuf::from).collect()); - // This can happen if paths are given as command-line arguments - // but none of the paths exist. - if filesystems.is_empty() { - return Ok(()); - } - - filesystems - } - }; - - Table::new(&opt, filesystems).write_to(&mut stdout())?; - - Ok(()) + df(paths.as_deref(), &opt) } pub fn uu_app() -> Command { From a47d91d4a05192b86c92a2c809a59e379e2718c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Wed, 13 May 2026 23:34:15 -0400 Subject: [PATCH 08/10] add streaming output tests for df --- src/uu/df/src/output.rs | 62 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs index b59d966c347..6d8bcb4a247 100644 --- a/src/uu/df/src/output.rs +++ b/src/uu/df/src/output.rs @@ -100,3 +100,65 @@ impl DfOutput for StreamingOutput { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsString; + use uucore::fsext::{FsUsage, MountInfo}; + + fn filesystem() -> Filesystem { + Filesystem { + file: Some(OsString::from("/tmp/file")), + mount_info: MountInfo { + dev_id: String::from("1"), + dev_name: String::from("/dev/disk1"), + fs_type: String::from("apfs"), + mount_dir: OsString::from("/"), + mount_option: String::new(), + mount_root: OsString::new(), + remote: false, + dummy: false, + }, + usage: FsUsage { + blocksize: 1024, + blocks: 10, + bfree: 4, + bavail: 3, + bavail_top_bit_set: false, + files: 20, + ffree: 5, + }, + } + } + + #[test] + fn test_streaming_output_new() { + let output = StreamingOutput::new(); + assert!(output.filesystems().is_empty()); + } + + #[test] + fn test_streaming_output_clear() { + let mut output = StreamingOutput::new(); + output + .write_filesystem(&filesystem(), &Options::default()) + .unwrap(); + assert_eq!(output.filesystems().len(), 1); + + output.clear(); + assert!(output.filesystems().is_empty()); + } + + #[test] + fn test_streaming_output_into_filesystems() { + let mut output = StreamingOutput::new(); + output + .write_filesystem(&filesystem(), &Options::default()) + .unwrap(); + + let filesystems = output.into_filesystems(); + assert_eq!(filesystems.len(), 1); + assert_eq!(filesystems[0].mount_info.dev_name, "/dev/disk1"); + } +} From f4ed9dd95ab89e92234d1fb323fd185134b18638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 14 May 2026 09:02:46 -0400 Subject: [PATCH 09/10] avoid cloning filesystems for df text output --- src/uu/df/src/df.rs | 10 +++++++--- src/uu/df/src/output.rs | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 07ac9a1d696..0fe5bfc0594 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -445,8 +445,12 @@ impl UError for DfError { pub struct TextOutput; impl DfOutput for TextOutput { - fn write_filesystems(&mut self, filesystems: &[Filesystem], options: &Options) -> UResult<()> { - Table::new(options, filesystems.to_vec()).write_to(&mut stdout())?; + fn write_filesystems( + &mut self, + filesystems: Vec, + options: &Options, + ) -> UResult<()> { + Table::new(options, filesystems).write_to(&mut stdout())?; Ok(()) } } @@ -501,7 +505,7 @@ where output.write_filesystem(filesystem, opt)?; } } else { - output.write_filesystems(&filesystems, opt)?; + output.write_filesystems(filesystems, opt)?; } output.finalize(opt)?; diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs index 6d8bcb4a247..41a092ba1b5 100644 --- a/src/uu/df/src/output.rs +++ b/src/uu/df/src/output.rs @@ -39,8 +39,12 @@ pub trait DfOutput { } /// Called with all filesystem entries in batch mode. - fn write_filesystems(&mut self, filesystems: &[Filesystem], options: &Options) -> UResult<()> { - for filesystem in filesystems { + fn write_filesystems( + &mut self, + filesystems: Vec, + options: &Options, + ) -> UResult<()> { + for filesystem in &filesystems { self.write_filesystem(filesystem, options)?; } Ok(()) From fcc342cdd7d15ca3c294aae337547ca8641fabb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9D=91=B7=F0=9D=92=89=F0=9D=92=8A=F0=9D=92=8D?= =?UTF-8?q?=F0=9D=92=90=F0=9D=92=84=F0=9D=92=82=F0=9D=92=8D=F0=9D=92=9A?= =?UTF-8?q?=F0=9D=92=94=F0=9D=92=95?= Date: Thu, 14 May 2026 09:24:15 -0400 Subject: [PATCH 10/10] spellchecker: ignore apfs --- src/uu/df/src/output.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs index 41a092ba1b5..2111e9b4481 100644 --- a/src/uu/df/src/output.rs +++ b/src/uu/df/src/output.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// spell-checker:ignore apfs + //! Output traits and types for programmatic access to df functionality. //! //! This module separates filesystem discovery from output formatting so