diff --git a/src/uu/comm/Cargo.toml b/src/uu/comm/Cargo.toml index 22c30e245e7..8edfdbfc077 100644 --- a/src/uu/comm/Cargo.toml +++ b/src/uu/comm/Cargo.toml @@ -18,7 +18,7 @@ doctest = false [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["fs"] } +uucore = { workspace = true, features = ["fs", "i18n-collator"] } fluent = { workspace = true } [lints] diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index af6d2f46466..aa649d09004 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -14,6 +14,9 @@ use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::fs::paths_refer_to_same_file; +use uucore::i18n::collator::{ + AlternateHandling, CollatorOptions, locale_cmp, should_use_locale_collation, try_init_collator, +}; use uucore::line_ending::LineEnding; use uucore::translate; @@ -53,6 +56,20 @@ struct OrderChecker { file_num: FileNumber, check_order: bool, has_error: bool, + use_locale: bool, +} + +/// Compare two lines the way the input was ordered. +/// +/// `sort` orders with the locale collation, and so do `join` and `ls`. Reading +/// that order back with a byte comparison rejects input that is in order and +/// puts lines in the wrong column, so `comm` has to measure it the same way. +fn line_cmp(a: &[u8], b: &[u8], use_locale: bool) -> Ordering { + if use_locale { + locale_cmp(a, b) + } else { + a.cmp(b) + } } enum Input { @@ -97,12 +114,13 @@ impl LineReader { } impl OrderChecker { - fn new(file_num: FileNumber, check_order: bool) -> Self { + fn new(file_num: FileNumber, check_order: bool, use_locale: bool) -> Self { Self { last_line: Vec::new(), file_num, check_order, has_error: false, + use_locale, } } @@ -112,7 +130,7 @@ impl OrderChecker { return true; } - let is_ordered = *current_line >= *self.last_line; + let is_ordered = line_cmp(current_line, &self.last_line, self.use_locale) != Ordering::Less; if !is_ordered && !self.has_error { let _ = writeln!( stderr(), @@ -234,15 +252,16 @@ fn comm( || are_files_identical(Path::new(filename1), Path::new(filename2)) .unwrap_or(false))); - let mut checker1 = OrderChecker::new(FileNumber::One, check_order); - let mut checker2 = OrderChecker::new(FileNumber::Two, check_order); + let use_locale = should_use_locale_collation(); + let mut checker1 = OrderChecker::new(FileNumber::One, check_order, use_locale); + let mut checker2 = OrderChecker::new(FileNumber::Two, check_order, use_locale); let mut input_error = false; while na != 0 || nb != 0 { let ord = match (na, nb) { (0, _) => Ordering::Greater, (_, 0) => Ordering::Less, - (_, _) => ra.as_slice().cmp(rb.as_slice()), + (_, _) => line_cmp(ra, rb, use_locale), }; match ord { @@ -343,6 +362,11 @@ fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + + let mut collator_opts = CollatorOptions::default(); + collator_opts.alternate_handling = Some(AlternateHandling::Shifted); + let _ = try_init_collator(collator_opts); + let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); let filename1 = matches.get_one::(options::FILE_1).unwrap(); let filename2 = matches.get_one::(options::FILE_2).unwrap(); diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index b0f3e928f8c..f1671682a86 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -6,6 +6,8 @@ use uutests::new_ucmd; use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_locale_available; use uutests::util_name; #[test] @@ -732,6 +734,50 @@ fn test_read_error() { .stderr_contains("comm: /proc/self/mem: Input/output error"); } +#[test] +#[cfg(unix)] +fn test_locale_collation() { + // In a UTF-8 locale the collation puts `a1` before `a-b` and the byte order + // puts them the other way round. Reading a file `sort` produced with a byte + // comparison rejects it as unsorted and drops the line the two files share. + let locale = "en_US.UTF-8"; + if !is_locale_available(locale) { + return; + } + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("f1", "a1\na-b\n"); + scene.fixtures.write("f2", "a-b\n"); + + scene + .ucmd() + .env("LC_ALL", locale) + .args(&["-12", "f1", "f2"]) + .succeeds() + .stdout_only("a-b\n"); + + scene + .ucmd() + .env("LC_ALL", locale) + .args(&["-23", "f1", "f2"]) + .succeeds() + .stdout_only("a1\n"); +} + +#[test] +#[cfg(unix)] +fn test_c_locale_still_orders_by_bytes() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.write("f1", "a-b\na1\n"); + scene.fixtures.write("f2", "a-b\n"); + + scene + .ucmd() + .env("LC_ALL", "C") + .args(&["-12", "f1", "f2"]) + .succeeds() + .stdout_only("a-b\n"); +} + #[test] #[cfg(target_os = "linux")] fn test_comm_write_error_dev_full() {