df: add support for --total option

Add support for the `--total` option to `df`, which displays the total
of each numeric column. For example,

    $ df --total
    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% -
This commit is contained in:
Jeffrey Finkelstein 2022-02-27 11:20:27 -05:00 committed by Sylvestre Ledru
parent 41acdb5471
commit d0ebd1c9d0
3 changed files with 117 additions and 0 deletions

View file

@ -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(())

View file

@ -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<f64>,
}
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<Filesystem> for Row {
fn from(fs: Filesystem) -> Self {
let MountInfo {

View file

@ -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::<u64>().unwrap();
computed_total_used += iter.next().unwrap().parse::<u64>().unwrap();
computed_total_avail += iter.next().unwrap().parse::<u64>().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...