diff --git a/src/uu/df/src/df.rs b/src/uu/df/src/df.rs index e856a6b1e..a51966d9e 100644 --- a/src/uu/df/src/df.rs +++ b/src/uu/df/src/df.rs @@ -5,8 +5,8 @@ // // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. +mod table; -use uucore::error::UError; use uucore::error::UResult; #[cfg(unix)] use uucore::fsext::statfs_fn; @@ -14,14 +14,11 @@ use uucore::fsext::{read_fs_list, FsUsage, MountInfo}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches}; -use number_prefix::NumberPrefix; use std::cell::Cell; use std::collections::HashMap; use std::collections::HashSet; -use std::error::Error; #[cfg(unix)] use std::ffi::CString; -use std::fmt::Display; use std::iter::FromIterator; #[cfg(unix)] use std::mem; @@ -29,6 +26,8 @@ use std::mem; #[cfg(windows)] use std::path::Path; +use crate::table::{DisplayRow, Header, Row}; + static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\ or all file systems by default."; @@ -58,6 +57,7 @@ struct FsSelector { exclude: HashSet, } +#[derive(Default)] struct Options { show_local_fs: bool, show_all_fs: bool, @@ -236,64 +236,6 @@ fn filter_mount_list(vmi: Vec, paths: &[String], opt: &Options) -> Ve .collect() } -/// Convert `value` to a human readable string based on `base`. -/// e.g. It returns 1G when value is 1 * 1024 * 1024 * 1024 and base is 1024. -/// Note: It returns `value` if `base` isn't positive. -fn human_readable(value: u64, base: i64) -> UResult { - let base_str = match base { - d if d < 0 => value.to_string(), - - // ref: [Binary prefix](https://en.wikipedia.org/wiki/Binary_prefix) @@ - // ref: [SI/metric prefix](https://en.wikipedia.org/wiki/Metric_prefix) @@ - 1000 => match NumberPrefix::decimal(value as f64) { - NumberPrefix::Standalone(bytes) => bytes.to_string(), - NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), - }, - - 1024 => match NumberPrefix::binary(value as f64) { - NumberPrefix::Standalone(bytes) => bytes.to_string(), - NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()), - }, - - _ => return Err(DfError::InvalidBaseValue(base.to_string()).into()), - }; - - Ok(base_str) -} - -fn use_size(free_size: u64, total_size: u64) -> String { - if total_size == 0 { - return String::from("-"); - } - return format!( - "{:.0}%", - 100f64 - 100f64 * (free_size as f64 / total_size as f64) - ); -} - -#[derive(Debug)] -enum DfError { - InvalidBaseValue(String), -} - -impl Display for DfError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DfError::InvalidBaseValue(s) => write!(f, "Internal error: Unknown base value {}", s), - } - } -} - -impl Error for DfError {} - -impl UError for DfError { - fn code(&self) -> i32 { - match self { - DfError::InvalidBaseValue(_) => 1, - } - } -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let usage = usage(); @@ -314,98 +256,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let opt = Options::from(&matches); - let fs_list = filter_mount_list(read_fs_list(), &paths, &opt) + let mounts = read_fs_list(); + let data: Vec = filter_mount_list(mounts, &paths, &opt) .into_iter() .filter_map(Filesystem::new) .filter(|fs| fs.usage.blocks != 0 || opt.show_all_fs || opt.show_listed_fs) - .collect::>(); - - // set headers - let mut header = vec!["Filesystem"]; - if opt.show_fs_type { - header.push("Type"); - } - header.extend_from_slice(&if opt.show_inode_instead { - // spell-checker:disable-next-line - ["Inodes", "Iused", "IFree", "IUses%"] - } else { - [ - if opt.human_readable_base == -1 { - "1k-blocks" - } else { - "Size" - }, - "Used", - "Available", - "Use%", - ] - }); - if cfg!(target_os = "macos") && !opt.show_inode_instead { - header.insert(header.len() - 1, "Capacity"); - } - header.push("Mounted on"); - - for (idx, title) in header.iter().enumerate() { - if idx == 0 || idx == header.len() - 1 { - print!("{0: <16} ", title); - } else if opt.show_fs_type && idx == 1 { - print!("{0: <5} ", title); - } else if idx == header.len() - 2 { - print!("{0: >5} ", title); - } else { - print!("{0: >12} ", title); - } - } - println!(); - for fs in &fs_list { - print!("{0: <16} ", fs.mount_info.dev_name); - if opt.show_fs_type { - print!("{0: <5} ", fs.mount_info.fs_type); - } - if opt.show_inode_instead { - print!( - "{0: >12} ", - human_readable(fs.usage.files, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(fs.usage.files - fs.usage.ffree, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(fs.usage.ffree, opt.human_readable_base)? - ); - print!( - "{0: >5} ", - format!( - "{0:.1}%", - 100f64 - 100f64 * (fs.usage.ffree as f64 / fs.usage.files as f64) - ) - ); - } else { - let total_size = fs.usage.blocksize * fs.usage.blocks; - let free_size = fs.usage.blocksize * fs.usage.bfree; - print!( - "{0: >12} ", - human_readable(total_size, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(total_size - free_size, opt.human_readable_base)? - ); - print!( - "{0: >12} ", - human_readable(free_size, opt.human_readable_base)? - ); - if cfg!(target_os = "macos") { - let used = fs.usage.blocks - fs.usage.bfree; - let blocks = used + fs.usage.bavail; - print!("{0: >12} ", use_size(used, blocks)); - } - print!("{0: >5} ", use_size(free_size, total_size)); - } - print!("{0: <16}", fs.mount_info.mount_dir); - println!(); + .map(Into::into) + .collect(); + println!("{}", Header::new(&opt)); + for row in data { + println!("{}", DisplayRow::new(row, &opt)); } Ok(()) diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs new file mode 100644 index 000000000..5876bf7d2 --- /dev/null +++ b/src/uu/df/src/table.rs @@ -0,0 +1,501 @@ +// * 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 tmpfs +//! The filesystem usage data table. +//! +//! A table comprises a header row ([`Header`]) and a collection of +//! data rows ([`Row`]), one per filesystem. To display a [`Row`], +//! combine it with [`Options`] in the [`DisplayRow`] struct; the +//! [`DisplayRow`] implements [`std::fmt::Display`]. +use number_prefix::NumberPrefix; + +use crate::{Filesystem, Options}; +use uucore::fsext::{FsUsage, MountInfo}; + +use std::fmt; + +/// A row in the filesystem usage data table. +/// +/// A row comprises several pieces of information, including the +/// filesystem device, the mountpoint, the number of bytes used, etc. +pub(crate) struct Row { + /// Name of the device on which the filesystem lives. + fs_device: String, + + /// Type of filesystem (for example, `"ext4"`, `"tmpfs"`, etc.). + fs_type: String, + + /// Path at which the filesystem is mounted. + fs_mount: String, + + /// Total number of bytes in the filesystem regardless of whether they are used. + bytes: u64, + + /// Number of used bytes. + bytes_used: u64, + + /// Number of free bytes. + bytes_free: u64, + + /// Percentage of bytes that are used, given as a float between 0 and 1. + /// + /// If the filesystem has zero bytes, then this is `None`. + bytes_usage: Option, + + /// Percentage of bytes that are available, given as a float between 0 and 1. + /// + /// These are the bytes that are available to non-privileged processes. + /// + /// If the filesystem has zero bytes, then this is `None`. + #[cfg(target_os = "macos")] + bytes_capacity: Option, + + /// Total number of inodes in the filesystem. + inodes: u64, + + /// Number of used inodes. + inodes_used: u64, + + /// Number of free inodes. + inodes_free: u64, + + /// Percentage of inodes that are used, given as a float between 0 and 1. + /// + /// If the filesystem has zero bytes, then this is `None`. + inodes_usage: Option, +} + +impl From for Row { + fn from(fs: Filesystem) -> Self { + let MountInfo { + dev_name, + fs_type, + mount_dir, + .. + } = fs.mount_info; + let FsUsage { + blocksize, + blocks, + bfree, + #[cfg(target_os = "macos")] + bavail, + files, + ffree, + .. + } = fs.usage; + Self { + fs_device: dev_name, + fs_type, + fs_mount: mount_dir, + bytes: blocksize * blocks, + bytes_used: blocksize * (blocks - bfree), + bytes_free: blocksize * bfree, + bytes_usage: if blocks == 0 { + None + } else { + Some(((blocks - bfree) as f64) / blocks as f64) + }, + #[cfg(target_os = "macos")] + bytes_capacity: if bavail == 0 { + None + } else { + Some(bavail as f64 / ((blocks - bfree + bavail) as f64)) + }, + inodes: files, + inodes_used: files - ffree, + inodes_free: ffree, + inodes_usage: if files == 0 { + None + } else { + Some(ffree as f64 / files as f64) + }, + } + } +} + +/// A displayable wrapper around a [`Row`]. +/// +/// The `options` control how the information in the row gets displayed. +pub(crate) struct DisplayRow<'a> { + /// The data in this row. + row: Row, + + /// Options that control how to display the data. + options: &'a Options, + // TODO We don't need all of the command-line options here. Some + // of the command-line options indicate which rows to include or + // exclude. Other command-line options indicate which columns to + // include or exclude. Still other options indicate how to format + // numbers. We could split the options up into those groups to + // reduce the coupling between this `table.rs` module and the main + // `df.rs` module. +} + +impl<'a> DisplayRow<'a> { + /// Instantiate this struct. + pub(crate) fn new(row: Row, options: &'a Options) -> Self { + Self { row, options } + } + + /// Get a string giving the scaled version of the input number. + /// + /// The scaling factor is defined in the `options` field. + /// + /// # Errors + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn scaled(&self, size: u64) -> Result { + // TODO The argument-parsing code should be responsible for + // ensuring that the `human_readable_base` number is + // positive. Then we could remove the `Err` case from this + // function. + // + // TODO We should not be using a negative number to indicate + // default behavior. The default behavior for `df` is to show + // sizes in blocks of 1K bytes each, so we should just do + // that. + // + // TODO Support arbitrary positive scaling factors (from the + // `--block-size` command-line argument). + let number_prefix = match self.options.human_readable_base { + 1000 => NumberPrefix::decimal(size as f64), + 1024 => NumberPrefix::binary(size as f64), + d if d < 0 => return Ok(size.to_string()), + _ => return Err(fmt::Error {}), + }; + match number_prefix { + NumberPrefix::Standalone(bytes) => Ok(bytes.to_string()), + NumberPrefix::Prefixed(prefix, bytes) => Ok(format!("{:.1}{}", bytes, prefix.symbol())), + } + } + + /// Convert a float between 0 and 1 into a percentage string. + /// + /// If `None`, return the string `"-"` instead. + fn percentage(fraction: Option) -> String { + match fraction { + None => "-".to_string(), + Some(x) => format!("{:.0}%", 100.0 * x), + } + } + + /// Write the bytes data for this row. + /// + /// # Errors + /// + /// If there is a problem writing to `f`. + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn fmt_bytes(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: >12} ", self.scaled(self.row.bytes)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.bytes_used)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.bytes_free)?)?; + #[cfg(target_os = "macos")] + write!( + f, + "{0: >12} ", + DisplayRow::percentage(self.row.bytes_capacity) + )?; + write!(f, "{0: >5} ", DisplayRow::percentage(self.row.bytes_usage))?; + Ok(()) + } + + /// Write the inodes data for this row. + /// + /// # Errors + /// + /// If there is a problem writing to `f`. + /// + /// If the scaling factor is not 1000, 1024, or a negative number. + fn fmt_inodes(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: >12} ", self.scaled(self.row.inodes)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.inodes_used)?)?; + write!(f, "{0: >12} ", self.scaled(self.row.inodes_free)?)?; + write!(f, "{0: >5} ", DisplayRow::percentage(self.row.inodes_usage))?; + Ok(()) + } +} + +impl fmt::Display for DisplayRow<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: <16} ", self.row.fs_device)?; + if self.options.show_fs_type { + write!(f, "{0: <5} ", self.row.fs_type)?; + } + if self.options.show_inode_instead { + self.fmt_inodes(f)?; + } else { + self.fmt_bytes(f)?; + } + write!(f, "{0: <16}", self.row.fs_mount)?; + Ok(()) + } +} + +/// The header row. +/// +/// The `options` control which columns are displayed. +pub(crate) struct Header<'a> { + /// Options that control which columns are displayed. + options: &'a Options, +} + +impl<'a> Header<'a> { + /// Instantiate this struct. + pub(crate) fn new(options: &'a Options) -> Self { + Self { options } + } +} + +impl fmt::Display for Header<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{0: <16} ", "Filesystem")?; + if self.options.show_fs_type { + write!(f, "{0: <5} ", "Type")?; + } + if self.options.show_inode_instead { + write!(f, "{0: >12} ", "Inodes")?; + write!(f, "{0: >12} ", "IUsed")?; + write!(f, "{0: >12} ", "IFree")?; + write!(f, "{0: >5} ", "IUse%")?; + } else { + if self.options.human_readable_base == -1 { + write!(f, "{0: >12} ", "1k-blocks")?; + } else { + write!(f, "{0: >12} ", "Size")?; + }; + write!(f, "{0: >12} ", "Used")?; + write!(f, "{0: >12} ", "Available")?; + #[cfg(target_os = "macos")] + write!(f, "{0: >12} ", "Capacity")?; + write!(f, "{0: >5} ", "Use%")?; + } + write!(f, "{0: <16} ", "Mounted on")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::table::{DisplayRow, Header, Row}; + use crate::Options; + + #[test] + fn test_header_display() { + let options = Options { + human_readable_base: -1, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem 1k-blocks Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_fs_type() { + let options = Options { + human_readable_base: -1, + show_fs_type: true, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Type 1k-blocks Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_inode() { + let options = Options { + human_readable_base: -1, + show_inode_instead: true, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Inodes IUsed IFree IUse% Mounted on " + ); + } + + #[test] + fn test_header_display_human_readable_binary() { + let options = Options { + human_readable_base: 1024, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Size Used Available Use% Mounted on " + ); + } + + #[test] + fn test_header_display_human_readable_si() { + let options = Options { + human_readable_base: 1000, + ..Default::default() + }; + assert_eq!( + Header::new(&options).to_string(), + "Filesystem Size Used Available Use% Mounted on " + ); + } + + #[test] + fn test_row_display() { + let options = Options { + human_readable_base: -1, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device 100 25 75 25% my_mount " + ); + } + + #[test] + fn test_row_display_fs_type() { + let options = Options { + human_readable_base: -1, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 100 25 75 25% my_mount " + ); + } + + #[test] + fn test_row_display_inodes() { + let options = Options { + human_readable_base: -1, + show_inode_instead: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 100, + bytes_used: 25, + bytes_free: 75, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device 10 2 8 20% my_mount " + ); + } + + #[test] + fn test_row_display_human_readable_si() { + let options = Options { + human_readable_base: 1000, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 4000, + bytes_used: 1000, + bytes_free: 3000, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 4.0k 1.0k 3.0k 25% my_mount " + ); + } + + #[test] + fn test_row_display_human_readable_binary() { + let options = Options { + human_readable_base: 1024, + show_fs_type: true, + ..Default::default() + }; + let row = Row { + fs_device: "my_device".to_string(), + fs_type: "my_type".to_string(), + fs_mount: "my_mount".to_string(), + + bytes: 4096, + bytes_used: 1024, + bytes_free: 3072, + bytes_usage: Some(0.25), + + #[cfg(target_os = "macos")] + bytes_capacity: Some(0.5), + + inodes: 10, + inodes_used: 2, + inodes_free: 8, + inodes_usage: Some(0.2), + }; + assert_eq!( + DisplayRow::new(row, &options).to_string(), + "my_device my_type 4.0Ki 1.0Ki 3.0Ki 25% my_mount " + ); + } +} diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index a3b05dff8..b8207d68c 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -567,7 +567,7 @@ impl FsUsage { // Total number of file nodes (inodes) on the file system. files: 0, // Not available on windows // Total number of free file nodes (inodes). - ffree: 4096, // Meaningless on Windows + ffree: 0, // Meaningless on Windows } } }