diff --git a/src/uu/pgrep/src/pgrep.rs b/src/uu/pgrep/src/pgrep.rs index 802ec507..c947c2d8 100644 --- a/src/uu/pgrep/src/pgrep.rs +++ b/src/uu/pgrep/src/pgrep.rs @@ -5,36 +5,14 @@ // Pid utils pub mod process; +pub mod process_matcher; -use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; -use process::{walk_process, ProcessInformation, Teletype}; -use regex::Regex; -use std::{collections::HashSet, sync::OnceLock}; -#[cfg(unix)] -use uucore::{display::Quotable, signals::signal_by_name_or_value}; -use uucore::{ - error::{UResult, USimpleError}, - format_usage, help_about, help_usage, -}; +use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, Command}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("pgrep.md"); const USAGE: &str = help_usage!("pgrep.md"); -static REGEX: OnceLock = OnceLock::new(); - -struct Settings { - exact: bool, - full: bool, - ignore_case: bool, - inverse: bool, - newest: bool, - oldest: bool, - older: Option, - parent: Option>, - runstates: Option, - terminal: Option>, -} - /// # Conceptual model of `pgrep` /// /// At first, `pgrep` command will check the patterns is legal. @@ -50,67 +28,10 @@ struct Settings { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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"), - inverse: matches.get_flag("inverse"), - newest: matches.get_flag("newest"), - oldest: matches.get_flag("oldest"), - parent: matches - .get_many::("parent") - .map(|parents| parents.copied().collect()), - runstates: matches.get_one::("runstates").cloned(), - older: matches.get_one::("older").copied(), - terminal: matches.get_many::("terminal").map(|ttys| { - ttys.cloned() - .flat_map(Teletype::try_from) - .collect::>() - }), - }; - - 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 `pgrep --help' for more information.", - )); - } - - // Parse signal - #[cfg(unix)] - let sig_num = parse_signal_value(matches.get_one::("signal").unwrap())?; + let settings = process_matcher::get_match_settings(&matches)?; // Collect pids - let pids = { - let mut pids = collect_matched_pids(&settings); - #[cfg(unix)] - if matches.get_flag("require-handler") { - pids.retain(|pid| { - let mask = - u64::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) - } - }; + let pids = process_matcher::find_matching_pids(&settings); // Processing output let output = if matches.get_flag("count") { @@ -148,153 +69,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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 { - let pattern = match matches.get_many::("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 { - // Filtration general parameters - let filtered: Vec = { - let mut tmp_vec = Vec::new(); - - for mut pid in walk_process().collect::>() { - 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//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//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//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::().unwrap()), - _ => true, - }; - - if (run_state_matched - && pattern_matched - && tty_matched - && older_matched - && parent_matched) - ^ settings.inverse - { - 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 { - 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::>(); - - 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 parse_signal_value(signal_name: &str) -> UResult { - signal_by_name_or_value(signal_name) - .ok_or_else(|| USimpleError::new(1, format!("Unknown signal {}", signal_name.quote()))) -} - #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) diff --git a/src/uu/pgrep/src/process_matcher.rs b/src/uu/pgrep/src/process_matcher.rs new file mode 100644 index 00000000..51a9b2b2 --- /dev/null +++ b/src/uu/pgrep/src/process_matcher.rs @@ -0,0 +1,250 @@ +// 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. + +// Common process matcher logic shared by pgrep, pkill and pidwait + +use std::collections::HashSet; + +use clap::ArgMatches; +use regex::Regex; +use uucore::error::{UResult, USimpleError}; +#[cfg(unix)] +use uucore::{display::Quotable, signals::signal_by_name_or_value}; + +use crate::process::{walk_process, ProcessInformation, Teletype}; + +pub struct Settings { + pub regex: Regex, + + pub exact: bool, + pub full: bool, + pub ignore_case: bool, + pub inverse: bool, + pub newest: bool, + pub oldest: bool, + pub older: Option, + pub parent: Option>, + pub runstates: Option, + pub terminal: Option>, + #[cfg(unix)] + pub signal: usize, + pub require_handler: bool, +} + +pub fn get_match_settings(matches: &ArgMatches) -> UResult { + let pattern = try_get_pattern_from(matches)?; + let regex = Regex::new(&pattern).map_err(|e| USimpleError::new(2, e.to_string()))?; + + let settings = Settings { + regex, + exact: matches.get_flag("exact"), + full: matches.get_flag("full"), + ignore_case: matches.get_flag("ignore-case"), + inverse: matches.get_flag("inverse"), + newest: matches.get_flag("newest"), + oldest: matches.get_flag("oldest"), + parent: matches + .get_many::("parent") + .map(|parents| parents.copied().collect()), + runstates: matches.get_one::("runstates").cloned(), + older: matches.get_one::("older").copied(), + terminal: matches.get_many::("terminal").map(|ttys| { + ttys.cloned() + .flat_map(Teletype::try_from) + .collect::>() + }), + #[cfg(unix)] + signal: parse_signal_value(matches.get_one::("signal").unwrap())?, + require_handler: matches.get_flag("require-handler"), + }; + + 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, + format!( + "no matching criteria specified\n\ + Try `{} --help' for more information.", + uucore::util_name() + ), + )); + } + + Ok(settings) +} + +pub fn find_matching_pids(settings: &Settings) -> Vec { + let mut pids = collect_matched_pids(settings); + #[cfg(unix)] + if settings.require_handler { + pids.retain(|pid| { + let mask = + u64::from_str_radix(pid.clone().status().get("SigCgt").unwrap(), 16).unwrap(); + mask & (1 << settings.signal) != 0 + }); + } + if pids.is_empty() { + uucore::error::set_exit_code(1); + pids + } else { + process_flag_o_n(settings, &mut pids) + } +} + +/// 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 { + let pattern = match matches.get_many::("pattern") { + Some(patterns) if patterns.len() > 1 => { + return Err(USimpleError::new( + 2, + format!( + "only one pattern can be provided\nTry `{} --help' for more information.", + uucore::util_name() + ), + )) + } + 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 { + // Filtration general parameters + let filtered: Vec = { + let mut tmp_vec = Vec::new(); + + for mut pid in walk_process().collect::>() { + 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//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//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] + }; + + settings.regex.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//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::().unwrap()), + _ => true, + }; + + if (run_state_matched + && pattern_matched + && tty_matched + && older_matched + && parent_matched) + ^ settings.inverse + { + 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 { + 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::>(); + + 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 parse_signal_value(signal_name: &str) -> UResult { + signal_by_name_or_value(signal_name) + .ok_or_else(|| USimpleError::new(1, format!("Unknown signal {}", signal_name.quote()))) +} diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 808b3e6d..3c121e2d 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -4,47 +4,28 @@ // file that was distributed with this source code. // Pid utils -use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; +use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, 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 uu_pgrep::process::ProcessInformation; +use uu_pgrep::process_matcher; #[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, -}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; const ABOUT: &str = help_about!("pkill.md"); const USAGE: &str = help_usage!("pkill.md"); -static REGEX: OnceLock = OnceLock::new(); - -struct Settings { - exact: bool, - full: bool, - ignore_case: bool, - inverse: bool, - newest: bool, - oldest: bool, - older: Option, - parent: Option>, - runstates: Option, - terminal: Option>, -} - #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(unix)] @@ -55,81 +36,24 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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"), - inverse: matches.get_flag("inverse"), - newest: matches.get_flag("newest"), - oldest: matches.get_flag("oldest"), - parent: matches - .get_many::("parent") - .map(|parents| parents.copied().collect()), - runstates: matches.get_one::("runstates").cloned(), - older: matches.get_one::("older").copied(), - terminal: matches.get_many::("terminal").map(|ttys| { - ttys.cloned() - .flat_map(Teletype::try_from) - .collect::>() - }), - }; - - 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 = parse_signal_value(matches.get_one::("signal").unwrap())?; + let settings = process_matcher::get_match_settings(&matches)?; #[cfg(unix)] - let sig_name = signal_name_by_value(sig_num); + let sig_name = signal_name_by_value(settings.signal); // Signal does not support converting from EXIT // Instead, nix::signal::kill expects Option::None to properly handle EXIT #[cfg(unix)] let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { None } else { - let sig = (sig_num as i32) + let sig = (settings.signal 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 = - u64::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) - } - }; + let pids = process_matcher::find_matching_pids(&settings); // Send signal // TODO: Implement -q @@ -145,147 +69,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 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 { - let pattern = match matches.get_many::("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 { - // Filtration general parameters - let filtered: Vec = { - let mut tmp_vec = Vec::new(); - - for mut pid in walk_process().collect::>() { - 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//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//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//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::().unwrap()), - _ => true, - }; - - if (run_state_matched - && pattern_matched - && tty_matched - && older_matched - && parent_matched) - ^ settings.inverse - { - 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 { - 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::>(); - - 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 [String]) { // Sanity check @@ -303,12 +86,6 @@ fn handle_obsolete(args: &mut [String]) { } } -#[cfg(unix)] -fn parse_signal_value(signal_name: &str) -> UResult { - signal_by_name_or_value(signal_name) - .ok_or_else(|| USimpleError::new(1, format!("Unknown signal {}", signal_name.quote()))) -} - #[cfg(unix)] fn kill(pids: &Vec, sig: Option, echo: bool) { for pid in pids {