Skip to content

Commit 4a88d69

Browse files
authored
Merge pull request #332 from dezgeg/uids
pgrep/pkill/pidwait: Support --uid, --euid & --group options
2 parents a0e254c + ef22c48 commit 4a88d69

File tree

6 files changed

+160
-31
lines changed

6 files changed

+160
-31
lines changed

src/uu/pgrep/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ categories = ["command-line-utilities"]
1313

1414

1515
[dependencies]
16-
uucore = { workspace = true }
16+
uucore = { workspace = true, features = ["entries"] }
1717
clap = { workspace = true }
1818
walkdir = { workspace = true }
1919
regex = { workspace = true }

src/uu/pgrep/src/process.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,43 @@ impl ProcessInformation {
296296
Ok(time)
297297
}
298298

299+
pub fn ppid(&mut self) -> Result<u64, io::Error> {
300+
// the PPID is the fourth field in /proc/<PID>/stat
301+
// (https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10)
302+
self.stat()
303+
.get(3)
304+
.ok_or(io::ErrorKind::InvalidData)?
305+
.parse::<u64>()
306+
.map_err(|_| io::ErrorKind::InvalidData.into())
307+
}
308+
309+
fn get_uid_or_gid_field(&mut self, field: &str, index: usize) -> Result<u32, io::Error> {
310+
self.status()
311+
.get(field)
312+
.ok_or(io::ErrorKind::InvalidData)?
313+
.split_whitespace()
314+
.nth(index)
315+
.ok_or(io::ErrorKind::InvalidData)?
316+
.parse::<u32>()
317+
.map_err(|_| io::ErrorKind::InvalidData.into())
318+
}
319+
320+
pub fn uid(&mut self) -> Result<u32, io::Error> {
321+
self.get_uid_or_gid_field("Uid", 0)
322+
}
323+
324+
pub fn euid(&mut self) -> Result<u32, io::Error> {
325+
self.get_uid_or_gid_field("Uid", 1)
326+
}
327+
328+
pub fn gid(&mut self) -> Result<u32, io::Error> {
329+
self.get_uid_or_gid_field("Gid", 0)
330+
}
331+
332+
pub fn egid(&mut self) -> Result<u32, io::Error> {
333+
self.get_uid_or_gid_field("Gid", 1)
334+
}
335+
299336
/// Fetch run state from [ProcessInformation::cached_stat]
300337
///
301338
/// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10)
@@ -507,4 +544,17 @@ mod tests {
507544
let case = "83875 (sleep (2) .sh) S 75750 83875 75750 34824 83875 4194304 173 0 0 0 0 0 0 0 20 0 1 0 18366278 23187456 821 18446744073709551615 94424231874560 94424232638561 140734866834816 0 0 0 65536 4 65538 1 0 0 17 6 0 0 0 0 0 94424232876752 94424232924772 94424259932160 140734866837287 140734866837313 140734866837313 140734866841576 0";
508545
assert!(stat_split(case)[1] == "sleep (2) .sh");
509546
}
547+
548+
#[test]
549+
#[cfg(target_os = "linux")]
550+
fn test_uid_gid() {
551+
let mut pid_entry = ProcessInformation::try_new(
552+
PathBuf::from_str(&format!("/proc/{}", current_pid())).unwrap(),
553+
)
554+
.unwrap();
555+
assert_eq!(pid_entry.uid().unwrap(), uucore::process::getuid());
556+
assert_eq!(pid_entry.euid().unwrap(), uucore::process::geteuid());
557+
assert_eq!(pid_entry.gid().unwrap(), uucore::process::getgid());
558+
assert_eq!(pid_entry.egid().unwrap(), uucore::process::getegid());
559+
}
510560
}

src/uu/pgrep/src/process_matcher.rs

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55

66
// Common process matcher logic shared by pgrep, pkill and pidwait
77

8-
use std::collections::HashSet;
8+
use std::hash::Hash;
9+
use std::{collections::HashSet, io};
910

1011
use clap::{arg, Arg, ArgAction, ArgMatches};
1112
use regex::Regex;
12-
use uucore::error::{UResult, USimpleError};
1313
#[cfg(unix)]
14-
use uucore::{display::Quotable, signals::signal_by_name_or_value};
14+
use uucore::{
15+
display::Quotable,
16+
entries::{grp2gid, usr2uid},
17+
signals::signal_by_name_or_value,
18+
};
19+
20+
use uucore::error::{UResult, USimpleError};
1521

1622
use crate::process::{walk_process, ProcessInformation, Teletype};
1723

