Repository: uutils/procps (rust)

Issue:
pgrep: `--terminal` doesn't support `?` as value
`pgrep` from procps-ng supports `?` as a value for `--terminal` to match processes that do not have a controlling terminal. Our implementation doesn't support this value.
```
$ pgrep --terminal=? konsole
1265
1599
1640
1681
1741

$ cargo run pgrep --terminal=? konsole
// no output
```

Pull request description:
pgrep: Add `TerminalType::Unknown` and implement `Display` for `TerminalType`
fix #136

Patch:
diff --git a/src/uu/pgrep/src/process.rs b/src/uu/pgrep/src/process.rs
index bd0145f..ad55ba2 100644
--- a/src/uu/pgrep/src/process.rs
+++ b/src/uu/pgrep/src/process.rs
@@ -17,13 +17,29 @@ pub enum TerminalType {
     Tty(u64),
     TtyS(u64),
     Pts(u64),
+    Unknown,
+}
+
+impl Display for TerminalType {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self {
+            Self::Tty(id) => write!(f, "/dev/pts/{}", id),
+            Self::TtyS(id) => write!(f, "/dev/tty{}", id),
+            Self::Pts(id) => write!(f, "/dev/ttyS{}", id),
+            Self::Unknown => write!(f, "?"),
+        }
+    }
 }
 
 impl TryFrom<String> for TerminalType {
     type Error = ();
 
     fn try_from(value: String) -> Result<Self, Self::Error> {
-        Self::try_from(PathBuf::from(value))
+        if value == "?" {
+            return Ok(TerminalType::Unknown);
+        }
+
+        Self::try_from(value.as_str())
     }
 }
 
@@ -291,6 +307,11 @@ impl ProcessInformation {
 
     /// This function will scan the `/proc/<pid>/fd` directory
     ///
+    /// If the process does not belong to any terminal,
+    /// the result will contain [TerminalType::Unknown].
+    ///
+    /// Otherwise [TerminalType::Unknown] does not appear in the result.
+    ///
     /// # Error
     ///
     /// If scanned pid had mismatched permission,
@@ -302,14 +323,22 @@ impl ProcessInformation {
 
         let path = PathBuf::from(format!("/proc/{}/fd", self.pid));
 
-        let result = Rc::new(
-            fs::read_dir(path)?
-                .flatten()
-                .filter(|it| it.path().is_symlink())
-                .flat_map(|it| fs::read_link(it.path()))
-                .flat_map(TerminalType::try_from)
-                .collect::<HashSet<_>>(),
-        );
+        let Ok(result) = fs::read_dir(path) else {
+            return Ok(Rc::new(HashSet::from_iter([TerminalType::Unknown])));
+        };
+
+        let mut result = result
+            .flatten()
+            .filter(|it| it.path().is_symlink())
+            .flat_map(|it| fs::read_link(it.path()))
+            .flat_map(TerminalType::try_from)
+            .collect::<HashSet<_>>();
+
+        if result.is_empty() {
+            result.insert(TerminalType::Unknown);
+        }
+
+        let result = Rc::new(result);
 
         self.cached_tty = Some(Rc::clone(&result));
 
@@ -376,7 +405,8 @@ mod tests {
     use std::str::FromStr;
 
     #[test]
-    fn test_tty_convention() {
+    fn test_tty_from() {
+        assert_eq!(TerminalType::try_from("?").unwrap(), TerminalType::Unknown);
         assert_eq!(
             TerminalType::try_from("/dev/tty1").unwrap(),
             TerminalType::Tty(1)
@@ -410,6 +440,14 @@ mod tests {
         assert!(TerminalType::try_from("TtyS10").is_err());
     }
 
+    #[test]
+    fn test_terminal_type_display() {
+        assert_eq!(TerminalType::Pts(10).to_string(), "/dev/pts/10");
+        assert_eq!(TerminalType::Tty(10).to_string(), "/dev/tty10");
+        assert_eq!(TerminalType::TtyS(10).to_string(), "/dev/ttyS10");
+        assert_eq!(TerminalType::Unknown.to_string(), "?");
+    }
+
     #[test]
     fn test_run_state_conversion() {
         assert_eq!(RunState::try_from("R").unwrap(), RunState::Running);


Test patch:
diff --git a/tests/by-util/test_pgrep.rs b/tests/by-util/test_pgrep.rs
index 721ffef..7144da6 100644
--- a/tests/by-util/test_pgrep.rs
+++ b/tests/by-util/test_pgrep.rs
@@ -253,6 +253,13 @@ fn test_terminal() {
     }
 }
 
+#[cfg(target_os = "linux")]
+#[test]
+fn test_unknown_terminal() {
+    new_ucmd!().arg("--terminal=?").succeeds();
+    new_ucmd!().arg("--terminal=?").arg("kthreadd").succeeds();
+}
+
 #[test]
 #[cfg(target_os = "linux")]
 fn test_terminal_invalid_terminal() {