diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index e1531ce8af8..0fe5bfc0594 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; @@ -22,13 +23,14 @@ 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}; 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() @@ -434,26 +441,35 @@ impl UError for DfError { } } -#[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - - #[cfg(windows)] - { - if matches.get_flag(OPT_INODES) { - println!( - "{}", - translate!("df-error-inodes-not-supported-windows", "program" => "df") - ); - return Ok(()); - } +/// 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: Vec, + options: &Options, + ) -> UResult<()> { + Table::new(options, filesystems).write_to(&mut stdout())?; + Ok(()) } +} + +/// 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 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) { + let filesystems = match paths { None => { - let filesystems = get_all_filesystems(&opt).map_err(|e| { + 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}")) })?; @@ -468,8 +484,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { filesystems } Some(paths) => { - let paths: Vec<_> = paths.collect(); - let filesystems = get_named_filesystems(&paths, &opt).map_err(|e| { + 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}")) })?; @@ -477,6 +492,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // 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(()); } @@ -484,11 +500,50 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } }; - Table::new(&opt, filesystems).write_to(&mut stdout())?; + 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)?; + + #[cfg(windows)] + { + if matches.get_flag(OPT_INODES) { + println!( + "{}", + translate!("df-error-inodes-not-supported-windows", "program" => "df") + ); + return Ok(()); + } + } + + let opt = Options::from_matches(&matches)?; + let paths: Option> = matches + .get_many::(OPT_PATHS) + .map(|paths| paths.map(PathBuf::from).collect()); + + df(paths.as_deref(), &opt) +} + pub fn uu_app() -> Command { Command::new("df") .version(uucore::crate_version!()) 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 diff --git a/src/uu/df/src/output.rs b/src/uu/df/src/output.rs new file mode 100644 index 00000000000..2111e9b4481 --- /dev/null +++ b/src/uu/df/src/output.rs @@ -0,0 +1,170 @@ +// 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. + +// spell-checker:ignore apfs + +//! 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: Vec, + 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(()) + } +} + +/// 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(()) + } +} + +#[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"); + } +}