From ec29d3342381bf36cd6ce3196d9aafd83f0959e2 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 18 Mar 2026 23:58:00 +0100 Subject: [PATCH] tr: fix complemented class truncation ordering --- src/uu/tr/src/operation.rs | 21 +++++++++++++++++++-- tests/by-util/test_tr.rs | 8 ++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 2f45bc9b990..e7b90cb9e12 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -292,11 +292,12 @@ impl Sequence { set2_uniques.sort_unstable(); set2_uniques.dedup(); + let set1_has_class = set1.iter().any(|x| matches!(x, Self::Class(_))); // If the complement flag is used in translate mode, only one unique // character may appear in set2. Validate this with the set of uniques // in set2 that we just generated. // Also, set2 must not overgrow set1, otherwise the mapping can't be 1:1. - if set1.iter().any(|x| matches!(x, Self::Class(_))) + if set1_has_class && translating && complement_flag && (set2_uniques.len() > 1 || set2_solved.len() > set1_len) @@ -306,7 +307,23 @@ impl Sequence { if set2_solved.len() < set1_solved.len() { if truncate_set1_flag { - set1_solved.truncate(set2_solved.len()); + if complement_flag && set1_has_class { + // GNU applies -t before complementing a character class. + // That means we must first truncate the expanded, non-complemented + // source set, then complement the truncated prefix to recover the + // final translation domain. Complementing first would incorrectly + // shrink the complemented domain to the prefix length. + let truncated_set1: Vec<_> = set1 + .iter() + .flat_map(Self::flatten) + .take(set2_solved.len()) + .collect(); + set1_solved = (0..=u8::MAX) + .filter(|x| !truncated_set1.contains(x)) + .collect(); + } else { + set1_solved.truncate(set2_solved.len()); + } } else if matches!( set2.last().copied(), Some(Self::Class(Class::Upper | Class::Lower)) diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 4a7c266b92a..84f62cd90b0 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -341,6 +341,14 @@ fn test_truncate_with_set1_shorter_than_set2() { .stdout_is("xycde"); } +#[test] +fn test_truncate_applies_before_complement_with_class() { + new_ucmd!() + .args(&["-ct", "[:digit:]", "X"]) + .pipe_in("A") + .succeeds() + .stdout_is("X"); +} #[test] fn missing_args_fails() { let (_, mut ucmd) = at_and_ucmd!();