Skip to content

Commit 48f4bc1

Browse files
committed
feat(ls,du,df): add thousands separator support with leading quote
Implement GNU-compatible thousands separator formatting when --block-size starts with a single quote (e.g., --block-size="'1"). Fixes #9084 Changes: - Add extract_thousands_separator_flag() to parse_size module - Add format_with_thousands_separator() with locale support (LC_NUMERIC) - Integrate into ls, du, and df utilities - Support environment variables (LS_BLOCK_SIZE, DU_BLOCK_SIZE, etc.) - Add 12 integration tests, all existing tests pass Examples: ls -l --block-size="'1" /bin # Shows: 1,024,000 du --block-size="'1K" /home # Shows: 1,234K df --block-size="'1" / # Shows: 494,384,795,648
1 parent e4f0216 commit 48f4bc1

File tree

12 files changed

+712
-58
lines changed

12 files changed

+712
-58
lines changed

.vscode/cspell.dictionaries/workspace.wordlist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ getcwd
363363
# * other
364364
weblate
365365
algs
366+
largefile
367+
verylargefile
366368

367369
# translation tests
368370
CLICOLOR

src/uu/df/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ path = "src/df.rs"
1919

2020
[dependencies]
2121
clap = { workspace = true }
22-
uucore = { workspace = true, features = ["libc", "fsext", "parser", "fs"] }
22+
uucore = { workspace = true, features = [
23+
"libc",
24+
"fsext",
25+
"parser",
26+
"fs",
27+
"format",
28+
] }
2329
unicode-width = { workspace = true }
2430
thiserror = { workspace = true }
2531
fluent = { workspace = true }

src/uu/df/src/blocks.rs

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use std::{env, fmt};
99

1010
use uucore::{
1111
display::Quotable,
12-
parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64},
12+
parser::parse_size::{
13+
ParseSizeError, extract_thousands_separator_flag, parse_size_non_zero_u64, parse_size_u64,
14+
},
1315
};
1416

1517
/// The first ten powers of 1024.
@@ -160,6 +162,13 @@ pub(crate) enum BlockSize {
160162
Bytes(u64),
161163
}
162164

