Repository: uutils/coreutils (Rust)

Issue:
tr: support -c (complement) when translating
Add support translating complement of a character set.

What happens now:
```
$ echo -n ab | target/debug/coreutils tr -c a X
tr: error: -c is only supported with -d or -s
```

What I expected:
```
$ echo -n ab | tr -c a X
aX
```



Discussion:

Patch:
diff --git a/src/uu/tr/src/tr.rs b/src/uu/tr/src/tr.rs
index 3db48949fca..a44f2733cc9 100644
--- a/src/uu/tr/src/tr.rs
+++ b/src/uu/tr/src/tr.rs
@@ -123,10 +123,17 @@ impl SymbolTranslator for DeleteAndSqueezeOperation {
 
 struct TranslateOperation {
     translate_map: FnvHashMap<usize, char>,
+    complement: bool,
+    s2_last: char,
 }
 
 impl TranslateOperation {
-    fn new(set1: ExpandSet, set2: &mut ExpandSet, truncate: bool) -> TranslateOperation {
+    fn new(
+        set1: ExpandSet,
+        set2: &mut ExpandSet,
+        truncate: bool,
+        complement: bool,
+    ) -> TranslateOperation {
         let mut map = FnvHashMap::default();
         let mut s2_prev = '_';
         for i in set1 {
@@ -139,13 +146,25 @@ impl TranslateOperation {
                 map.insert(i as usize, s2_prev);
             }
         }
-        TranslateOperation { translate_map: map }
+        TranslateOperation {
+            translate_map: map,
+            complement,
+            s2_last: set2.last().unwrap_or(s2_prev),
+        }
     }
 }
 
 impl SymbolTranslator for TranslateOperation {
     fn translate(&self, c: char, _prev_c: char) -> Option<char> {
-        Some(*self.translate_map.get(&(c as usize)).unwrap_or(&c))
+        if self.complement {
+            Some(if self.translate_map.contains_key(&(c as usize)) {
+                c
+            } else {
+                self.s2_last
+            })
+        } else {
+            Some(*self.translate_map.get(&(c as usize)).unwrap_or(&c))
+        }
     }
 }
 
@@ -163,7 +182,7 @@ impl TranslateAndSqueezeOperation {
         complement: bool,
     ) -> TranslateAndSqueezeOperation {
         TranslateAndSqueezeOperation {
-            translate: TranslateOperation::new(set1, set2, truncate),
+            translate: TranslateOperation::new(set1, set2, truncate, complement),
             squeeze: SqueezeOperation::new(set2_, complement),
         }
     }
@@ -198,8 +217,8 @@ fn translate_input<T: SymbolTranslator>(
                 // Set `prev_c` to the post-translate character. This
                 // allows the squeeze operation to correctly function
                 // after the translate operation.
-                if res.is_some() {
-                    prev_c = res.unwrap();
+                if let Some(rc) = res {
+                    prev_c = rc;
                 }
                 res
             });
@@ -300,11 +319,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
         return 1;
     }
 
-    if complement_flag && !delete_flag && !squeeze_flag {
-        show_error!("-c is only supported with -d or -s");
-        return 1;
-    }
-
     let stdin = stdin();
     let mut locked_stdin = stdin.lock();
     let stdout = stdout();
@@ -339,8 +353,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
         }
     } else {
         let mut set2 = ExpandSet::new(sets[1].as_ref());
-        let op = TranslateOperation::new(set1, &mut set2, truncate_flag);
-        translate_input(&mut locked_stdin, &mut buffered_stdout, op)
+        let op = TranslateOperation::new(set1, &mut set2, truncate_flag, complement_flag);
+        translate_input(&mut locked_stdin, &mut buffered_stdout, op);
     }
 
     0


Test patch:
diff --git a/tests/by-util/test_tr.rs b/tests/by-util/test_tr.rs
index 8654af4f9d0..a035faae797 100644
--- a/tests/by-util/test_tr.rs
+++ b/tests/by-util/test_tr.rs
@@ -59,6 +59,53 @@ fn test_delete_complement_2() {
         .stdout_is("01234567890");
 }
 
+#[test]
+fn test_complement1() {
+    new_ucmd!()
+        .args(&["-c", "a", "X"])
+        .pipe_in("ab")
+        .run()
+        .stdout_is("aX");
+}
+
+#[test]
+fn test_complement2() {
+    new_ucmd!()
+        .args(&["-c", "0-9", "x"])
+        .pipe_in("Phone: 01234 567890")
+        .run()
+        .stdout_is("xxxxxxx01234x567890");
+}
+
+#[test]
+fn test_complement3() {
+    new_ucmd!()
+        .args(&["-c", "abcdefgh", "123"])
+        .pipe_in("the cat and the bat")
+        .run()
+        .stdout_is("3he3ca33a3d33he3ba3");
+}
+
+#[test]
+fn test_complement4() {
+    // $ echo -n '0x1y2z3' | tr -c '0-@' '*-~'
+    // 0~1~2~3
+    new_ucmd!()
+        .args(&["-c", "0-@", "*-~"])
+        .pipe_in("0x1y2z3")
+        .run()
+        .stdout_is("0~1~2~3");
+
+    // TODO: fix this
+    // $ echo '0x1y2z3' | tr -c '\0-@' '*-~'
+    // 0a1b2c3
+    // new_ucmd!()
+    //     .args(&["-c", "\\0-@", "*-~"])
+    //     .pipe_in("0x1y2z3")
+    //     .run()
+    //     .stdout_is("0a1b2c3");
+}
+
 #[test]
 fn test_squeeze() {
     new_ucmd!()