diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index 436c19247..6371bb0be 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -106,6 +106,11 @@ impl From<&ArgMatches> for BlockSize { } } +/// Parameters that control the behavior of `df`. +/// +/// 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. #[derive(Default)] struct Options { show_local_fs: bool, @@ -115,6 +120,9 @@ struct Options { show_inode_instead: bool, block_size: BlockSize, fs_selector: FsSelector, + + /// Whether to show a final row comprising the totals for each column. + show_total: bool, } impl Options { @@ -128,6 +136,7 @@ impl Options { show_inode_instead: matches.is_present(OPT_INODES), block_size: BlockSize::from(matches), fs_selector: FsSelector::from(matches), + show_total: matches.is_present(OPT_TOTAL), } } } @@ -307,9 +316,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .filter(|fs| fs.usage.blocks != 0 || opt.show_all_fs || opt.show_listed_fs) .map(Into::into) .collect(); + println!("{}", Header::new(&opt)); + let mut total = Row::new("total"); for row in data { println!("{}", DisplayRow::new(&row, &opt)); + total += row; + } + if opt.show_total { + println!("{}", DisplayRow::new(&total, &opt)); } Ok(()) diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index 682d7836b..1b1c61d1f 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -15,6 +15,7 @@ use crate::{BlockSize, Filesystem, Options}; use uucore::fsext::{FsUsage, MountInfo}; use std::fmt; +use std::ops::AddAssign; /// A row in the filesystem usage data table. /// @@ -67,6 +68,63 @@ pub(crate) struct Row { inodes_usage: Option, } +impl Row { + pub(crate) fn new(source: &str) -> Self { + Self { + fs_device: source.into(), + fs_type: "-".into(), + fs_mount: "-".into(), + bytes: 0, + bytes_used: 0, + bytes_free: 0, + bytes_usage: None, + #[cfg(target_os = "macos")] + bytes_capacity: None, + inodes: 0, + inodes_used: 0, + inodes_free: 0, + inodes_usage: None, + } + } +} + +impl AddAssign for Row { + /// Sum the numeric values of two rows. + /// + /// The `Row::fs_device` field is set to `"total"` and the + /// remaining `String` fields are set to `"-"`. + fn add_assign(&mut self, rhs: Self) { + let bytes = self.bytes + rhs.bytes; + let bytes_used = self.bytes_used + rhs.bytes_used; + let inodes = self.inodes + rhs.inodes; + let inodes_used = self.inodes_used + rhs.inodes_used; + *self = Self { + fs_device: "total".into(), + fs_type: "-".into(), + fs_mount: "-".into(), + bytes, + bytes_used, + bytes_free: self.bytes_free + rhs.bytes_free, + bytes_usage: if bytes == 0 { + None + } else { + Some(bytes_used as f64 / bytes as f64) + }, + // TODO Figure out how to compute this. + #[cfg(target_os = "macos")] + bytes_capacity: None, + inodes, + inodes_used, + inodes_free: self.inodes_free + rhs.inodes_free, + inodes_usage: if inodes == 0 { + None + } else { + Some(inodes_used as f64 / inodes as f64) + }, + } + } +} + impl From for Row { fn from(fs: Filesystem) -> Self { let MountInfo { diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 3af02428e..513423766 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -1,3 +1,4 @@ +// spell-checker:ignore udev use crate::common::util::*; #[test] @@ -77,4 +78,47 @@ fn test_type_option() { new_ucmd!().args(&["-t", "ext4", "-t", "ext3"]).succeeds(); } +#[test] +fn test_total() { + // Example output: + // + // Filesystem 1K-blocks Used Available Use% Mounted on + // udev 3858016 0 3858016 0% /dev + // ... + // /dev/loop14 63488 63488 0 100% /snap/core20/1361 + // total 258775268 98099712 148220200 40% - + let output = new_ucmd!().arg("--total").succeeds().stdout_move_str(); + + // Skip the header line. + let lines: Vec<&str> = output.lines().skip(1).collect(); + + // Parse the values from the last row. + let last_line = lines.last().unwrap(); + let mut iter = last_line.split_whitespace(); + assert_eq!(iter.next().unwrap(), "total"); + let reported_total_size = iter.next().unwrap().parse().unwrap(); + let reported_total_used = iter.next().unwrap().parse().unwrap(); + let reported_total_avail = iter.next().unwrap().parse().unwrap(); + + // Loop over each row except the last, computing the sum of each column. + let mut computed_total_size = 0; + let mut computed_total_used = 0; + let mut computed_total_avail = 0; + let n = lines.len(); + for line in &lines[..n - 1] { + let mut iter = line.split_whitespace(); + iter.next().unwrap(); + computed_total_size += iter.next().unwrap().parse::().unwrap(); + computed_total_used += iter.next().unwrap().parse::().unwrap(); + computed_total_avail += iter.next().unwrap().parse::().unwrap(); + } + + // Check that the sum of each column matches the reported value in + // the last row. + assert_eq!(computed_total_size, reported_total_size); + assert_eq!(computed_total_used, reported_total_used); + assert_eq!(computed_total_avail, reported_total_avail); + // TODO We could also check here that the use percentage matches. +} + // ToDO: more tests...