ps: Support more process selection flags

This commit is contained in:
Tuomas Tynkkynen
2025-11-03 19:04:15 +02:00
parent 6c674aca97
commit 13c2f7510a
3 changed files with 328 additions and 27 deletions
+59 -2
View File
@@ -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<HashSet<String>>,
/// - `-p, --pid` Select specific process IDs
pub pids: Option<HashSet<usize>>,
/// - `--ppid` Select specific parent process IDs
pub ppids: Option<HashSet<usize>>,
/// - `--sid` Select specific session IDs
pub sids: Option<HashSet<usize>>,
/// - `-G, --Group` Select by real group ID or name
pub real_groups: Option<HashSet<u32>>,
/// - `-g, --group` Select by effective group ID or name
pub eff_groups: Option<HashSet<u32>>,
/// - `-U, --User` Select by real user ID or name
pub real_users: Option<HashSet<u32>>,
/// - `-u, --user` Select by effective user ID or name
pub eff_users: Option<HashSet<u32>>,
/// - `-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::<Vec<String>>("command")
.map(|xs| xs.flatten().cloned().collect()),
pids: matches
.get_many::<Vec<usize>>("pid")
.map(|xs| xs.flatten().copied().collect()),
ppids: matches
.get_many::<Vec<usize>>("ppid")
.map(|xs| xs.flatten().copied().collect()),
sids: matches
.get_many::<Vec<usize>>("sid")
.map(|xs| xs.flatten().copied().collect()),
real_groups: matches
.get_many::<Vec<u32>>("real-group")
.map(|xs| xs.flatten().copied().collect()),
eff_groups: matches
.get_many::<Vec<u32>>("effective-group")
.map(|xs| xs.flatten().copied().collect()),
real_users: matches
.get_many::<Vec<u32>>("real-user")
.map(|xs| xs.flatten().copied().collect()),
eff_users: matches
.get_many::<Vec<u32>>("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<bool> = None;
fn update_match<T, U>(
matched: &mut Option<bool>,
set_opt: &Option<HashSet<T>>,
value: U,
) where
T: std::cmp::Eq + std::hash::Hash + std::borrow::Borrow<U>,
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 {
+101 -22
View File
@@ -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<u32> {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"unsupported on this platform",
))
}
#[cfg(not(unix))]
pub fn grp2gid(_name: &str) -> std::io::Result<u32> {
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<Vec<usize>, String> {
fn split_arg_list(s: &str) -> impl Iterator<Item = &str> {
s.split(|c: char| c.is_whitespace() || c == ',')
}
fn parse_numeric_list(s: &str) -> Result<Vec<usize>, String> {
split_arg_list(s)
.map(|word| {
word.parse::<usize>()
.map_err(|_| format!("invalid number: '{}'", word))
@@ -147,6 +169,32 @@ fn parse_numeric_list(s: &str) -> Result<Vec<usize>, String> {
.collect()
}
fn parse_uid_list(s: &str) -> Result<Vec<u32>, String> {
split_arg_list(s)
.map(|uid_or_username| {
uid_or_username
.parse::<u32>()
.or_else(|_| usr2uid(uid_or_username))
.map_err(|_| format!("invalid user name '{}'", uid_or_username))
})
.collect()
}
fn parse_gid_list(s: &str) -> Result<Vec<u32>, String> {
split_arg_list(s)
.map(|gid_or_group_name| {
gid_or_group_name
.parse::<u32>()
.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<Vec<String>, 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"),
// ])
}
+168 -3
View File
@@ -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(&regex);
}
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(&regex);
}
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(&regex);
}
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(&regex);
}
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<u32> = new_ucmd!()
.args(&[
"--pid",
"1",
"--user",
&geteuid().to_string(),
"--no-headers",
"-o",
"pid",
])
.succeeds()
.stdout_str()
.lines()
.filter_map(|line| line.trim().parse::<u32>().ok())
.collect();
// Should include PID 1 and processes owned by current user
assert!(pids.contains(&1));
assert!(pids.len() > 1);
}