Skip to content

Commit faf2e81

Browse files
committed
feat(ls,du,df): add thousands separator support with locale-aware formatting
Implements thousands separator formatting for ls, du, and df commands with support for leading quote format specifier. Adds comprehensive locale handling using ICU4X when available, with fallback implementations for common locales. Core changes: - Add get_thousands_separator() with ICU4X support and locale caching - Implement fallback logic for French (U+202F), English (comma), and POSIX - Add leading quote format support to df, du, and ls - Include locale-specific test helpers with cache bypass Fixes: - French locale (fr_*) now returns correct U+202F separator instead of '.' - English locale (en_*) fallback now returns ',' instead of '\0' - POSIX locale detection uses direct constant comparison for reliability - Default return value correctly returns '\0' for unavailable locales Dependencies: - Add icu_decimal and serial_test for locale testing - Update Cargo.lock with new dependencies Tests: - Add comprehensive locale and format tests for df, du, ls - Include tests for thousands separator edge cases and locale handling
1 parent efa1aa7 commit faf2e81

16 files changed

Lines changed: 980 additions & 59 deletions

File tree

.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

Cargo.lock

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ regex = "1.10.4"
362362
rstest = "0.26.0"
363363
rust-ini = "0.21.0"
364364
same-file = "1.0.6"
365+
serial_test = "3.1"
365366
self_cell = "1.0.4"
366367
# FIXME we use the exact version because the new 0.5.3 requires an MSRV of 1.88
367368
selinux = "=0.5.2"

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-size", "fs"] }
22+
uucore = { workspace = true, features = [
23+
"libc",
24+
"fsext",
25+
"parser-size",
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(),

0 commit comments

Comments
 (0)