@@ -9,7 +9,8 @@ use argh::FromArgs;
99use crossterm:: event:: MouseEventKind ;
1010use crossterm:: event:: { KeyCode , KeyModifiers } ;
1111use nix:: unistd:: User ;
12- use process:: { ProcessList , ProcessSnapshot } ;
12+ use process:: { ProcessInfo , ProcessList , ProcessSnapshot } ;
13+ use regex:: Regex ;
1314use taskstats:: { TaskStats , TaskStatsConnection } ;
1415use tokio_util:: sync:: CancellationToken ;
1516use ui:: { Event , SortColumn , Tui , UIState } ;
@@ -54,6 +55,10 @@ struct Args {
5455 #[ argh( option, short = 'u' ) ]
5556 user : Vec < String > ,
5657
58+ /// regex to match against the command line (can be repeated)
59+ #[ argh( option, short = 'c' ) ]
60+ command : Vec < String > ,
61+
5762 /// add timestamp on each line (implies --batch)
5863 #[ argh( switch, short = 't' ) ]
5964 time : bool ,
@@ -85,6 +90,9 @@ async fn main() -> Result<()> {
8590 // Resolve usernames to UIDs
8691 let uids = resolve_users ( & args. user ) ?;
8792
93+ // Compile command-line regex filters (invalid patterns abort early)
94+ let command_regexes = compile_command_regexes ( & args. command ) ?;
95+
8896 // Connect to taskstats
8997 let taskstats_conn = TaskStatsConnection :: new ( ) ?;
9098 warn_if_taskstats_unreadable ( & taskstats_conn) ;
@@ -93,9 +101,9 @@ async fn main() -> Result<()> {
93101 . with_uids ( uids. clone ( ) ) ;
94102
95103 if args. batch || args. time || args. quiet || args. json || args. csv {
96- run_batch_mode ( & mut process_list, & args) ?;
104+ run_batch_mode ( & mut process_list, & args, & command_regexes ) ?;
97105 } else {
98- run_interactive_mode ( & mut process_list, & args) . await ?;
106+ run_interactive_mode ( & mut process_list, & args, & command_regexes ) . await ?;
99107 }
100108
101109 Ok ( ( ) )
@@ -167,7 +175,32 @@ fn resolve_users(users: &[String]) -> Result<Vec<u32>> {
167175 Ok ( uids)
168176}
169177
170- async fn run_interactive_mode ( process_list : & mut ProcessList , args : & Args ) -> Result < ( ) > {
178+ /// Compile the `-c/--command` regex filters. Returns an error describing the
179+ /// offending pattern so invalid regexes fail fast at startup.
180+ fn compile_command_regexes ( patterns : & [ String ] ) -> Result < Vec < Regex > > {
181+ patterns
182+ . iter ( )
183+ . map ( |p| {
184+ Regex :: new ( p) . map_err ( |e| anyhow:: anyhow!( "invalid command regex {:?}: {}" , p, e) )
185+ } )
186+ . collect ( )
187+ }
188+
189+ /// Filter a process list in place so only processes whose command line matches
190+ /// at least one of the given regexes remain. When `regexes` is empty no
191+ /// filtering is applied. Used by batch and interactive modes alike.
192+ fn filter_by_command ( processes : & mut Vec < & ProcessInfo > , regexes : & [ Regex ] ) {
193+ if regexes. is_empty ( ) {
194+ return ;
195+ }
196+ processes. retain ( |p| regexes. iter ( ) . any ( |re| re. is_match ( p. get_cmdline ( ) ) ) ) ;
197+ }
198+
199+ async fn run_interactive_mode (
200+ process_list : & mut ProcessList ,
201+ args : & Args ,
202+ command_regexes : & [ Regex ] ,
203+ ) -> Result < ( ) > {
171204 let mut tui = Tui :: new ( ) ?;
172205 tui. enter ( ) ?;
173206
@@ -212,7 +245,13 @@ async fn run_interactive_mode(process_list: &mut ProcessList, args: &Args) -> Re
212245
213246 }
214247 Event :: DataUpdate ( snapshot) => {
215- render_snapshot( & mut tui, & snapshot, & mut state, has_delay_acct) ?;
248+ render_snapshot(
249+ & mut tui,
250+ & snapshot,
251+ & mut state,
252+ has_delay_acct,
253+ command_regexes,
254+ ) ?;
216255
217256 // Check iteration limit
218257 if let Some ( max_iter) = args. iterations {
@@ -224,7 +263,13 @@ async fn run_interactive_mode(process_list: &mut ProcessList, args: &Args) -> Re
224263 }
225264 Event :: Render => {
226265 if let Some ( ref snapshot) = current_snapshot {
227- render_snapshot( & mut tui, snapshot, & mut state, has_delay_acct) ?;
266+ render_snapshot(
267+ & mut tui,
268+ snapshot,
269+ & mut state,
270+ has_delay_acct,
271+ command_regexes,
272+ ) ?;
228273 }
229274 }
230275 Event :: Key ( key) => match key. code {
@@ -418,12 +463,14 @@ fn render_snapshot(
418463 snapshot : & ProcessSnapshot ,
419464 state : & mut UIState ,
420465 has_delay_acct : bool ,
466+ command_regexes : & [ Regex ] ,
421467) -> Result < ( ) > {
422468 let mut processes: Vec < & process:: ProcessInfo > = snapshot. processes . values ( ) . collect ( ) ;
423469
424470 if state. only_active {
425471 processes. retain ( |p| p. did_some_io ( state. accumulated ) ) ;
426472 }
473+ filter_by_command ( & mut processes, command_regexes) ;
427474
428475 sort_processes ( & mut processes, state) ;
429476
@@ -515,14 +562,18 @@ fn sort_processes(processes: &mut Vec<&process::ProcessInfo>, state: &UIState) {
515562/// Batch mode outputs process I/O statistics to stdout in a parseable format.
516563/// This function gracefully handles broken pipe errors (e.g., when output is
517564/// piped to `head` or similar utilities) by returning Ok(()) when write errors occur.
518- fn run_batch_mode ( process_list : & mut ProcessList , args : & Args ) -> Result < ( ) > {
565+ fn run_batch_mode (
566+ process_list : & mut ProcessList ,
567+ args : & Args ,
568+ command_regexes : & [ Regex ] ,
569+ ) -> Result < ( ) > {
519570 // Machine-readable output modes take precedence over the plain-text batch
520571 // output. They share the same refresh loop and iteration/delay semantics.
521572 if args. json {
522- return run_json_mode ( process_list, args) ;
573+ return run_json_mode ( process_list, args, command_regexes ) ;
523574 }
524575 if args. csv {
525- return run_csv_mode ( process_list, args) ;
576+ return run_csv_mode ( process_list, args, command_regexes ) ;
526577 }
527578
528579 use std:: io:: { self , Write } ;
@@ -607,6 +658,7 @@ fn run_batch_mode(process_list: &mut ProcessList, args: &Args) -> Result<()> {
607658 if args. only {
608659 processes. retain ( |p| p. did_some_io ( args. accumulated ) ) ;
609660 }
661+ filter_by_command ( & mut processes, command_regexes) ;
610662
611663 processes. sort_by ( |a, b| {
612664 let stats_a = if args. accumulated {
@@ -786,7 +838,11 @@ fn collect_row(process: &process::ProcessInfo, accumulated: bool, duration: f64)
786838
787839/// Batch mode with `--json`: emit one JSON object per iteration, containing the
788840/// iteration timestamp, total/actual disk I/O, and the process list.
789- fn run_json_mode ( process_list : & mut ProcessList , args : & Args ) -> Result < ( ) > {
841+ fn run_json_mode (
842+ process_list : & mut ProcessList ,
843+ args : & Args ,
844+ command_regexes : & [ Regex ] ,
845+ ) -> Result < ( ) > {
790846 use std:: io:: { self , Write } ;
791847 use std:: thread;
792848 use std:: time:: Duration ;
@@ -806,6 +862,7 @@ fn run_json_mode(process_list: &mut ProcessList, args: &Args) -> Result<()> {
806862 if args. only {
807863 processes. retain ( |p| p. did_some_io ( args. accumulated ) ) ;
808864 }
865+ filter_by_command ( & mut processes, command_regexes) ;
809866 sort_processes_by_io ( & mut processes, args) ;
810867
811868 let rows: Vec < serde_json:: Value > = processes
@@ -864,7 +921,11 @@ fn csv_field(value: &str) -> String {
864921/// Batch mode with `--csv`: emit one CSV row per process per iteration. A header
865922/// row is printed before the first iteration (unless `--quiet`). With `--time` a
866923/// leading time column is added. Compatible with `-t`/`-q`.
867- fn run_csv_mode ( process_list : & mut ProcessList , args : & Args ) -> Result < ( ) > {
924+ fn run_csv_mode (
925+ process_list : & mut ProcessList ,
926+ args : & Args ,
927+ command_regexes : & [ Regex ] ,
928+ ) -> Result < ( ) > {
868929 use std:: io:: { self , Write } ;
869930 use std:: thread;
870931 use std:: time:: Duration ;
@@ -887,6 +948,7 @@ fn run_csv_mode(process_list: &mut ProcessList, args: &Args) -> Result<()> {
887948 if args. only {
888949 processes. retain ( |p| p. did_some_io ( args. accumulated ) ) ;
889950 }
951+ filter_by_command ( & mut processes, command_regexes) ;
890952 sort_processes_by_io ( & mut processes, args) ;
891953
892954 if !args. quiet && !header_printed {
@@ -952,3 +1014,82 @@ fn run_csv_mode(process_list: &mut ProcessList, args: &Args) -> Result<()> {
9521014 }
9531015 Ok ( ( ) )
9541016}
1017+
1018+ #[ cfg( test) ]
1019+ mod tests {
1020+ use super :: * ;
1021+
1022+ fn process_with_cmdline ( pid : i32 , cmdline : & str ) -> ProcessInfo {
1023+ let mut p = ProcessInfo :: new ( pid) ;
1024+ p. cmdline = Some ( cmdline. to_string ( ) ) ;
1025+ p
1026+ }
1027+
1028+ fn refs ( owned : & mut [ ProcessInfo ] ) -> Vec < & ProcessInfo > {
1029+ owned. iter ( ) . collect ( )
1030+ }
1031+
1032+ #[ test]
1033+ fn test_compile_command_regexes_valid ( ) {
1034+ let regexes = compile_command_regexes ( & [ "bash" . to_string ( ) , "ssh.*" . to_string ( ) ] ) . unwrap ( ) ;
1035+ assert_eq ! ( regexes. len( ) , 2 ) ;
1036+ }
1037+
1038+ #[ test]
1039+ fn test_compile_command_regexes_invalid ( ) {
1040+ assert ! ( compile_command_regexes( & [ "[" . to_string( ) ] ) . is_err( ) ) ;
1041+ }
1042+
1043+ #[ test]
1044+ fn test_filter_by_command_empty_noop ( ) {
1045+ let mut owned = vec ! [
1046+ process_with_cmdline( 1 , "bash" ) ,
1047+ process_with_cmdline( 2 , "sshd -D" ) ,
1048+ ] ;
1049+ let mut processes = refs ( & mut owned) ;
1050+ filter_by_command ( & mut processes, & [ ] ) ;
1051+ assert_eq ! ( processes. len( ) , 2 ) ;
1052+ }
1053+
1054+ #[ test]
1055+ fn test_filter_by_command_matches_substring ( ) {
1056+ let mut owned = vec ! [
1057+ process_with_cmdline( 1 , "/usr/bin/bash --login" ) ,
1058+ process_with_cmdline( 2 , "sshd: root@pts/0" ) ,
1059+ process_with_cmdline( 3 , "nginx: worker" ) ,
1060+ ] ;
1061+ let regexes = compile_command_regexes ( & [ "bash" . to_string ( ) ] ) . unwrap ( ) ;
1062+ let mut processes = refs ( & mut owned) ;
1063+ filter_by_command ( & mut processes, & regexes) ;
1064+ assert_eq ! ( processes. len( ) , 1 ) ;
1065+ assert_eq ! ( processes[ 0 ] . pid, 1 ) ;
1066+ }
1067+
1068+ #[ test]
1069+ fn test_filter_by_command_multiple_regexes_any ( ) {
1070+ let mut owned = vec ! [
1071+ process_with_cmdline( 1 , "bash" ) ,
1072+ process_with_cmdline( 2 , "nginx" ) ,
1073+ process_with_cmdline( 3 , "sleep 10" ) ,
1074+ ] ;
1075+ let regexes =
1076+ compile_command_regexes ( & [ "bash" . to_string ( ) , "nginx" . to_string ( ) ] ) . unwrap ( ) ;
1077+ let mut processes = refs ( & mut owned) ;
1078+ filter_by_command ( & mut processes, & regexes) ;
1079+ let remaining: Vec < i32 > = processes. iter ( ) . map ( |p| p. pid ) . collect ( ) ;
1080+ assert_eq ! ( remaining, vec![ 1 , 2 ] ) ;
1081+ }
1082+
1083+ #[ test]
1084+ fn test_filter_by_command_regex_anchoring ( ) {
1085+ let mut owned = vec ! [
1086+ process_with_cmdline( 1 , "bash" ) ,
1087+ process_with_cmdline( 2 , "notbash" ) ,
1088+ ] ;
1089+ let regexes = compile_command_regexes ( & [ "^bash$" . to_string ( ) ] ) . unwrap ( ) ;
1090+ let mut processes = refs ( & mut owned) ;
1091+ filter_by_command ( & mut processes, & regexes) ;
1092+ assert_eq ! ( processes. len( ) , 1 ) ;
1093+ assert_eq ! ( processes[ 0 ] . pid, 1 ) ;
1094+ }
1095+ }
0 commit comments