Skip to content

Commit ec29d33

Browse files
committed
tr: fix complemented class truncation ordering
1 parent ef8e45c commit ec29d33

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

src/uu/tr/src/operation.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,12 @@ impl Sequence {
292292
set2_uniques.sort_unstable();
293293
set2_uniques.dedup();
294294

295+
let set1_has_class = set1.iter().any(|x| matches!(x, Self::Class(_)));
295296
// If the complement flag is used in translate mode, only one unique
296297
// character may appear in set2. Validate this with the set of uniques
297298
// in set2 that we just generated.
298299
// Also, set2 must not overgrow set1, otherwise the mapping can't be 1:1.
299-
if set1.iter().any(|x| matches!(x, Self::Class(_)))
300+
if set1_has_class
300301
&& translating
301302
&& complement_flag
302303
&& (set2_uniques.len() > 1 || set2_solved.len() > set1_len)
@@ -306,7 +307,23 @@ impl Sequence {
306307

307308
if set2_solved.len() < set1_solved.len() {
308309
if truncate_set1_flag {
309-
set1_solved.truncate(set2_solved.len());
310+
if complement_flag && set1_has_class {
311+
// GNU applies -t before complementing a character class.
312+
// That means we must first truncate the expanded, non-complemented
313+
// source set, then complement the truncated prefix to recover the
314+
// final translation domain. Complementing first would incorrectly
315+
// shrink the complemented domain to the prefix length.
316+
let truncated_set1: Vec<_> = set1
317+
.iter()
318+
.flat_map(Self::flatten)
319+
.take(set2_solved.len())
320+
.collect();
321+
set1_solved = (0..=u8::MAX)
322+
.filter(|x| !truncated_set1.contains(x))
323+
.collect();
324+
} else {
325+
set1_solved.truncate(set2_solved.len());
326+
}
310327
} else if matches!(
311328
set2.last().copied(),
312329
Some(Self::Class(Class::Upper | Class::Lower))

tests/by-util/test_tr.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ fn test_truncate_with_set1_shorter_than_set2() {
341341
.stdout_is("xycde");
342342
}
343343

344+
#[test]
345+
fn test_truncate_applies_before_complement_with_class() {
346+
new_ucmd!()
347+
.args(&["-ct", "[:digit:]", "X"])
348+
.pipe_in("A")
349+
.succeeds()
350+
.stdout_is("X");
351+
}
344352
#[test]
345353
fn missing_args_fails() {
346354
let (_, mut ucmd) = at_and_ucmd!();

0 commit comments

Comments
 (0)