From f2e7c2f04df418135839ce0386e125602c89431a Mon Sep 17 00:00:00 2001 From: mattsu Date: Thu, 1 Oct 2026 21:19:51 +0900 Subject: [PATCH] du: show suffixes for unit-only block sizes --- src/uu/du/src/block_size.rs | 49 ++++++++++++++++++++ src/uu/du/src/du.rs | 63 ++++++++++++-------------- tests/by-util/test_du.rs | 90 +++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 34 deletions(-) create mode 100644 src/uu/du/src/block_size.rs diff --git a/src/uu/du/src/block_size.rs b/src/uu/du/src/block_size.rs new file mode 100644 index 0000000000..57841f2629 --- /dev/null +++ b/src/uu/du/src/block_size.rs @@ -0,0 +1,49 @@ +// 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. + +use uucore::parser::parse_size::{ParseSizeError, parse_size_non_zero_u64}; + +#[derive(Clone)] +pub(super) struct BlockSize { + pub(super) bytes: u64, + suffix: String, +} + +impl BlockSize { + pub(super) fn new(bytes: u64) -> Self { + Self { + bytes, + suffix: String::new(), + } + } + + pub(super) fn parse(value: &str) -> Result { + let size = value; + let bytes = parse_size_non_zero_u64(size)?; + // An omitted multiplier requests a unit label; an explicit multiplier + // requests just the count, even when that multiplier is one. + let mut suffix = if size.starts_with(|c: char| c.is_ascii_digit()) { + String::new() + } else { + size.to_owned() + }; + if let Some(first) = suffix.get_mut(..1) { + first.make_ascii_uppercase(); + } + if suffix == "KB" { + "kB".clone_into(&mut suffix); + } else if suffix.ends_with('D') { + suffix.pop(); + } + Ok(Self { bytes, suffix }) + } + + pub(super) fn format(&self, size: u64) -> String { + let count = size.div_ceil(self.bytes); + let mut number = count.to_string(); + number.push_str(&self.suffix); + number + } +} diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 48dff14064..cb1ce38d59 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -33,6 +33,7 @@ use uucore::line_ending::LineEnding; use uucore::safe_traversal::{DirFd, SymlinkBehavior}; use uucore::translate; +use block_size::BlockSize; use uucore::parser::parse_block_size; use uucore::parser::parse_glob; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; @@ -47,6 +48,8 @@ use windows_sys::Win32::Storage::FileSystem::{ FileStandardInfo, GetFileInformationByHandleEx, }; +mod block_size; + mod options { pub const HELP: &str = "help"; pub const NULL: &str = "0"; @@ -114,7 +117,7 @@ enum Deref { enum SizeFormat { HumanDecimal, HumanBinary, - BlockSize(u64), + BlockSize(BlockSize), } #[derive(PartialEq, Eq, Hash, Clone, Copy)] @@ -302,35 +305,27 @@ fn get_file_info(path: &Path, _metadata: &Metadata) -> Option { result } -fn read_block_size(s: Option<&str>, diag_args: Option<&[OsString]>) -> UResult { +fn read_block_size(s: Option<&str>, diag_args: Option<&[OsString]>) -> UResult { if let Some(s) = s { - parse_size_u64(s) - .and_then(|bytes| { - // A block size of zero is rejected here rather than by the - // caller, so that it goes through the caret path like every - // other bad SIZE. - if bytes == 0 { - Err(ParseSizeError::ParseFailure(s.to_string())) - } else { - Ok(bytes) - } - }) - .map_err(|e| { - let message = format_error_message(&e, s, options::BLOCK_SIZE); - e.size_value_error( - diag_args, - &OptionValue::new(s, 'B', options::BLOCK_SIZE), - 0, - &message, - USimpleError::new(1, message.clone()), - ) - }) - } else if let Some(bytes) = - parse_block_size::block_size_from_env(&["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"]).found() - { - Ok(bytes) + BlockSize::parse(s).map_err(|e| { + let message = format_error_message(&e, s, options::BLOCK_SIZE); + e.size_value_error( + diag_args, + &OptionValue::new(s, 'B', options::BLOCK_SIZE), + 0, + &message, + USimpleError::new(1, message.clone()), + ) + }) } else { - Ok(parse_block_size::default_block_size()) + // The first set variable wins, even if its value is invalid. + let from_env = ["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] + .into_iter() + .find_map(|name| env::var(name).ok()); + Ok(from_env + .as_deref() + .and_then(|value| BlockSize::parse(value).ok()) + .unwrap_or_else(|| BlockSize::new(parse_block_size::default_block_size()))) } } @@ -899,7 +894,7 @@ impl StatPrinter { } fn convert_size(&self, size: u64) -> String { - match self.size_format { + match &self.size_format { SizeFormat::HumanDecimal => uucore::format::human::human_readable( size, uucore::format::human::SizeFormat::Decimal, @@ -913,7 +908,7 @@ impl StatPrinter { // we ignore block size (-B) with --inodes size.to_string() } else { - size.div_ceil(block_size).to_string() + block_size.format(size) } } } @@ -1017,15 +1012,15 @@ fn parse_size_format(matches: &ArgMatches, diag_args: Option<&[OsString]>) -> UR )?); let candidates = [ ( - SizeFormat::BlockSize(1), + SizeFormat::BlockSize(BlockSize::new(1)), get_size_format_flag_arg_index_if_present(matches, options::BYTES), ), ( - SizeFormat::BlockSize(1024), + SizeFormat::BlockSize(BlockSize::new(1024)), get_size_format_flag_arg_index_if_present(matches, options::BLOCK_SIZE_1K), ), ( - SizeFormat::BlockSize(1024 * 1024), + SizeFormat::BlockSize(BlockSize::new(1024 * 1024)), get_size_format_flag_arg_index_if_present(matches, options::BLOCK_SIZE_1M), ), ( @@ -1633,7 +1628,7 @@ mod test_du { fn test_read_block_size() { let test_data = [Some("1024".to_string()), Some("K".to_string()), None]; for it in &test_data { - assert!(matches!(read_block_size(it.as_deref(), None), Ok(1024))); + assert_eq!(read_block_size(it.as_deref(), None).unwrap().bytes, 1024); } } } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 39cc462699..76b8da7cdf 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -298,6 +298,96 @@ fn test_du_env_block_size_hierarchy() { assert_eq!(expected, result2); } +#[test] +fn test_du_block_size_unit_display() { + use std::fmt::Write; + + let ts = TestScenario::new(util_name!()); + // Sparse files exercise rounding without depending on filesystem allocation. + let files = ["empty", "below", "exact", "above", "large"]; + for (unit, bytes, suffix) in [ + ("K", 1024_u64, "K"), + ("k", 1024, "K"), + ("KiB", 1024, "KiB"), + ("kiB", 1024, "KiB"), + ("M", 1_048_576, "M"), + ("m", 1_048_576, "M"), + ("MiB", 1_048_576, "MiB"), + ("miB", 1_048_576, "MiB"), + ("kB", 1000, "kB"), + ("KB", 1000, "kB"), + ("MB", 1_000_000, "MB"), + ("mB", 1_000_000, "MB"), + ("kD", 1000, "K"), + ] { + let large_blocks = if bytes <= 1024 { 12_345 } else { 12 }; + let lengths = [0, bytes - 1, bytes, bytes + 1, bytes * large_blocks + 17]; + for (file, len) in files.iter().zip(lengths) { + std::fs::File::create(ts.fixtures.plus(file)) + .unwrap() + .set_len(len) + .unwrap(); + } + for prefix in ["", "1"] { + let value = format!("{prefix}{unit}"); + let display_suffix = if prefix.is_empty() { suffix } else { "" }; + let mut expected = String::new(); + for (file, count) in files.iter().zip([0, 1, 1, 2, large_blocks + 1]) { + writeln!(expected, "{count}{display_suffix}\t{file}").unwrap(); + } + for source in [ + "-B", + "--block-size=", + "DU_BLOCK_SIZE", + "BLOCK_SIZE", + "BLOCKSIZE", + ] { + let mut cmd = ts.ucmd(); + cmd.arg("-A").args(&files); + if source.starts_with('-') { + cmd.arg(format!("{source}{value}")); + } else { + cmd.env(source, &value); + } + cmd.succeeds().stdout_only(&expected); + } + } + } +} + +#[test] +fn test_du_block_size_display_overrides() { + let ts = TestScenario::new(util_name!()); + std::fs::File::create(ts.fixtures.plus("data")) + .unwrap() + .set_len(4097) + .unwrap(); + for (args, expected) in [ + (vec!["-k"], "5\tdata\n"), + (vec!["-m"], "1\tdata\n"), + (vec!["-b"], "4097\tdata\n"), + (vec!["-h"], "4.1K\tdata\n"), + (vec!["--block-size=1KiB"], "5\tdata\n"), + (vec!["--block-size=2KiB"], "3\tdata\n"), + (vec!["-BKiB", "-k"], "5\tdata\n"), + (vec!["-k", "-BKiB"], "5KiB\tdata\n"), + (vec!["-c", "-BKiB"], "5KiB\tdata\n5KiB\ttotal\n"), + ] { + ts.ucmd() + .env("DU_BLOCK_SIZE", "KiB") + .args(&["-A", "data"]) + .args(&args) + .succeeds() + .stdout_only(expected); + } + ts.ucmd() + .env("DU_BLOCK_SIZE", "invalid") + .env("BLOCK_SIZE", "KiB") + .args(&["-A", "data"]) + .succeeds() + .stdout_only("5\tdata\n"); +} + #[test] fn test_du_binary_block_size() { let ts = TestScenario::new(util_name!());