1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 19:47:45 +00:00

df: fix block size header for multiples of 1024

Correct the column header printed by `df` when the `--block-size`
argument has a value that is a multiple of 1024. After this commit,
the header looks like "1K" or "4M" or "117G", etc., depending on the
particular value of the block size. For example:

    $ df --block-size=1024 | head -n1
    Filesystem                1K-blocks     Used Available Use% Mounted on
    $ df --block-size=2048 | head -n1
    Filesystem                2K-blocks     Used Available Use% Mounted on
    $ df --block-size=3072 | head -n1
    Filesystem                3K-blocks     Used Available Use% Mounted on
    $ df --block-size=4096 | head -n1
    Filesystem                4K-blocks     Used Available Use% Mounted on
This commit is contained in:
Jeffrey Finkelstein 2022-02-23 21:28:07 -05:00
parent 14e3f50651
commit 5cf7139467
4 changed files with 225 additions and 25 deletions

View file

@ -3,8 +3,72 @@
// * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code.
//! Types for representing and displaying block sizes.
use crate::{OPT_HUMAN_READABLE, OPT_HUMAN_READABLE_2};
use crate::{OPT_BLOCKSIZE, OPT_HUMAN_READABLE, OPT_HUMAN_READABLE_2};
use clap::ArgMatches;
use std::fmt;
use std::num::ParseIntError;
/// The first ten powers of 1024.
const IEC_BASES: [u128; 10] = [
1,
1_024,
1_048_576,
1_073_741_824,
1_099_511_627_776,
1_125_899_906_842_624,
1_152_921_504_606_846_976,
1_180_591_620_717_411_303_424,
1_208_925_819_614_629_174_706_176,
1_237_940_039_285_380_274_899_124_224,
];
/// Suffixes for the first nine multi-byte unit suffixes.
const SUFFIXES: [char; 9] = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
/// Convert a multiple of 1024 into a string like "12K" or "34M".
///
/// # Examples
///
/// Powers of 1024 become "1K", "1M", "1G", etc.
///
/// ```rust,ignore
/// assert_eq!(to_magnitude_and_suffix_1024(1024).unwrap(), "1K");
/// assert_eq!(to_magnitude_and_suffix_1024(1024 * 1024).unwrap(), "1M");
/// assert_eq!(to_magnitude_and_suffix_1024(1024 * 1024 * 1024).unwrap(), "1G");
/// ```
///
/// Multiples of those powers affect the magnitude part of the
/// returned string:
///
/// ```rust,ignore
/// assert_eq!(to_magnitude_and_suffix_1024(123 * 1024).unwrap(), "123K");
/// assert_eq!(to_magnitude_and_suffix_1024(456 * 1024 * 1024).unwrap(), "456M");
/// assert_eq!(to_magnitude_and_suffix_1024(789 * 1024 * 1024 * 1024).unwrap(), "789G");
/// ```
fn to_magnitude_and_suffix_1024(n: u128) -> Result<String, ()> {
// Find the smallest power of 1024 that is larger than `n`. That
// number indicates which units and suffix to use.
for i in 0..IEC_BASES.len() - 1 {
if n < IEC_BASES[i + 1] {
return Ok(format!("{}{}", n / IEC_BASES[i], SUFFIXES[i]));
}
}
Err(())
}
/// Convert a number into a magnitude and a multi-byte unit suffix.
///
/// # Errors
///
/// If the number is too large to represent.
fn to_magnitude_and_suffix(n: u128) -> Result<String, ()> {
if n % 1024 == 0 {
to_magnitude_and_suffix_1024(n)
} else {
// TODO Implement this, probably using code from `numfmt`.
Ok("1kB".into())
}
}
/// A block size to use in condensing the display of a large number of bytes.
///
@ -43,14 +107,91 @@ impl Default for BlockSize {
}
}
impl From<&ArgMatches> for BlockSize {
fn from(matches: &ArgMatches) -> Self {
if matches.is_present(OPT_HUMAN_READABLE) {
Self::HumanReadableBinary
} else if matches.is_present(OPT_HUMAN_READABLE_2) {
Self::HumanReadableDecimal
} else {
Self::default()
pub(crate) fn block_size_from_matches(matches: &ArgMatches) -> Result<BlockSize, ParseIntError> {
if matches.is_present(OPT_HUMAN_READABLE) {
Ok(BlockSize::HumanReadableBinary)
} else if matches.is_present(OPT_HUMAN_READABLE_2) {
Ok(BlockSize::HumanReadableDecimal)
} else if matches.is_present(OPT_BLOCKSIZE) {
let s = matches.value_of(OPT_BLOCKSIZE).unwrap();
Ok(BlockSize::Bytes(s.parse()?))
} else {
Ok(Default::default())
}
}
impl fmt::Display for BlockSize {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::HumanReadableBinary => write!(f, "Size"),
Self::HumanReadableDecimal => write!(f, "Size"),
Self::Bytes(n) => match to_magnitude_and_suffix(*n as u128) {
Ok(s) => write!(f, "{}-blocks", s),
Err(_) => Err(fmt::Error),
},
}
}
}
#[cfg(test)]
mod tests {
use crate::blocks::{to_magnitude_and_suffix, BlockSize};
#[test]
fn test_to_magnitude_and_suffix_powers_of_1024() {
assert_eq!(to_magnitude_and_suffix(1024).unwrap(), "1K");
assert_eq!(to_magnitude_and_suffix(2048).unwrap(), "2K");
assert_eq!(to_magnitude_and_suffix(4096).unwrap(), "4K");
assert_eq!(to_magnitude_and_suffix(1024 * 1024).unwrap(), "1M");
assert_eq!(to_magnitude_and_suffix(2 * 1024 * 1024).unwrap(), "2M");
assert_eq!(to_magnitude_and_suffix(1024 * 1024 * 1024).unwrap(), "1G");
assert_eq!(
to_magnitude_and_suffix(34 * 1024 * 1024 * 1024).unwrap(),
"34G"
);
}
// TODO We have not yet implemented this behavior, but when we do,
// uncomment this test.
// #[test]
// fn test_to_magnitude_and_suffix_not_powers_of_1024() {
// assert_eq!(to_magnitude_and_suffix(1).unwrap(), "1B");
// assert_eq!(to_magnitude_and_suffix(999).unwrap(), "999B");
// assert_eq!(to_magnitude_and_suffix(1000).unwrap(), "1kB");
// assert_eq!(to_magnitude_and_suffix(1001).unwrap(), "1.1kB");
// assert_eq!(to_magnitude_and_suffix(1023).unwrap(), "1.1kB");
// assert_eq!(to_magnitude_and_suffix(1025).unwrap(), "1.1kB");
// assert_eq!(to_magnitude_and_suffix(999_000).unwrap(), "999kB");
// assert_eq!(to_magnitude_and_suffix(999_001).unwrap(), "1MB");
// assert_eq!(to_magnitude_and_suffix(999_999).unwrap(), "1MB");
// assert_eq!(to_magnitude_and_suffix(1_000_000).unwrap(), "1MB");
// assert_eq!(to_magnitude_and_suffix(1_000_001).unwrap(), "1.1MB");
// assert_eq!(to_magnitude_and_suffix(1_100_000).unwrap(), "1.1MB");
// assert_eq!(to_magnitude_and_suffix(1_100_001).unwrap(), "1.2MB");
// assert_eq!(to_magnitude_and_suffix(1_900_000).unwrap(), "1.9MB");
// assert_eq!(to_magnitude_and_suffix(1_900_001).unwrap(), "2MB");
// assert_eq!(to_magnitude_and_suffix(9_900_000).unwrap(), "9.9MB");
// assert_eq!(to_magnitude_and_suffix(9_900_001).unwrap(), "10MB");
// assert_eq!(to_magnitude_and_suffix(999_000_000).unwrap(), "999MB");
// assert_eq!(to_magnitude_and_suffix(999_000_001).unwrap(), "1GB");
// assert_eq!(to_magnitude_and_suffix(1_000_000_000).unwrap(), "1GB");
// // etc.
// }
#[test]
fn test_block_size_display() {
assert_eq!(format!("{}", BlockSize::HumanReadableBinary), "Size");
assert_eq!(format!("{}", BlockSize::HumanReadableDecimal), "Size");
assert_eq!(format!("{}", BlockSize::Bytes(1024)), "1K-blocks");
assert_eq!(format!("{}", BlockSize::Bytes(2 * 1024)), "2K-blocks");
assert_eq!(
format!("{}", BlockSize::Bytes(3 * 1024 * 1024)),
"3M-blocks"
);
}
}

View file

@ -9,20 +9,22 @@
mod blocks;
mod table;
use uucore::error::{UResult, USimpleError};
use uucore::format_usage;
#[cfg(unix)]
use uucore::fsext::statfs;
use uucore::fsext::{read_fs_list, FsUsage, MountInfo};
use uucore::{error::UResult, format_usage};
use clap::{crate_version, App, AppSettings, Arg, ArgMatches};
use std::collections::HashSet;
use std::fmt;
use std::iter::FromIterator;
#[cfg(windows)]
use std::path::Path;
use crate::blocks::BlockSize;
use crate::blocks::{block_size_from_matches, BlockSize};
use crate::table::{DisplayRow, Header, Row};
static ABOUT: &str = "Show information about the file system on which each FILE resides,\n\
@ -78,19 +80,36 @@ struct Options {
show_total: bool,
}
enum OptionsError {
InvalidBlockSize,
}
impl fmt::Display for OptionsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
// TODO This should include the raw string provided as the argument.
//
// TODO This needs to vary based on whether `--block-size`
// or `-B` were provided.
Self::InvalidBlockSize => write!(f, "invalid --block-size argument"),
}
}
}
impl Options {
/// Convert command-line arguments into [`Options`].
fn from(matches: &ArgMatches) -> Self {
Self {
fn from(matches: &ArgMatches) -> Result<Self, OptionsError> {
Ok(Self {
show_local_fs: matches.is_present(OPT_LOCAL),
show_all_fs: matches.is_present(OPT_ALL),
show_listed_fs: false,
show_fs_type: matches.is_present(OPT_PRINT_TYPE),
show_inode_instead: matches.is_present(OPT_INODES),
block_size: BlockSize::from(matches),
block_size: block_size_from_matches(matches)
.map_err(|_| OptionsError::InvalidBlockSize)?,
fs_selector: FsSelector::from(matches),
show_total: matches.is_present(OPT_TOTAL),
}
})
}
}
@ -260,7 +279,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}
let opt = Options::from(&matches);
let opt = Options::from(&matches).map_err(|e| USimpleError::new(1, format!("{}", e)))?;
let mounts = read_fs_list();
let data: Vec<Row> = filter_mount_list(mounts, &paths, &opt)