@@ -25,12 +31,15 @@ pub struct Settings {
2531
pub newest: bool,
2632
pub oldest: bool,
2733
pub older: Option<u64>,
28-
pub parent: Option<Vec<u64>>,
34+
pub parent: Option<HashSet<u64>>,
2935
pub runstates: Option<String>,
3036
pub terminal: Option<HashSet<Teletype>>,
3137
#[cfg(unix)]
3238
pub signal: usize,
3339
pub require_handler: bool,
40+
pub uid: Option<HashSet<u32>>,
41+
pub euid: Option<HashSet<u32>>,
42+
pub gid: Option<HashSet<u32>>,
3443
}
3544

3645
pub fn get_match_settings(matches: &ArgMatches) -> UResult<Settings> {
@@ -58,14 +67,26 @@ pub fn get_match_settings(matches: &ArgMatches) -> UResult<Settings> {
5867
#[cfg(unix)]
5968
signal: parse_signal_value(matches.get_one::<String>("signal").unwrap())?,
6069
require_handler: matches.get_flag("require-handler"),
70+
uid: matches
71+
.get_many::<u32>("uid")
72+
.map(|ids| ids.cloned().collect()),
73+
euid: matches
74+
.get_many::<u32>("euid")
75+
.map(|ids| ids.cloned().collect()),
76+
gid: matches
77+
.get_many::<u32>("group")
78+
.map(|ids| ids.cloned().collect()),
6179
};
6280

63-
if (!settings.newest
81+
if !settings.newest
6482
&& !settings.oldest
6583
&& settings.runstates.is_none()
6684
&& settings.older.is_none()
6785
&& settings.parent.is_none()
68-
&& settings.terminal.is_none())
86+
&& settings.terminal.is_none()
87+
&& settings.uid.is_none()
88+
&& settings.euid.is_none()
89+
&& settings.gid.is_none()
6990
&& pattern.is_empty()
7091
{
7192
return Err(USimpleError::new(
@@ -137,6 +158,10 @@ fn try_get_pattern_from(matches: &ArgMatches) -> UResult<String> {
137158
Ok(pattern.to_string())
138159
}
139160

161+
fn any_matches<T: Eq + Hash>(optional_ids: &Option<HashSet<T>>, id: T) -> bool {
162+
optional_ids.as_ref().is_none_or(|ids| ids.contains(&id))
163+
}
164+
140165
/// Collect pids with filter construct from command line arguments
141166
fn collect_matched_pids(settings: &Settings) -> Vec<ProcessInformation> {
142167
// Filtration general parameters
@@ -171,28 +196,23 @@ fn collect_matched_pids(settings: &Settings) -> Vec<ProcessInformation> {
171196
settings.regex.is_match(want)
172197
};
173198

174-
let tty_matched = match &settings.terminal {
175-
Some(ttys) => ttys.contains(&pid.tty()),
176-
None => true,
177-
};
199+
let tty_matched = any_matches(&settings.terminal, pid.tty());
178200

179201
let arg_older = settings.older.unwrap_or(0);
180202
let older_matched = pid.start_time().unwrap() >= arg_older;
181203

182-
// the PPID is the fourth field in /proc/<PID>/stat
183-
// (https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10)
184-
let stat = pid.stat();
185-
let ppid = stat.get(3);
186-
let parent_matched = match (&settings.parent, ppid) {
187-
(Some(parents), Some(ppid)) => parents.contains(&ppid.parse::<u64>().unwrap()),
188-
_ => true,
189-
};
204+
let parent_matched = any_matches(&settings.parent, pid.ppid().unwrap());
205+
206+
let ids_matched = any_matches(&settings.uid, pid.uid().unwrap())
207+
&& any_matches(&settings.euid, pid.euid().unwrap())
208+
&& any_matches(&settings.gid, pid.gid().unwrap());
190209

191210
if (run_state_matched
192211
&& pattern_matched
193212
&& tty_matched
194213
&& older_matched
195-
&& parent_matched)
214+
&& parent_matched
215+
&& ids_matched)
196216
^ settings.inverse
197217
{
198218
tmp_vec.push(pid);
@@ -249,6 +269,36 @@ fn parse_signal_value(signal_name: &str) -> UResult<usize> {
249269
.ok_or_else(|| USimpleError::new(1, format!("Unknown signal {}", signal_name.quote())))
250270
}
251271

272+
#[cfg(not(unix))]
273+
pub fn usr2uid(_name: &str) -> io::Result<u32> {
274+
Err(io::Error::new(
275+
io::ErrorKind::InvalidInput,
276+
"unsupported on this platform",
277+
))
278+
}
279+
280+
#[cfg(not(unix))]
281+
pub fn grp2gid(_name: &str) -> io::Result<u32> {
282+
Err(io::Error::new(
283+
io::ErrorKind::InvalidInput,
284+
"unsupported on this platform",
285+
))
286+
}
287+
288+
fn parse_uid_or_username(uid_or_username: &str) -> io::Result<u32> {
289+
uid_or_username
290+
.parse::<u32>()
291+
.or_else(|_| usr2uid(uid_or_username))
292+
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid user name"))
293+
}
294+
295+
fn parse_gid_or_group_name(gid_or_group_name: &str) -> io::Result<u32> {
296+
gid_or_group_name
297+
.parse::<u32>()
298+
.or_else(|_| grp2gid(gid_or_group_name))
299+
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid group name"))
300+
}
301+
252302
#[allow(clippy::cognitive_complexity)]
253303
pub fn clap_args(pattern_help: &'static str, enable_v_flag: bool) -> Vec<Arg> {
254304
vec![
@@ -263,9 +313,9 @@ pub fn clap_args(pattern_help: &'static str, enable_v_flag: bool) -> Vec<Arg> {
263313
// arg!(-g --pgroup <PGID> "match listed process group IDs")
264314
// .value_delimiter(',')
265315
// .value_parser(clap::value_parser!(u64)),
266-
// arg!(-G --group <GID> "match real group IDs")
267-
// .value_delimiter(',')
268-
// .value_parser(clap::value_parser!(u64)),
316+
arg!(-G --group <GID> "match real group IDs")
317+
.value_delimiter(',')
318+
.value_parser(parse_gid_or_group_name),
269319
arg!(-i --"ignore-case" "match case insensitively"),
270320
arg!(-n --newest "select most recently started")
271321
.group("oldest_newest_inverse"),
@@ -282,12 +332,12 @@ pub fn clap_args(pattern_help: &'static str, enable_v_flag: bool) -> Vec<Arg> {
282332
arg!(--signal <sig> "signal to send (either number or name)")
283333
.default_value("SIGTERM"),
284334
arg!(-t --terminal <tty> "match by controlling terminal").value_delimiter(','),
285-
// arg!(-u --euid <ID> "match by effective IDs")
286-
// .value_delimiter(',')
287-
// .value_parser(clap::value_parser!(u64)),
288-
// arg!(-U --uid <ID> "match by real IDs")
289-
// .value_delimiter(',')
290-
// .value_parser(clap::value_parser!(u64)),
335+
arg!(-u --euid <ID> "match by effective IDs")
336+
.value_delimiter(',')
337+
.value_parser(parse_uid_or_username),
338+
arg!(-U --uid <ID> "match by real IDs")
339+
.value_delimiter(',')
340+
.value_parser(parse_uid_or_username),
291341
arg!(-x --exact "match exactly with the command name"),
292342
// arg!(-F --pidfile <file> "read PIDs from file"),
293343
// arg!(-L --logpidfile "fail if PID file is not locked"),

src/uu/pidwait/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ categories = ["command-line-utilities"]
1414

1515
[dependencies]
1616
nix = { workspace = true }
17-
uucore = { workspace = true }
17+
uucore = { workspace = true, features = ["entries"] }
1818
clap = { workspace = true }
1919
regex = { workspace = true }
2020
uu_pgrep = { path = "../pgrep" }

src/uu/pkill/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ categories = ["command-line-utilities"]
1313

1414

1515
[dependencies]
16-
uucore = { workspace = true }
16+
uucore = { workspace = true, features = ["entries"] }
1717
clap = { workspace = true }
1818
walkdir = { workspace = true }
1919
regex = { workspace = true }

tests/by-util/test_pgrep.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,32 @@ fn test_too_long_pattern() {
391391
.code_is(1)
392392
.stderr_contains("pattern that searches for process name longer than 15 characters will result in zero matches");
393393
}
394+
395+
#[test]
396+
#[cfg(target_os = "linux")]
397+
fn test_invalid_username() {
398+
new_ucmd!()
399+
.arg("--uid=DOES_NOT_EXIST")
400+
.fails()
401+
.code_is(1)
402+
.stderr_contains("invalid user name");
403+
}
404+
405+
#[test]
406+
#[cfg(target_os = "linux")]
407+
fn test_invalid_group_name() {
408+
new_ucmd!()
409+
.arg("--group=DOES_NOT_EXIST")
410+
.fails()
411+
.code_is(1)
412+
.stderr_contains("invalid group name");
413+
}
414+
415+
#[test]
416+
#[cfg(target_os = "linux")]
417+
fn test_current_user() {
418+
new_ucmd!()
419+
.arg("-U")
420+
.arg(uucore::process::getuid().to_string())
421+
.succeeds();
422+
}

0 commit comments

Comments
 (0)