Repository: uutils/procps
Pull request #290 (2024-12-26 08:43:05)

pkill: implement
#27 

sylvestre: Great start. Could you please add tests? 
Krysztal112233: And also, more unit tests would be better. If it cannot run on Windows, you can disable it for Windows simply. 

eg.
```rust
#[cfg(target_os = "linux")]
#[test]
fn test_something() {
...
}
```
Krysztal112233: Could you please fix the CI failures?
Krysztal112233: I think this file will help you to be compatible with macOS and other *NIX implementations.

https://github.com/uutils/coreutils/blob/main/src/uucore/src/lib/features/signals.rs

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 5d555ad..e03ba19 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -136,18 +136,18 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.23"
+version = "4.5.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
+checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
 dependencies = [
  "clap_builder",
 ]
 
 [[package]]
 name = "clap_builder"
-version = "4.5.23"
+version = "4.5.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
+checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
 dependencies = [
  "anstream",
  "anstyle",
@@ -158,9 +158,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.40"
+version = "4.5.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac2e663e3e3bed2d32d065a8404024dad306e699a04263ec59919529f803aee9"
+checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0"
 dependencies = [
  "clap",
 ]
@@ -173,9 +173,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
 [[package]]
 name = "clap_mangen"
-version = "0.2.24"
+version = "0.2.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf"
+checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a"
 dependencies = [
  "clap",
  "roff",
@@ -615,6 +615,7 @@ dependencies = [
  "uu_pgrep",
  "uu_pidof",
  "uu_pidwait",
+ "uu_pkill",
  "uu_pmap",
  "uu_ps",
  "uu_pwdx",
@@ -929,11 +930,11 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.9"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
 dependencies = [
- "thiserror-impl 2.0.9",
+ "thiserror-impl 2.0.11",
 ]
 
 [[package]]
@@ -949,9 +950,9 @@ dependencies = [
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.9"
+version = "2.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1023,7 +1024,7 @@ dependencies = [
  "clap",
  "sysinfo",
  "uucore",
- "windows 0.58.0",
+ "windows 0.59.0",
 ]
 
 [[package]]
@@ -1056,6 +1057,18 @@ dependencies = [
  "uucore",
 ]
 
+[[package]]
+name = "uu_pkill"
+version = "0.0.1"
+dependencies = [
+ "clap",
+ "nix",
+ "regex",
+ "uu_pgrep",
+ "uucore",
+ "walkdir",
+]
+
 [[package]]
 name = "uu_pmap"
 version = "0.0.1"
@@ -1103,7 +1116,7 @@ dependencies = [
  "nix",
  "prettytable-rs",
  "sysinfo",
- "thiserror 2.0.9",
+ "thiserror 2.0.11",
  "uu_pgrep",
  "uucore",
 ]
@@ -1297,12 +1310,12 @@ dependencies = [
 
 [[package]]
 name = "windows"
-version = "0.58.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
+checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
 dependencies = [
- "windows-core 0.58.0",
- "windows-targets 0.52.6",
+ "windows-core 0.59.0",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
@@ -1328,15 +1341,15 @@ dependencies = [
 
 [[package]]
 name = "windows-core"
-version = "0.58.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
+checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
 dependencies = [
- "windows-implement 0.58.0",
- "windows-interface 0.58.0",
- "windows-result 0.2.0",
+ "windows-implement 0.59.0",
+ "windows-interface 0.59.0",
+ "windows-result 0.3.0",
  "windows-strings",
- "windows-targets 0.52.6",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
@@ -1352,9 +1365,9 @@ dependencies = [
 
 [[package]]
 name = "windows-implement"
-version = "0.58.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
+checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1374,9 +1387,9 @@ dependencies = [
 
 [[package]]
 name = "windows-interface"
-version = "0.58.0"
+version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
+checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1394,21 +1407,20 @@ dependencies = [
 
 [[package]]
 name = "windows-result"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
+checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
 name = "windows-strings"
-version = "0.1.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
+checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491"
 dependencies = [
- "windows-result 0.2.0",
- "windows-targets 0.52.6",
+ "windows-targets 0.53.0",
 ]
 
 [[package]]
@@ -1462,13 +1474,29 @@ dependencies = [
  "windows_aarch64_gnullvm 0.52.6",
  "windows_aarch64_msvc 0.52.6",
  "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
  "windows_i686_msvc 0.52.6",
  "windows_x86_64_gnu 0.52.6",
  "windows_x86_64_gnullvm 0.52.6",
  "windows_x86_64_msvc 0.52.6",
 ]
 
+[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
 [[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.48.5"
@@ -1481,6 +1509,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.5"
@@ -1493,6 +1527,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.48.5"
@@ -1505,12 +1545,24 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
 [[package]]
 name = "windows_i686_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.48.5"
@@ -1523,6 +1575,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.5"
@@ -1535,6 +1593,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
 [[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.5"
@@ -1547,6 +1611,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.5"
@@ -1559,6 +1629,12 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
 [[package]]
 name = "xattr"
 version = "1.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index e48a3d1..e4d9217 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,7 @@ feat_common_core = [
     "pwdx",
     "slabtop",
     "snice",
+    "pkill",
     "top",
     "w",
     "watch",
@@ -59,7 +60,7 @@ textwrap = { version = "0.16.1", features = ["terminal_size"] }
 thiserror = "2.0.4"
 uucore = "0.0.28"
 walkdir = "2.5.0"
-windows = { version = "0.58.0" }
+windows = { version = "0.59.0" }
 xattr = "1.3.1"
 
 [dependencies]
@@ -82,6 +83,7 @@ ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps"
 pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" }
 slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" }
 snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" }
+pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
 top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" }
 w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" }
 watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" }
diff --git a/src/uu/pkill/Cargo.toml b/src/uu/pkill/Cargo.toml
new file mode 100644
index 0000000..a7c042c
--- /dev/null
+++ b/src/uu/pkill/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "uu_pkill"
+version = "0.0.1"
+edition = "2021"
+authors = ["uutils developers"]
+license = "MIT"
+description = "pgrep ~ (uutils) Kills processes based on name and other attributes."
+
+homepage = "https://github.com/uutils/procps"
+repository = "https://github.com/uutils/procps/tree/main/src/uu/pkill"
+keywords = ["acl", "uutils", "cross-platform", "cli", "utility"]
+categories = ["command-line-utilities"]
+
+
+[dependencies]
+uucore = { workspace = true }
+clap = { workspace = true }
+walkdir = { workspace = true }
+regex = { workspace = true }
+nix = { workspace = true, features = ["signal"] }
+
+uu_pgrep = { path = "../pgrep" }
+
+[lib]
+path = "src/pkill.rs"
+
+[[bin]]
+name = "pkill"
+path = "src/main.rs"
diff --git a/src/uu/pkill/pkill.md b/src/uu/pkill/pkill.md
new file mode 100644
index 0000000..c835328
--- /dev/null
+++ b/src/uu/pkill/pkill.md
@@ -0,0 +1,7 @@
+# pgrep
+
+```
+pkill [options] <pattern>
+```
+
+Kills processes based on name and other attributes.
diff --git a/src/uu/pkill/src/main.rs b/src/uu/pkill/src/main.rs
new file mode 100644
index 0000000..7bba173
--- /dev/null
+++ b/src/uu/pkill/src/main.rs
@@ -0,0 +1,1 @@
+uucore::bin!(uu_pkill);
diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs
new file mode 100644
index 0000000..9d3fb78
--- /dev/null
+++ b/src/uu/pkill/src/pkill.rs
@@ -0,0 +1,398 @@
+// This file is part of the uutils procps package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+// Pid utils
+use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command};
+#[cfg(unix)]
+use nix::{
+    sys::signal::{self, Signal},
+    unistd::Pid,
+};
+use regex::Regex;
+#[cfg(unix)]
+use std::io::Error;
+use std::{collections::HashSet, sync::OnceLock};
+use uu_pgrep::process::{walk_process, ProcessInformation, Teletype};
+#[cfg(unix)]
+use uucore::{
+    display::Quotable,
+    error::FromIo,
+    show,
+    signals::{signal_by_name_or_value, signal_name_by_value},
+};
+use uucore::{
+    error::{UResult, USimpleError},
+    format_usage, help_about, help_usage,
+};
+
+const ABOUT: &str = help_about!("pkill.md");
+const USAGE: &str = help_usage!("pkill.md");
+
+static REGEX: OnceLock<Regex> = OnceLock::new();
+
+struct Settings {
+    exact: bool,
+    full: bool,
+    ignore_case: bool,
+    newest: bool,
+    oldest: bool,
+    older: Option<u64>,
+    parent: Option<Vec<u64>>,
+    runstates: Option<String>,
+    terminal: Option<HashSet<Teletype>>,
+}
+
+#[uucore::main]
+pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    #[cfg(unix)]
+    let mut args = args.collect_ignore();
+    #[cfg(target_os = "windows")]
+    let args = args.collect_ignore();
+    #[cfg(unix)]
+    let obs_signal = handle_obsolete(&mut args);
+
+    let matches = uu_app().try_get_matches_from(&args)?;
+
+    let pattern = try_get_pattern_from(&matches)?;
+    REGEX
+        .set(Regex::new(&pattern).map_err(|e| USimpleError::new(2, e.to_string()))?)
+        .unwrap();
+
+    let settings = Settings {
+        exact: matches.get_flag("exact"),
+        full: matches.get_flag("full"),
+        ignore_case: matches.get_flag("ignore-case"),
+        newest: matches.get_flag("newest"),
+        oldest: matches.get_flag("oldest"),
+        parent: matches
+            .get_many::<u64>("parent")
+            .map(|parents| parents.copied().collect()),
+        runstates: matches.get_one::<String>("runstates").cloned(),
+        older: matches.get_one::<u64>("older").copied(),
+        terminal: matches.get_many::<String>("terminal").map(|ttys| {
+            ttys.cloned()
+                .flat_map(Teletype::try_from)
+                .collect::<HashSet<_>>()
+        }),
+    };
+
+    if (!settings.newest
+        && !settings.oldest
+        && settings.runstates.is_none()
+        && settings.older.is_none()
+        && settings.parent.is_none()
+        && settings.terminal.is_none())
+        && pattern.is_empty()
+    {
+        return Err(USimpleError::new(
+            2,
+            "no matching criteria specified\nTry `pkill --help' for more information.",
+        ));
+    }
+
+    // Parse signal
+    #[cfg(unix)]
+    let sig_num = if let Some(signal) = obs_signal {
+        signal
+    } else if let Some(signal) = matches.get_one::<String>("signal") {
+        parse_signal_value(signal)?
+    } else {
+        15_usize //SIGTERM
+    };
+
+    #[cfg(unix)]
+    let sig_name = signal_name_by_value(sig_num);
+    // Signal does not support converting from EXIT
+    // Instead, nix::signal::kill expects Option::None to properly handle EXIT
+    #[cfg(unix)]
+    let sig: Option<Signal> = if sig_name.is_some_and(|name| name == "EXIT") {
+        None
+    } else {
+        let sig = (sig_num as i32)
+            .try_into()
+            .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
+        Some(sig)
+    };
+
+    // Collect pids
+    let pids = {
+        let mut pids = collect_matched_pids(&settings);
+        #[cfg(unix)]
+        if matches.get_flag("require-handler") {
+            pids.retain(|pid| {
+                let mask =
+                    u32::from_str_radix(pid.clone().status().get("SigCgt").unwrap(), 16).unwrap();
+                mask & (1 << sig_num) != 0
+            });
+        }
+        if pids.is_empty() {
+            uucore::error::set_exit_code(1);
+            pids
+        } else {
+            process_flag_o_n(&settings, &mut pids)
+        }
+    };
+
+    // Send signal
+    // TODO: Implement -q
+    #[cfg(unix)]
+    let echo = matches.get_flag("echo");
+    #[cfg(unix)]
+    kill(&pids, sig, echo);
+
+    if matches.get_flag("count") {
+        println!("{}", pids.len());
+    }
+
+    Ok(())
+}
+
+/// Try to get the pattern from the command line arguments. Returns an empty string if no pattern
+/// is specified.
+fn try_get_pattern_from(matches: &ArgMatches) -> UResult<String> {
+    let pattern = match matches.get_many::<String>("pattern") {
+        Some(patterns) if patterns.len() > 1 => {
+            return Err(USimpleError::new(
+                2,
+                "only one pattern can be provided\nTry `pgrep --help' for more information.",
+            ))
+        }
+        Some(mut patterns) => patterns.next().unwrap(),
+        None => return Ok(String::new()),
+    };
+
+    let pattern = if matches.get_flag("ignore-case") {
+        &pattern.to_lowercase()
+    } else {
+        pattern
+    };
+
+    let pattern = if matches.get_flag("exact") {
+        &format!("^{}$", pattern)
+    } else {
+        pattern
+    };
+
+    Ok(pattern.to_string())
+}
+
+/// Collect pids with filter construct from command line arguments
+fn collect_matched_pids(settings: &Settings) -> Vec<ProcessInformation> {
+    // Filtration general parameters
+    let filtered: Vec<ProcessInformation> = {
+        let mut tmp_vec = Vec::new();
+
+        for mut pid in walk_process().collect::<Vec<_>>() {
+            let run_state_matched = match (&settings.runstates, pid.run_state()) {
+                (Some(arg_run_states), Ok(pid_state)) => {
+                    arg_run_states.contains(&pid_state.to_string())
+                }
+                (_, Err(_)) => false,
+                _ => true,
+            };
+
+            let binding = pid.status();
+            let name = binding.get("Name").unwrap();
+            let name = if settings.ignore_case {
+                name.to_lowercase()
+            } else {
+                name.into()
+            };
+            let pattern_matched = {
+                let want = if settings.exact {
+                    // Equals `Name` in /proc/<pid>/status
+                    // The `unwrap` operation must succeed
+                    // because the REGEX has been verified as correct in `uumain`.
+                    &name
+                } else if settings.full {
+                    // Equals `cmdline` in /proc/<pid>/cmdline
+                    &pid.cmdline
+                } else {
+                    // From manpage:
+                    // The process name used for matching is limited to the 15 characters present in the output of /proc/pid/stat.
+                    &pid.proc_stat()[..15]
+                };
+
+                REGEX.get().unwrap().is_match(want)
+            };
+
+            let tty_matched = match &settings.terminal {
+                Some(ttys) => ttys.contains(&pid.tty()),
+                None => true,
+            };
+
+            let arg_older = settings.older.unwrap_or(0);
+            let older_matched = pid.start_time().unwrap() >= arg_older;
+
+            // the PPID is the fourth field in /proc/<PID>/stat
+            // (https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10)
+            let stat = pid.stat();
+            let ppid = stat.get(3);
+            let parent_matched = match (&settings.parent, ppid) {
+                (Some(parents), Some(ppid)) => parents.contains(&ppid.parse::<u64>().unwrap()),
+                _ => true,
+            };
+
+            if run_state_matched
+                && pattern_matched
+                && tty_matched
+                && older_matched
+                && parent_matched
+            {
+                tmp_vec.push(pid);
+            }
+        }
+        tmp_vec
+    };
+
+    filtered
+}
+
+/// Sorting pids for flag `-o` and `-n`.
+///
+/// This function can also be used as a filter to filter out process information.
+fn process_flag_o_n(
+    settings: &Settings,
+    pids: &mut [ProcessInformation],
+) -> Vec<ProcessInformation> {
+    if settings.oldest || settings.newest {
+        pids.sort_by(|a, b| {
+            b.clone()
+                .start_time()
+                .unwrap()
+                .cmp(&a.clone().start_time().unwrap())
+        });
+
+        let start_time = if settings.newest {
+            pids.first().cloned().unwrap().start_time().unwrap()
+        } else {
+            pids.last().cloned().unwrap().start_time().unwrap()
+        };
+
+        // There might be some process start at same time, so need to be filtered.
+        let mut filtered = pids
+            .iter()
+            .filter(|it| (*it).clone().start_time().unwrap() == start_time)
+            .collect::<Vec<_>>();
+
+        if settings.newest {
+            filtered.sort_by(|a, b| b.pid.cmp(&a.pid));
+        } else {
+            filtered.sort_by(|a, b| a.pid.cmp(&b.pid));
+        }
+
+        vec![filtered.first().cloned().unwrap().clone()]
+    } else {
+        pids.to_vec()
+    }
+}
+
+#[cfg(unix)]
+fn handle_obsolete(args: &mut Vec<String>) -> Option<usize> {
+    // Sanity check
+    if args.len() > 2 {
+        // Old signal can only be in the first argument position
+        let slice = args[1].as_str();
+        if let Some(signal) = slice.strip_prefix('-') {
+            // Check if it is a valid signal
+            let opt_signal = signal_by_name_or_value(signal);
+            if opt_signal.is_some() {
+                // remove the signal before return
+                args.remove(1);
+                return opt_signal;
+            }
+        }
+    }
+    None
+}
+
+#[cfg(unix)]
+fn parse_signal_value(signal_name: &str) -> UResult<usize> {
+    let optional_signal_value = signal_by_name_or_value(signal_name);
+    match optional_signal_value {
+        Some(x) => Ok(x),
+        None => Err(USimpleError::new(
+            1,
+            format!("Unknown signal {}", signal_name.quote()),
+        )),
+    }
+}
+
+#[cfg(unix)]
+fn kill(pids: &Vec<ProcessInformation>, sig: Option<Signal>, echo: bool) {
+    for pid in pids {
+        if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) {
+            show!(Error::from_raw_os_error(e as i32)
+                .map_err_context(|| format!("killing pid {} failed", pid.pid)));
+        } else if echo {
+            println!(
+                "{} killed (pid {})",
+                pid.cmdline.split(" ").next().unwrap_or(""),
+                pid.pid
+            );
+        }
+    }
+}
+
+#[allow(clippy::cognitive_complexity)]
+pub fn uu_app() -> Command {
+    Command::new(uucore::util_name())
+        .version(crate_version!())
+        .about(ABOUT)
+        .override_usage(format_usage(USAGE))
+        .args_override_self(true)
+        .group(ArgGroup::new("oldest_newest").args(["oldest", "newest"]))
+        .args([
+            // arg!(-<sig>                    "signal to send (either number or name)"),
+            arg!(-H --"require-handler"    "match only if signal handler is present"),
+            arg!(-q --queue <value>        "integer value to be sent with the signal"),
+            arg!(-e --echo                 "display what is killed"),
+            arg!(-c --count                "count of matching processes"),
+            arg!(-f --full                 "use full process name to match"),
+            arg!(-g --pgroup <PGID>        "match listed process group IDs")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-G --group <GID>          "match real group IDs")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-i --"ignore-case"        "match case insensitively"),
+            arg!(-n --newest               "select most recently started"),
+            arg!(-o --oldest               "select least recently started"),
+            arg!(-O --older <seconds>      "select where older than seconds")
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-P --parent <PPID>        "match only child processes of the given parent")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-s --session <SID>        "match session IDs")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(--signal <sig>            "signal to send (either number or name)"),
+            arg!(-t --terminal <tty>       "match by controlling terminal")
+                .value_delimiter(','),
+            arg!(-u --euid <ID>            "match by effective IDs")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-U --uid <ID>             "match by real IDs")
+                .value_delimiter(',')
+                .value_parser(clap::value_parser!(u64)),
+            arg!(-x --exact                "match exactly with the command name"),
+            arg!(-F --pidfile <file>       "read PIDs from file"),
+            arg!(-L --logpidfile           "fail if PID file is not locked"),
+            arg!(-r --runstates <state>    "match runstates [D,S,Z,...]"),
+            arg!(-A --"ignore-ancestors"   "exclude our ancestors from results"),
+            arg!(--cgroup <grp>            "match by cgroup v2 names")
+                .value_delimiter(','),
+            arg!(--ns <PID>                "match the processes that belong to the same namespace as <pid>"),
+            arg!(--nslist <ns>             "list which namespaces will be considered for the --ns option.")
+                .value_delimiter(',')
+                .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]),
+        ])
+        .arg(
+            Arg::new("pattern")
+                .help("Name of the program to find the PID of")
+                .action(ArgAction::Append)
+                .index(1),
+        )
+}


Test patch:
diff --git a/tests/by-util/test_pgrep.rs b/tests/by-util/test_pgrep.rs
index bad57b9..6917d46 100644
--- a/tests/by-util/test_pgrep.rs
+++ b/tests/by-util/test_pgrep.rs
@@ -55,11 +55,10 @@ fn test_help() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_oldest() {
+    let re = &Regex::new(SINGLE_PID).unwrap();
+
     for arg in ["-o", "--oldest"] {
-        new_ucmd!()
-            .arg(arg)
-            .succeeds()
-            .stdout_matches(&Regex::new(SINGLE_PID).unwrap());
+        new_ucmd!().arg(arg).succeeds().stdout_matches(re);
     }
 }
 
@@ -77,11 +76,10 @@ fn test_oldest_non_matching_pattern() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_newest() {
+    let re = &Regex::new(SINGLE_PID).unwrap();
+
     for arg in ["-n", "--newest"] {
-        new_ucmd!()
-            .arg(arg)
-            .succeeds()
-            .stdout_matches(&Regex::new(SINGLE_PID).unwrap());
+        new_ucmd!().arg(arg).succeeds().stdout_matches(re);
     }
 }
 
@@ -99,12 +97,10 @@ fn test_newest_non_matching_pattern() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_older() {
+    let re = &Regex::new(MULTIPLE_PIDS).unwrap();
+
     for arg in ["-O", "--older"] {
-        new_ucmd!()
-            .arg(arg)
-            .arg("0")
-            .succeeds()
-            .stdout_matches(&Regex::new(MULTIPLE_PIDS).unwrap());
+        new_ucmd!().arg(arg).arg("0").succeeds().stdout_matches(re);
     }
 }
 
@@ -199,13 +195,16 @@ fn test_ignore_case() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_list_full() {
+    // (?m) enables multi-line mode
+    let re = &Regex::new("(?m)^[1-9][0-9]* .+$").unwrap();
+
     for arg in ["-a", "--list-full"] {
         new_ucmd!()
             .arg("sh")
             .arg(arg)
             .succeeds()
             // (?m) enables multi-line mode
-            .stdout_matches(&Regex::new("(?m)^[1-9][0-9]* .+$").unwrap());
+            .stdout_matches(re);
     }
 }
 
@@ -246,13 +245,15 @@ fn test_count_with_non_matching_pattern() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_terminal() {
+    let re = &Regex::new(MULTIPLE_PIDS).unwrap();
+
     for arg in ["-t", "--terminal"] {
         new_ucmd!()
             .arg(arg)
             .arg("tty1")
             .arg("--inverse") // XXX hack to make test pass in CI
             .succeeds()
-            .stdout_matches(&Regex::new(MULTIPLE_PIDS).unwrap());
+            .stdout_matches(re);
     }
 }
 
@@ -286,12 +287,10 @@ fn test_terminal_invalid_terminal() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_runstates() {
+    let re = &Regex::new(MULTIPLE_PIDS).unwrap();
+
     for arg in ["-r", "--runstates"] {
-        new_ucmd!()
-            .arg(arg)
-            .arg("S")
-            .succeeds()
-            .stdout_matches(&Regex::new(MULTIPLE_PIDS).unwrap());
+        new_ucmd!().arg(arg).arg("S").succeeds().stdout_matches(re);
     }
 }
 
@@ -308,12 +307,10 @@ fn test_runstates_invalid_runstate() {
 #[test]
 #[cfg(target_os = "linux")]
 fn test_parent() {
+    let re = &Regex::new(MULTIPLE_PIDS).unwrap();
+
     for arg in ["-P", "--parent"] {
-        new_ucmd!()
-            .arg(arg)
-            .arg("0")
-            .succeeds()
-            .stdout_matches(&Regex::new(MULTIPLE_PIDS).unwrap());
+        new_ucmd!().arg(arg).arg("0").succeeds().stdout_matches(re);
     }
 }
 
diff --git a/tests/by-util/test_pidof.rs b/tests/by-util/test_pidof.rs
index 96e88e2..5358069 100644
--- a/tests/by-util/test_pidof.rs
+++ b/tests/by-util/test_pidof.rs
@@ -72,10 +72,12 @@ fn test_omit_pid() {
 fn test_separator() {
     use regex::Regex;
 
+    let re = &Regex::new("^[1-9][0-9]*separator[1-9][0-9]*\n$").unwrap();
+
     for arg in ["-S", "-d", "--separator"] {
         new_ucmd!()
             .args(&[arg, "separator", "kthreadd", "kthreadd"])
             .succeeds()
-            .stdout_matches(&Regex::new("^[1-9][0-9]*separator[1-9][0-9]*\n$").unwrap());
+            .stdout_matches(re);
     }
 }
diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs
new file mode 100644
index 0000000..8493502
--- /dev/null
+++ b/tests/by-util/test_pkill.rs
@@ -0,0 +1,51 @@
+// This file is part of the uutils procps package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+#[cfg(unix)]
+use crate::common::util::TestScenario;
+
+#[cfg(unix)]
+#[test]
+fn test_no_args() {
+    new_ucmd!()
+        .fails()
+        .code_is(2)
+        .no_stdout()
+        .stderr_contains("no matching criteria specified");
+}
+
+#[cfg(unix)]
+#[test]
+fn test_non_matching_pattern() {
+    new_ucmd!()
+        .arg("THIS_PATTERN_DOES_NOT_MATCH")
+        .fails()
+        .code_is(1)
+        .no_output();
+}
+
+#[cfg(unix)]
+#[test]
+fn test_too_many_patterns() {
+    new_ucmd!()
+        .arg("sh")
+        .arg("sh")
+        .fails()
+        .code_is(2)
+        .no_stdout()
+        .stderr_contains("only one pattern can be provided");
+}
+
+#[cfg(unix)]
+#[test]
+fn test_invalid_arg() {
+    new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
+}
+
+#[cfg(unix)]
+#[test]
+fn test_help() {
+    new_ucmd!().arg("--help").succeeds();
+}
diff --git a/tests/tests.rs b/tests/tests.rs
index 17b2076..77ce68f 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -52,3 +52,7 @@ mod test_top;
 #[cfg(feature = "snice")]
 #[path = "by-util/test_snice.rs"]
 mod test_snice;
+
+#[cfg(feature = "pkill")]
+#[path = "by-util/test_pkill.rs"]
+mod test_pkill;