Skip to content

Commit 53e2999

Browse files
sylvestreHermes Agent
authored andcommitted
ls: use locale-aware collation for name sort
GNU ls uses strcoll() so in UTF-8 locales punctuation is reordered (e.g. '.' sorts before '#'), placing '.' and '..' first in `ls -a` output. uutils was always doing a byte-wise compare, diverging from GNU in non-C locales. Wire up uucore's ICU collator for Sort::Name, gated on should_use_locale_collation() so C/POSIX locales keep the existing zero-overhead byte compare path. Fixes #11831
1 parent 47d7c76 commit 53e2999

3 files changed

Lines changed: 35 additions & 1 deletion

File tree

src/uu/ls/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ uucore = { workspace = true, features = [
3535
"fs",
3636
"fsext",
3737
"fsxattr",
38+
"i18n-collator",
3839
"parser-size",
3940
"parser-glob",
4041
"quoting-style",

src/uu/ls/src/ls.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ impl UError for LsError {
119119
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
120120
let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?;
121121

122+
uucore::i18n::collator::init_locale_collation();
123+
122124
let config = Config::from(&matches)?;
123125

124126
let locs = matches
@@ -1489,7 +1491,18 @@ fn sort_entries(entries: &mut [PathData], config: &Config) {
14891491
});
14901492
}
14911493
// The default sort in GNU ls is case insensitive
1492-
Sort::Name => entries.sort_unstable_by(|a, b| a.display_name().cmp(b.display_name())),
1494+
Sort::Name => {
1495+
if uucore::i18n::collator::should_use_locale_collation() {
1496+
entries.sort_unstable_by(|a, b| {
1497+
uucore::i18n::collator::locale_cmp(
1498+
os_str_as_bytes_lossy(a.display_name()).as_ref(),
1499+
os_str_as_bytes_lossy(b.display_name()).as_ref(),
1500+
)
1501+
});
1502+
} else {
1503+
entries.sort_unstable_by(|a, b| a.display_name().cmp(b.display_name()));
1504+
}
1505+
}
14931506
Sort::Version => entries.sort_unstable_by(|a, b| {
14941507
version_cmp(
14951508
os_str_as_bytes_lossy(a.file_name()).as_ref(),

tests/by-util/test_ls.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,26 @@ fn test_ls_sort_name() {
21012101
.stdout_is(".a\n.b\na\nb\n");
21022102
}
21032103

2104+
// https://github.com/uutils/coreutils/issues/11831
2105+
// In a UTF-8 locale, GNU ls places "." and ".." before names starting with
2106+
// punctuation such as '#' due to locale-aware collation.
2107+
#[test]
2108+
fn test_ls_sort_dot_first_utf8_locale() {
2109+
let scene = TestScenario::new(util_name!());
2110+
let at = &scene.fixtures;
2111+
at.touch("#asdf");
2112+
at.touch("bar");
2113+
at.touch("foo");
2114+
2115+
scene
2116+
.ucmd()
2117+
.env("LANG", "en_US.UTF-8")
2118+
.env("LC_ALL", "en_US.UTF-8")
2119+
.arg("-1a")
2120+
.succeeds()
2121+
.stdout_is(".\n..\n#asdf\nbar\nfoo\n");
2122+
}
2123+
21042124
#[test]
21052125
fn test_ls_sort_width() {
21062126
let scene = TestScenario::new(util_name!());

0 commit comments

Comments
 (0)