diff --git a/src/uu/ps/src/process_selection.rs b/src/uu/ps/src/process_selection.rs index f955364..3653743 100644 --- a/src/uu/ps/src/process_selection.rs +++ b/src/uu/ps/src/process_selection.rs @@ -47,8 +47,22 @@ pub struct ProcessSelectionSettings { /// - `-x` Lift "must have a tty" restriction. pub dont_require_tty: bool, + /// - `-C` Select by command name + pub command_names: Option>, /// - `-p, --pid` Select specific process IDs pub pids: Option>, + /// - `--ppid` Select specific parent process IDs + pub ppids: Option>, + /// - `--sid` Select specific session IDs + pub sids: Option>, + /// - `-G, --Group` Select by real group ID or name + pub real_groups: Option>, + /// - `-g, --group` Select by effective group ID or name + pub eff_groups: Option>, + /// - `-U, --User` Select by real user ID or name + pub real_users: Option>, + /// - `-u, --user` Select by effective user ID or name + pub eff_users: Option>, /// - `-r` Restrict the selection to only running processes. pub only_running: bool, @@ -64,9 +78,30 @@ impl ProcessSelectionSettings { select_non_session_leaders_with_tty: matches.get_flag("a"), select_non_session_leaders: matches.get_flag("d"), dont_require_tty: matches.get_flag("x"), + command_names: matches + .get_many::>("command") + .map(|xs| xs.flatten().cloned().collect()), pids: matches .get_many::>("pid") .map(|xs| xs.flatten().copied().collect()), + ppids: matches + .get_many::>("ppid") + .map(|xs| xs.flatten().copied().collect()), + sids: matches + .get_many::>("sid") + .map(|xs| xs.flatten().copied().collect()), + real_groups: matches + .get_many::>("real-group") + .map(|xs| xs.flatten().copied().collect()), + eff_groups: matches + .get_many::>("effective-group") + .map(|xs| xs.flatten().copied().collect()), + real_users: matches + .get_many::>("real-user") + .map(|xs| xs.flatten().copied().collect()), + eff_users: matches + .get_many::>("effective-user") + .map(|xs| xs.flatten().copied().collect()), only_running: matches.get_flag("r"), negate_selection: matches.get_flag("deselect"), } @@ -86,8 +121,30 @@ impl ProcessSelectionSettings { return Ok(true); } - if let Some(ref pids) = self.pids { - return Ok(pids.contains(&process.pid)); + // Flags in this group seem to cause rest of the flags to be ignored + let mut matched: Option = None; + fn update_match( + matched: &mut Option, + set_opt: &Option>, + value: U, + ) where + T: std::cmp::Eq + std::hash::Hash + std::borrow::Borrow, + U: std::cmp::Eq + std::hash::Hash, + { + if let Some(ref set) = set_opt { + *matched.get_or_insert_default() |= set.contains(&value); + } + } + update_match(&mut matched, &self.command_names, process.name().unwrap()); + update_match(&mut matched, &self.pids, process.pid); + update_match(&mut matched, &self.ppids, process.ppid().unwrap() as usize); + update_match(&mut matched, &self.sids, process.sid().unwrap() as usize); + update_match(&mut matched, &self.real_users, process.uid().unwrap()); + update_match(&mut matched, &self.eff_users, process.euid().unwrap()); + update_match(&mut matched, &self.real_groups, process.gid().unwrap()); + update_match(&mut matched, &self.eff_groups, process.egid().unwrap()); + if let Some(m) = matched { + return Ok(m); } if self.select_non_session_leaders_with_tty { diff --git a/src/uu/ps/src/ps.rs b/src/uu/ps/src/ps.rs index 14bd749..32f016c 100644 --- a/src/uu/ps/src/ps.rs +++ b/src/uu/ps/src/ps.rs @@ -21,6 +21,8 @@ use parser::{parser, OptionalKeyValue}; use prettytable::{format::consts::FORMAT_CLEAN, Row, Table}; use process_selection::ProcessSelectionSettings; use std::cell::RefCell; +#[cfg(unix)] +use uucore::entries::{grp2gid, usr2uid}; use uucore::{ error::{UError, UResult, USimpleError}, format_usage, help_about, help_usage, @@ -29,6 +31,22 @@ use uucore::{ const ABOUT: &str = help_about!("ps.md"); const USAGE: &str = help_usage!("ps.md"); +#[cfg(not(unix))] +pub fn usr2uid(_name: &str) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "unsupported on this platform", + )) +} + +#[cfg(not(unix))] +pub fn grp2gid(_name: &str) -> std::io::Result { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "unsupported on this platform", + )) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args)?; @@ -138,8 +156,12 @@ fn collect_format( Ok(collect) } -fn parse_numeric_list(s: &str) -> Result, String> { +fn split_arg_list(s: &str) -> impl Iterator { s.split(|c: char| c.is_whitespace() || c == ',') +} + +fn parse_numeric_list(s: &str) -> Result, String> { + split_arg_list(s) .map(|word| { word.parse::() .map_err(|_| format!("invalid number: '{}'", word)) @@ -147,6 +169,32 @@ fn parse_numeric_list(s: &str) -> Result, String> { .collect() } +fn parse_uid_list(s: &str) -> Result, String> { + split_arg_list(s) + .map(|uid_or_username| { + uid_or_username + .parse::() + .or_else(|_| usr2uid(uid_or_username)) + .map_err(|_| format!("invalid user name '{}'", uid_or_username)) + }) + .collect() +} + +fn parse_gid_list(s: &str) -> Result, String> { + split_arg_list(s) + .map(|gid_or_group_name| { + gid_or_group_name + .parse::() + .or_else(|_| grp2gid(gid_or_group_name)) + .map_err(|_| format!("invalid group name '{}'", gid_or_group_name)) + }) + .collect() +} + +fn parse_command_list(s: &str) -> Result, String> { + Ok(split_arg_list(s).map(|part| part.to_string()).collect()) +} + #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -271,6 +319,13 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue) .help("do not print header at all"), ) + .arg( + Arg::new("command") + .short('C') + .action(ArgAction::Append) + .value_parser(parse_command_list) + .help("select by command name"), + ) .arg( Arg::new("pid") .short('p') @@ -279,33 +334,57 @@ pub fn uu_app() -> Command { .value_parser(parse_numeric_list) .help("select by process ID"), ) + .arg( + Arg::new("ppid") + .long("ppid") + .action(ArgAction::Append) + .value_parser(parse_numeric_list) + .help("select by parent process ID"), + ) + .arg( + Arg::new("sid") + .long("sid") + .action(ArgAction::Append) + .value_parser(parse_numeric_list) + .help("select by session ID"), + ) + .arg( + Arg::new("real-group") + .short('G') + .long("Group") + .action(ArgAction::Append) + .value_parser(parse_gid_list) + .help("select by real group ID (RGID) or name"), + ) + .arg( + Arg::new("effective-group") + .short('g') + .long("group") + .action(ArgAction::Append) + .value_parser(parse_gid_list) + .help("select by effective group ID (EGID) or name"), + ) + .arg( + Arg::new("real-user") + .short('U') + .long("User") + .action(ArgAction::Append) + .value_parser(parse_uid_list) + .help("select by real user ID (RUID) or name"), + ) + .arg( + Arg::new("effective-user") + .long("user") + .action(ArgAction::Append) + .value_parser(parse_uid_list) + .help("select by effective user ID (EUID) or name"), + ) // .args([ - // Arg::new("command").short('c').help("command name"), - // Arg::new("GID") - // .short('G') - // .long("Group") - // .help("real group id or name"), - // Arg::new("group") - // .short('g') - // .long("group") - // .help("session or effective group name"), // Arg::new("pPID").long("ppid").help("parent process id"), // Arg::new("qPID") // .short('q') // .long("quick-pid") // .help("process id"), - // Arg::new("session") - // .short('s') - // .long("sid") - // .help("session id"), // Arg::new("t").short('t').long("tty").help("terminal"), - // Arg::new("eUID") - // .short('u') - // .long("user") - // .help("effective user id or name"), - // Arg::new("rUID") - // .short('U') - // .long("User") - // .help("real user id or name"), // ]) } diff --git a/tests/by-util/test_ps.rs b/tests/by-util/test_ps.rs index 18f2e90..de997e7 100644 --- a/tests/by-util/test_ps.rs +++ b/tests/by-util/test_ps.rs @@ -7,6 +7,9 @@ use regex::Regex; use uutests::new_ucmd; +#[cfg(target_os = "linux")] +use uucore::process::geteuid; + #[test] #[cfg(target_os = "linux")] fn test_select_all_processes() { @@ -194,7 +197,7 @@ fn test_deselect() { .args(&["--deselect", "-A", "--no-headers"]) .fails() .code_is(1) - .stdout_is(""); + .no_output(); // PID 1 should be present in inverse of default filter criteria new_ucmd!() @@ -203,6 +206,28 @@ fn test_deselect() { .stdout_matches(&Regex::new("\n *1 ").unwrap()); } +#[test] +#[cfg(target_os = "linux")] +fn test_command_name_selection() { + // Test that test runner process can be located with -C flag + let our_pid = std::process::id(); + let our_comm = std::fs::read_to_string(format!("/proc/{}/comm", our_pid)) + .unwrap() + .trim() + .to_string(); + new_ucmd!() + .args(&["-C", &our_comm, "--no-headers", "-o", "pid"]) + .succeeds() + .stdout_contains(our_pid.to_string()); + + // Test nonexistent command + new_ucmd!() + .args(&["-C", "non_existent_command", "--no-headers"]) + .fails() + .code_is(1) + .no_output(); +} + #[test] #[cfg(target_os = "linux")] fn test_pid_selection() { @@ -224,12 +249,12 @@ fn test_pid_selection() { test(&[flag, "1", flag, &our_pid.to_string()]); } - // Test nonexistent PID (should show no output) + // Test nonexistent PID new_ucmd!() .args(&["-p", "0", "--no-headers"]) .fails() .code_is(1) - .stdout_is(""); + .no_output(); // Test invalid PID new_ucmd!() @@ -237,3 +262,143 @@ fn test_pid_selection() { .fails() .stderr_contains("invalid number"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_ppid_selection() { + new_ucmd!() + .args(&["--ppid", "1"]) + .succeeds() + .stdout_does_not_match(&Regex::new(".*\n *1 +.*").unwrap()); + + // Test nonexistent PPID + new_ucmd!() + .args(&["--ppid", "999999", "--no-headers"]) + .fails() + .code_is(1) + .no_output(); + + // Test invalid PPID + new_ucmd!() + .args(&["--ppid", "invalid"]) + .fails() + .stderr_contains("invalid number"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_sid_selection() { + new_ucmd!() + .args(&["--sid", "1"]) + .succeeds() + .stdout_matches(&Regex::new(".*\n *1 +.*").unwrap()); + + // Test nonexistent SID + new_ucmd!() + .args(&["--sid", "999999", "--no-headers"]) + .fails() + .code_is(1) + .no_output(); + + // Test invalid SID + new_ucmd!() + .args(&["--sid", "invalid"]) + .fails() + .stderr_contains("invalid number"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_effective_user_selection() { + let regex = Regex::new("^( *0 +root *\n)+").unwrap(); + + for user_param in ["root", "0"] { + new_ucmd!() + .args(&["--user", user_param, "--no-headers", "-o", "euid,euser"]) + .succeeds() + .stdout_matches(®ex); + } + + new_ucmd!() + .args(&["--user", "nonexistent_user"]) + .fails() + .stderr_contains("invalid user name"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_real_user_selection() { + let regex = Regex::new("^( *0 +root *\n)+").unwrap(); + + for user_param in ["root", "0"] { + new_ucmd!() + .args(&["--User", user_param, "--no-headers", "-o", "ruid,ruser"]) + .succeeds() + .stdout_matches(®ex); + } + + new_ucmd!() + .args(&["--User", "nonexistent_user"]) + .fails() + .stderr_contains("invalid user name"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_effective_group_selection() { + let regex = Regex::new("^( *0 +root *\n)+").unwrap(); + + for group_param in ["root", "0"] { + new_ucmd!() + .args(&["--group", group_param, "--no-headers", "-o", "egid,egroup"]) + .succeeds() + .stdout_matches(®ex); + } + + new_ucmd!() + .args(&["--group", "nonexistent_group"]) + .fails() + .stderr_contains("invalid group name"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_real_group_selection() { + let regex = Regex::new("^( *0 +root *\n)+").unwrap(); + + for group_param in ["root", "0"] { + new_ucmd!() + .args(&["--Group", group_param, "--no-headers", "-o", "rgid,rgroup"]) + .succeeds() + .stdout_matches(®ex); + } + + new_ucmd!() + .args(&["--Group", "nonexistent_group"]) + .fails() + .stderr_contains("invalid group name"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_combined_selection_criteria() { + let pids: Vec = new_ucmd!() + .args(&[ + "--pid", + "1", + "--user", + &geteuid().to_string(), + "--no-headers", + "-o", + "pid", + ]) + .succeeds() + .stdout_str() + .lines() + .filter_map(|line| line.trim().parse::().ok()) + .collect(); + + // Should include PID 1 and processes owned by current user + assert!(pids.contains(&1)); + assert!(pids.len() > 1); +}