165+
/// Configuration for block size display, including thousands separator flag.
166+
#[derive(Debug, PartialEq)]
167+
pub(crate) struct BlockSizeConfig {
168+
pub(crate) block_size: BlockSize,
169+
pub(crate) use_thousands_separator: bool,
170+
}
171+
163172
impl BlockSize {
164173
/// Returns the associated value
165174
pub(crate) fn as_u64(&self) -> u64 {
@@ -191,29 +200,47 @@ impl Default for BlockSize {
191200
}
192201
}
193202

194-
pub(crate) fn read_block_size(matches: &ArgMatches) -> Result<BlockSize, ParseSizeError> {
203+
pub(crate) fn read_block_size(matches: &ArgMatches) -> Result<BlockSizeConfig, ParseSizeError> {
195204
if matches.contains_id(OPT_BLOCKSIZE) {
196205
let s = matches.get_one::<String>(OPT_BLOCKSIZE).unwrap();
197-
let bytes = parse_size_u64(s)?;
206+
let (cleaned, use_thousands) = extract_thousands_separator_flag(s);
207+
let bytes = parse_size_u64(cleaned)?;
198208

199209
if bytes > 0 {
200-
Ok(BlockSize::Bytes(bytes))
210+
Ok(BlockSizeConfig {
211+
block_size: BlockSize::Bytes(bytes),
212+
use_thousands_separator: use_thousands,
213+
})
201214
} else {
202215
Err(ParseSizeError::ParseFailure(format!("{}", s.quote())))
203216
}
204217
} else if matches.get_flag(OPT_PORTABILITY) {
205-
Ok(BlockSize::default())
206-
} else if let Some(bytes) = block_size_from_env() {
207-
Ok(BlockSize::Bytes(bytes))
218+
Ok(BlockSizeConfig {
219+
block_size: BlockSize::default(),
220+
use_thousands_separator: false,
221+
})
222+
} else if let Some((bytes, use_thousands)) = block_size_from_env() {
223+
Ok(BlockSizeConfig {
224+
block_size: BlockSize::Bytes(bytes),
225+
use_thousands_separator: use_thousands,
226+
})
208227
} else {
209-
Ok(BlockSize::default())
228+
Ok(BlockSizeConfig {
229+
block_size: BlockSize::default(),
230+
use_thousands_separator: false,
231+
})
210232
}
211233
}
212234

213-
fn block_size_from_env() -> Option<u64> {
235+
fn block_size_from_env() -> Option<(u64, bool)> {
214236
for env_var in ["DF_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] {
215237
if let Ok(env_size) = env::var(env_var) {
216-
return parse_size_non_zero_u64(&env_size).ok();
238+
let (cleaned, use_thousands) = extract_thousands_separator_flag(&env_size);
239+
if let Ok(size) = parse_size_non_zero_u64(cleaned) {
240+
return Some((size, use_thousands));
241+
}
242+
// If env var is set but invalid, return None (don't check other env vars)
243+
return None;
217244
}
218245
}
219246

src/uu/df/src/df.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::io::stdout;
2525
use std::path::Path;
2626
use thiserror::Error;
2727

28-
use crate::blocks::{BlockSize, read_block_size};
28+
use crate::blocks::{BlockSize, BlockSizeConfig, read_block_size};
2929
use crate::columns::{Column, ColumnError};
3030
use crate::filesystem::Filesystem;
3131
use crate::filesystem::FsError;
@@ -62,7 +62,7 @@ struct Options {
6262
show_local_fs: bool,
6363
show_all_fs: bool,
6464
human_readable: Option<HumanReadable>,
65-
block_size: BlockSize,
65+
block_size_config: BlockSizeConfig,
6666
header_mode: HeaderMode,
6767

6868
/// Optional list of filesystem types to include in the output table.
@@ -92,7 +92,10 @@ impl Default for Options {
9292
Self {
9393
show_local_fs: Default::default(),
9494
show_all_fs: Default::default(),
95-
block_size: BlockSize::default(),
95+
block_size_config: BlockSizeConfig {
96+
block_size: BlockSize::default(),
97+
use_thousands_separator: false,
98+
},
9699
human_readable: Option::default(),
97100
header_mode: HeaderMode::default(),
98101
include: Option::default(),
@@ -160,7 +163,7 @@ impl Options {
160163
show_local_fs: matches.get_flag(OPT_LOCAL),
161164
show_all_fs: matches.get_flag(OPT_ALL),
162165
sync: matches.get_flag(OPT_SYNC),
163-
block_size: read_block_size(matches).map_err(|e| match e {
166+
block_size_config: read_block_size(matches).map_err(|e| match e {
164167
ParseSizeError::InvalidSuffix(s) => OptionsError::InvalidSuffix(s),
165168
ParseSizeError::SizeTooBig(_) => OptionsError::BlockSizeTooLarge(
166169
matches.get_one::<String>(OPT_BLOCKSIZE).unwrap().to_owned(),

src/uu/df/src/table.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::blocks::{SuffixType, to_magnitude_and_suffix};
1313
use crate::columns::{Alignment, Column};
1414
use crate::filesystem::Filesystem;
1515
use crate::{BlockSize, Options};
16+
use uucore::format::human::format_with_thousands_separator;
1617
use uucore::fsext::{FsUsage, MountInfo};
1718
use uucore::translate;
1819

@@ -266,8 +267,13 @@ impl<'a> RowFormatter<'a> {
266267
let s = if let Some(h) = self.options.human_readable {
267268
to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h), true)
268269
} else {
269-
let BlockSize::Bytes(d) = self.options.block_size;
270-
(size as f64 / d as f64).ceil().to_string()
270+
let BlockSize::Bytes(d) = self.options.block_size_config.block_size;
271+
let result = (size as f64 / d as f64).ceil() as u64;
272+
if self.options.block_size_config.use_thousands_separator {
273+
format_with_thousands_separator(result)
274+
} else {
275+
result.to_string()
276+
}
271277
};
272278
Cell::from_ascii_string(s)
273279
}
@@ -373,13 +379,13 @@ impl Header {
373379
HeaderMode::PosixPortability => {
374380
format!(
375381
"{}{}",
376-
options.block_size.as_u64(),
382+
options.block_size_config.block_size.as_u64(),
377383
translate!("df-blocks-suffix")
378384
)
379385
}
380386
_ => format!(
381387
"{}{}",
382-
options.block_size.to_header(),
388+
options.block_size_config.block_size.to_header(),
383389
translate!("df-blocks-suffix")
384390
),
385391
},
@@ -642,7 +648,10 @@ mod tests {
642648
fn test_header_with_block_size_1024() {
643649
init();
644650
let options = Options {
645-
block_size: BlockSize::Bytes(3 * 1024),
651+
block_size_config: crate::blocks::BlockSizeConfig {
652+
block_size: BlockSize::Bytes(3 * 1024),
653+
use_thousands_separator: false,
654+
},
646655
..Default::default()
647656
};
648657
assert_eq!(
@@ -722,7 +731,10 @@ mod tests {
722731
fn test_row_formatter() {
723732
init();
724733
let options = Options {
725-
block_size: BlockSize::Bytes(1),
734+
block_size_config: crate::blocks::BlockSizeConfig {
735+
block_size: BlockSize::Bytes(1),
736+
use_thousands_separator: false,
737+
},
726738
..Default::default()
727739
};
728740
let row = Row {
@@ -748,7 +760,10 @@ mod tests {
748760
init();
749761
let options = Options {
750762
columns: COLUMNS_WITH_FS_TYPE.to_vec(),
751-
block_size: BlockSize::Bytes(1),
763+
block_size_config: crate::blocks::BlockSizeConfig {
764+
block_size: BlockSize::Bytes(1),
765+
use_thousands_separator: false,
766+
},
752767
..Default::default()
753768
};
754769
let row = Row {
@@ -775,7 +790,10 @@ mod tests {
775790
init();
776791
let options = Options {
777792
columns: COLUMNS_WITH_INODES.to_vec(),
778-
block_size: BlockSize::Bytes(1),
793+
block_size_config: crate::blocks::BlockSizeConfig {
794+
block_size: BlockSize::Bytes(1),
795+
use_thousands_separator: false,
796+
},
779797
..Default::default()
780798
};
781799
let row = Row {
@@ -801,7 +819,10 @@ mod tests {
801819
init();
802820
let options = Options {
803821
columns: vec![Column::Size, Column::Itotal],
804-
block_size: BlockSize::Bytes(100),
822+
block_size_config: crate::blocks::BlockSizeConfig {
823+
block_size: BlockSize::Bytes(100),
824+
use_thousands_separator: false,
825+
},
805826
..Default::default()
806827
};
807828
let row = Row {
@@ -903,7 +924,10 @@ mod tests {
903924
init();
904925
fn get_formatted_values(bytes: u64, bytes_used: u64, bytes_avail: u64) -> Vec<Cell> {
905926
let options = Options {
906-
block_size: BlockSize::Bytes(1000),
927+
block_size_config: crate::blocks::BlockSizeConfig {
928+
block_size: BlockSize::Bytes(1000),
929+
use_thousands_separator: false,
930+
},
907931
columns: vec![Column::Size, Column::Used, Column::Avail],
908932
..Default::default()
909933
};

src/uu/du/src/du.rs

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ use uucore::line_ending::LineEnding;
3030
use uucore::safe_traversal::DirFd;
3131
use uucore::translate;
3232

33+
use uucore::format::human::format_with_thousands_separator;
3334
use uucore::parser::parse_glob;
34-
use uucore::parser::parse_size::{ParseSizeError, parse_size_non_zero_u64, parse_size_u64};
35+
use uucore::parser::parse_size::{
36+
ParseSizeError, extract_thousands_separator_flag, parse_size_non_zero_u64, parse_size_u64,
37+
};
3538
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
3639
use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
3740
use uucore::{format_usage, show, show_error, show_warning};
@@ -91,6 +94,7 @@ struct StatPrinter {
9194
threshold: Option<Threshold>,
9295
apparent_size: bool,
9396
size_format: SizeFormat,
97+
use_thousands_separator: bool,
9498
time: Option<MetadataTimeField>,
9599
time_format: String,
96100
line_ending: LineEnding,
@@ -271,26 +275,33 @@ fn get_file_info(path: &Path, _metadata: &Metadata) -> Option<FileInfo> {
271275
result
272276
}
273277

274-
fn block_size_from_env() -> Option<u64> {
278+
fn block_size_from_env() -> Option<(u64, bool)> {
275279
for env_var in ["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] {
276280
if let Ok(env_size) = env::var(env_var) {
277-
return parse_size_non_zero_u64(&env_size).ok();
281+
let (cleaned, use_thousands) = extract_thousands_separator_flag(&env_size);
282+
if let Ok(size) = parse_size_non_zero_u64(cleaned) {
283+
return Some((size, use_thousands));
284+
}
285+
// If env var is set but invalid, return None (don't check other env vars)
286+
return None;
278287
}
279288
}
280289

281290
None
282291
}
283292

284-
fn read_block_size(s: Option<&str>) -> UResult<u64> {
293+
fn read_block_size(s: Option<&str>) -> UResult<(u64, bool)> {
285294
if let Some(s) = s {
286-
parse_size_u64(s)
295+
let (cleaned, use_thousands) = extract_thousands_separator_flag(s);
296+
parse_size_u64(cleaned)
297+
.map(|size| (size, use_thousands))
287298
.map_err(|e| USimpleError::new(1, format_error_message(&e, s, options::BLOCK_SIZE)))
288-
} else if let Some(bytes) = block_size_from_env() {
289-
Ok(bytes)
299+
} else if let Some((bytes, use_thousands)) = block_size_from_env() {
300+
Ok((bytes, use_thousands))
290301
} else if env::var("POSIXLY_CORRECT").is_ok() {
291-
Ok(512)
302+
Ok((512, false))
292303
} else {
293-
Ok(1024)
304+
Ok((1024, false))
294305
}
295306
}
296307

@@ -874,7 +885,12 @@ impl StatPrinter {
874885
// we ignore block size (-B) with --inodes
875886
size.to_string()
876887
} else {
877-
size.div_ceil(block_size).to_string()
888+
let result = size.div_ceil(block_size);
889+
if self.use_thousands_separator {
890+
format_with_thousands_separator(result)
891+
} else {
892+
result.to_string()
893+
}
878894
}
879895
}
880896
}
@@ -1005,24 +1021,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
10051021
.map_or(MetadataTimeField::Modification, |s| s.as_str().into())
10061022
});
10071023

1008-
let size_format = if matches.get_flag(options::HUMAN_READABLE) {
1009-
SizeFormat::HumanBinary
1024+
let (size_format, use_thousands_separator) = if matches.get_flag(options::HUMAN_READABLE) {
1025+
(SizeFormat::HumanBinary, false)
10101026
} else if matches.get_flag(options::SI) {
1011-
SizeFormat::HumanDecimal
1027+
(SizeFormat::HumanDecimal, false)
10121028
} else if matches.get_flag(options::BYTES) {
1013-
SizeFormat::BlockSize(1)
1029+
(SizeFormat::BlockSize(1), false)
10141030
} else if matches.get_flag(options::BLOCK_SIZE_1K) {
1015-
SizeFormat::BlockSize(1024)
1031+
(SizeFormat::BlockSize(1024), false)
10161032
} else if matches.get_flag(options::BLOCK_SIZE_1M) {
1017-
SizeFormat::BlockSize(1024 * 1024)
1033+
(SizeFormat::BlockSize(1024 * 1024), false)
10181034
} else {
10191035
let block_size_str = matches.get_one::<String>(options::BLOCK_SIZE);
1020-
let block_size = read_block_size(block_size_str.map(AsRef::as_ref))?;
1036+
let (block_size, use_thousands) = read_block_size(block_size_str.map(AsRef::as_ref))?;
10211037
if block_size == 0 {
10221038
return Err(std::io::Error::other(translate!("du-error-invalid-block-size-argument", "option" => options::BLOCK_SIZE, "value" => block_size_str.map_or("???BUG", |v| v).quote()))
10231039
.into());
10241040
}
1025-
SizeFormat::BlockSize(block_size)
1041+
(SizeFormat::BlockSize(block_size), use_thousands)
10261042
};
10271043

10281044
let traversal_options = TraversalOptions {
@@ -1051,6 +1067,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
10511067
let stat_printer = StatPrinter {
10521068
max_depth,
10531069
size_format,
1070+
use_thousands_separator,
10541071
summarize,
10551072
total: matches.get_flag(options::TOTAL),
10561073
inodes: matches.get_flag(options::INODES),
@@ -1508,7 +1525,11 @@ mod test_du {
15081525
fn test_read_block_size() {
15091526
let test_data = [Some("1024".to_string()), Some("K".to_string()), None];
15101527
for it in &test_data {
1511-
assert!(matches!(read_block_size(it.as_deref()), Ok(1024)));
1528+
assert!(matches!(read_block_size(it.as_deref()), Ok((1024, false))));
15121529
}
1530+
1531+
// Test with thousands separator flag
1532+
assert!(matches!(read_block_size(Some("'1024")), Ok((1024, true))));
1533+
assert!(matches!(read_block_size(Some("'1K")), Ok((1024, true))));
15131534
}
15141535
}

0 commit comments

Comments
 (0)