diff --git a/src/uu/tr/src/operation.rs b/src/uu/tr/src/operation.rs index 22fd890471..c6bdbfe227 100644 --- a/src/uu/tr/src/operation.rs +++ b/src/uu/tr/src/operation.rs @@ -386,8 +386,9 @@ impl Sequence { .collect(); // For every upper/lower in set2, there must be an upper/lower in set1 at the same position. The position is calculated by expanding everything before the upper/lower in both sets + // With -c, set1 positions are those of the complement, so there is nothing to match. for (set2_pos, set2_item) in set2.iter().enumerate() { - if matches!(set2_item, Self::Class(_)) { + if !complement_flag && matches!(set2_item, Self::Class(_)) { let set2_part_solved_len = Self::expanded_len_of(&set2[..set2_pos]); let mut class_matches = false; @@ -416,20 +417,6 @@ impl Sequence { let set2_uniques = Self::unique_chars(&set2_runs); 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_has_class - && translating - && complement_flag - && (set2_uniques.len() > 1 || set2_len > set1_len) - { - return Err(SequenceError::whole_set( - BadSequence::ComplementMoreThanOneUniqueInSet2, - 2, - )); - } if set2_len < set1_len { if truncate_set1_flag { @@ -460,6 +447,21 @@ impl Sequence { } } + // 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_has_class + && translating + && complement_flag + && (set2_uniques.len() > 1 || set2_len > set1_len) + { + return Err(SequenceError::whole_set( + BadSequence::ComplementMoreThanOneUniqueInSet2, + 2, + )); + } + // Line the two sets up position by position, one run at a time. A run // of one character in set1 maps that character to every character it // lines up with in set2, and the last mapping wins, so the pair at the diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs index 2fd477dbf5..2bde3e176a 100644 --- a/tests/by-util/test_tr.rs +++ b/tests/by-util/test_tr.rs @@ -1047,6 +1047,64 @@ fn tr_translate_backslash_at_end() { .stderr_is("tr: warning: an unescaped backslash at end of string is not portable\n"); } +#[test] +fn test_complement_set1_longer_than_set2_ending_in_class() { + let msg = "tr: when translating with string1 longer than string2,\nthe latter string must not end with a character class\n"; + for (flags, set1, set2) in [ + ("-c", "[:upper:]", "[:lower:]"), + ("-c", "[:lower:]", "[:upper:]"), + ("-c", "[:upper:]", "x[:lower:]"), + ("-c", "[:digit:]", "[:upper:]"), + ("-c", "[:upper:]", "[:upper:]"), + ("-c", "a-z", "[:upper:]"), + ("-c", "a", "[:upper:]"), + ("-cs", "[:upper:]", "[:lower:]"), + ] { + new_ucmd!() + .args(&[flags, set1, set2]) + .pipe_in("AbC1z\n") + .fails_with_code(1) + .stderr_only(msg); + } +} + +#[test] +fn test_complement_class_in_set2_without_class_in_set1() { + // The complement of everything is empty, so nothing is translated. + new_ucmd!() + .args(&["-c", "\\000-\\377", "[:upper:]"]) + .pipe_in("AbC1z\n") + .succeeds() + .stdout_only("AbC1z\n"); + + new_ucmd!() + .args(&["-c", "\\000-\\375", "[:upper:]"]) + .pipe_in(b"\xfe\xffAa\n".as_slice()) + .succeeds() + .stdout_only("ABAa\n"); + + new_ucmd!() + .args(&["-c", "a-z", "[:upper:]x"]) + .pipe_in("AbC1z\n") + .succeeds() + .stdout_only("xbxxzK"); +} + +#[test] +fn test_complement_class_in_set2_unique_chars_error() { + let msg = "tr: when translating with complemented character classes,\nstring2 must map all characters in the domain to one\n"; + for (flags, set1, set2) in [ + ("-c", "[:upper:]", "[:lower:]x"), + ("-ct", "[:upper:]", "[:lower:]"), + ] { + new_ucmd!() + .args(&[flags, set1, set2]) + .pipe_in("AbC1z\n") + .fails_with_code(1) + .stderr_only(msg); + } +} + #[test] fn tr_ross_delete_no_squeeze() { // # From Ross