View file

@ -306,13 +306,12 @@ impl fmt::Display for Header<'_> {
write!(f, "{0: >12} ", "IFree")?;
write!(f, "{0: >5} ", "IUse%")?;
} else {
// TODO Support arbitrary positive scaling factors (from
// the `--block-size` command-line argument).
if let BlockSize::Bytes(_) = self.options.block_size {
write!(f, "{0: >12} ", "1k-blocks")?;
} else {
write!(f, "{0: >12} ", "Size")?;
};
// `Display` is implemented for `BlockSize`, but `Display`
// only works when formatting an object into an empty
// format, `{}`. So we use `format!()` first to create the
// string, then use `write!()` to align the string and pad
// with spaces.
write!(f, "{0: >12} ", format!("{}", self.options.block_size))?;
write!(f, "{0: >12} ", "Used")?;
write!(f, "{0: >12} ", "Available")?;
#[cfg(target_os = "macos")]
@ -335,7 +334,7 @@ mod tests {
let options = Default::default();
assert_eq!(
Header::new(&options).to_string(),
"Filesystem 1k-blocks Used Available Use% Mounted on "
"Filesystem 1K-blocks Used Available Use% Mounted on "
);
}
@ -347,7 +346,7 @@ mod tests {
};
assert_eq!(
Header::new(&options).to_string(),
"Filesystem Type 1k-blocks Used Available Use% Mounted on "
"Filesystem Type 1K-blocks Used Available Use% Mounted on "
);
}
@ -363,6 +362,18 @@ mod tests {
);
}
#[test]
fn test_header_display_block_size_1024() {
let options = Options {
block_size: BlockSize::Bytes(3 * 1024),
..Default::default()
};
assert_eq!(
Header::new(&options).to_string(),
"Filesystem 3K-blocks Used Available Use% Mounted on "
);
}
#[test]
fn test_header_display_human_readable_binary() {
let options = Options {

View file

@ -28,6 +28,8 @@ fn test_df_compatible_si() {
#[test]
fn test_df_output() {
// TODO These should fail because `-total` should have two dashes,
// not just one. But they don't fail.
if cfg!(target_os = "macos") {
new_ucmd!().arg("-H").arg("-total").succeeds().
stdout_only("Filesystem Size Used Available Capacity Use% Mounted on \n");
@ -121,4 +123,31 @@ fn test_total() {
// TODO We could also check here that the use percentage matches.
}
#[test]
fn test_block_size_1024() {
fn get_header(block_size: u64) -> String {
// TODO When #3057 is resolved, we should just use
//
// new_ucmd!().arg("--output=size").succeeds().stdout_move_str();
//
// instead of parsing the entire `df` table as a string.
let output = new_ucmd!()
.args(&["-B", &format!("{}", block_size)])
.succeeds()
.stdout_move_str();
let first_line = output.lines().next().unwrap();
let mut column_labels = first_line.split_whitespace();
let size_column_label = column_labels.nth(1).unwrap();
size_column_label.into()
}
assert_eq!(get_header(1024), "1K-blocks");
assert_eq!(get_header(2048), "2K-blocks");
assert_eq!(get_header(4096), "4K-blocks");
assert_eq!(get_header(1024 * 1024), "1M-blocks");
assert_eq!(get_header(2 * 1024 * 1024), "2M-blocks");
assert_eq!(get_header(1024 * 1024 * 1024), "1G-blocks");
assert_eq!(get_header(34 * 1024 * 1024 * 1024), "34G-blocks");
}
// ToDO: more tests...