Skip to content
101 changes: 78 additions & 23 deletions src/uu/df/src/df.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
mod blocks;
mod columns;
mod filesystem;
pub mod output;
mod table;

use blocks::HumanReadable;
Expand All @@ -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";
Expand Down Expand Up @@ -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<HumanReadable>,
Expand Down Expand Up @@ -112,6 +114,11 @@ impl Default for Options {
}

impl Options {
/// Convert command-line arguments into [`Options`].
pub fn from_matches(matches: &ArgMatches) -> UResult<Self> {
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()
Expand Down Expand Up @@ -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<Filesystem>,
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<P, O>(paths: Option<&[P]>, opt: &Options, output: &mut O) -> UResult<()>
where
P: AsRef<Path>,
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<Filesystem> = match matches.get_many::<OsString>(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}"))
})?;
Expand All @@ -468,27 +484,66 @@ 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}"))
})?;

// 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
}
};

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<P>(paths: Option<&[P]>, opt: &Options) -> UResult<()>
where
P: AsRef<Path>,
{
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<Vec<PathBuf>> = matches
.get_many::<OsString>(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!())
Expand Down
2 changes: 1 addition & 1 deletion src/uu/df/src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
170 changes: 170 additions & 0 deletions src/uu/df/src/output.rs
Original file line number Diff line number Diff line change
@@ -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<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(())
}
}

/// A streaming sink that collects filesystem entries as they are emitted.
#[derive(Debug, Default)]
pub struct StreamingOutput {
filesystems: Vec<Filesystem>,
}

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<Filesystem> {
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");
}
}
Loading