Skip to main content

proc_cli/commands/
by.rs

1//! `proc by` - Filter processes by name
2//!
3//! Examples:
4//!   proc by node               # Processes named 'node'
5//!   proc by node --cmd         # ... or with node anywhere in their command line
6//!   proc by node --in .        # Node processes in current directory
7//!   proc by node --min-cpu 5   # Node processes using >5% CPU
8//!   proc by "my app"           # Processes with spaces in name
9
10use crate::core::{resolve_in_dir, sort_processes, NameMatch, Process, SortKey, StatusFilter};
11use crate::error::Result;
12use crate::ui::Printer;
13use clap::Args;
14use std::path::PathBuf;
15
16/// Filter processes by name
17#[derive(Args, Debug)]
18pub struct ByCommand {
19    /// Process name to match (the whole name, ignoring case)
20    pub name: String,
21
22    /// Match the name as a substring of the process name or full command line (like pkill -f)
23    #[arg(long)]
24    pub cmd: bool,
25
26    /// Filter by directory; takes the next word as the directory unless it starts with '-'
27    /// (bare --in is the current directory, --in=DIR is explicit)
28    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
29    pub in_dir: Option<String>,
30
31    /// Only show processes using more than this CPU %
32    #[arg(long)]
33    pub min_cpu: Option<f32>,
34
35    /// Only show processes using more than this memory (MB)
36    #[arg(long)]
37    pub min_mem: Option<f64>,
38
39    /// Filter by status: running, sleeping, stopped, zombie
40    #[arg(long, value_enum, ignore_case = true)]
41    pub status: Option<StatusFilter>,
42
43    /// Only show processes running longer than this (seconds)
44    #[arg(long)]
45    pub min_uptime: Option<u64>,
46
47    /// Only show children of this parent PID
48    #[arg(long)]
49    pub parent: Option<u32>,
50
51    /// Output as JSON
52    #[arg(long, short = 'j')]
53    pub json: bool,
54
55    /// Show verbose output with command line, cwd, parent PID, and uptime
56    #[arg(long, short = 'v')]
57    pub verbose: bool,
58
59    /// Print full directories and arguments instead of fitting the terminal width
60    #[arg(long, short = 'w')]
61    pub wide: bool,
62
63    /// Limit the number of results
64    #[arg(long, short = 'n')]
65    pub limit: Option<usize>,
66
67    /// Sort by: cpu, mem (biggest first); pid, name (ascending). Ties come by PID
68    #[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
69    pub sort: SortKey,
70}
71
72impl ByCommand {
73    /// Executes the by command, listing processes matching the name filter.
74    pub fn execute(&self) -> Result<()> {
75        let printer = Printer::from_flags(self.json, self.verbose).wide(self.wide);
76
77        // Get processes by name
78        let mut processes = Process::find_by_name(&self.name, NameMatch::from_flag(self.cmd))?;
79
80        // Real CPU readings for the table, --min-cpu and --sort cpu
81        Process::measure_cpu(&mut processes);
82
83        // Resolve --in filter path
84        let in_dir_filter = resolve_in_dir(&self.in_dir)?;
85        let in_dir_has_any = in_dir_filter.as_ref().is_some_and(|dir_path| {
86            processes
87                .iter()
88                .any(|p| crate::core::filters::matches_in_filter(p, dir_path))
89        });
90        crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
91
92        // Apply filters
93        processes.retain(|p| self.keeps(p, &in_dir_filter));
94
95        // Sort processes
96        sort_processes(&mut processes, self.sort);
97
98        // Apply limit if specified
99        if let Some(limit) = self.limit {
100            processes.truncate(limit);
101        }
102
103        // Build context string for output
104        let mut context_parts = vec![format!("by '{}'", self.name)];
105        if let Some(ref dir) = in_dir_filter {
106            context_parts.push(format!("in {}", dir.display()));
107        }
108        let context = Some(context_parts.join(" "));
109
110        printer.print_processes_as("by", &processes, context.as_deref());
111        Ok(())
112    }
113
114    /// Whether a process passes every filter: `--in`, `--min-cpu`, `--min-mem`,
115    /// `--status`, `--min-uptime` and `--parent`.
116    fn keeps(&self, p: &Process, in_dir_filter: &Option<PathBuf>) -> bool {
117        // Directory filter (--in)
118        if let Some(ref dir_path) = in_dir_filter {
119            if !crate::core::filters::matches_in_filter(p, dir_path) {
120                return false;
121            }
122        }
123
124        // CPU filter
125        if let Some(min_cpu) = self.min_cpu {
126            if p.cpu_percent < min_cpu {
127                return false;
128            }
129        }
130
131        // Memory filter
132        if let Some(min_mem) = self.min_mem {
133            if p.memory_mb < min_mem {
134                return false;
135            }
136        }
137
138        // Status filter
139        if let Some(status) = self.status {
140            if !status.matches(p.status) {
141                return false;
142            }
143        }
144
145        // Uptime filter
146        if let Some(min_uptime) = self.min_uptime {
147            if let Some(start_time) = p.start_time {
148                let now = std::time::SystemTime::now()
149                    .duration_since(std::time::UNIX_EPOCH)
150                    .map(|d| d.as_secs())
151                    .unwrap_or(0);
152                if now.saturating_sub(start_time) < min_uptime {
153                    return false;
154                }
155            } else {
156                return false;
157            }
158        }
159
160        // Parent PID filter
161        if let Some(ppid) = self.parent {
162            if p.parent_pid != Some(ppid) {
163                return false;
164            }
165        }
166
167        true
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174    use crate::core::ProcessStatus;
175
176    fn command() -> ByCommand {
177        ByCommand {
178            name: "x".to_string(),
179            cmd: false,
180            in_dir: None,
181            min_cpu: None,
182            min_mem: None,
183            status: None,
184            min_uptime: None,
185            parent: None,
186            json: false,
187            verbose: false,
188            wide: false,
189            limit: None,
190            sort: SortKey::Cpu,
191        }
192    }
193
194    /// A process record with no working directory and no start time, which
195    /// some processes really are (a zombie has neither on Linux)
196    fn bare_process() -> Process {
197        Process {
198            pid: 1,
199            name: "x".to_string(),
200            exe_path: None,
201            cwd: None,
202            command: None,
203            argv0: None,
204            raw: None,
205            cpu_percent: 0.0,
206            memory_mb: 0.0,
207            status: ProcessStatus::Sleeping,
208            user: None,
209            parent_pid: None,
210            start_time: None,
211            uptime_secs: None,
212        }
213    }
214
215    #[test]
216    fn a_process_with_no_working_directory_fails_in() {
217        let dir = Some(PathBuf::from("/"));
218        assert!(!command().keeps(&bare_process(), &dir));
219        assert!(command().keeps(&bare_process(), &None));
220    }
221
222    #[test]
223    fn a_process_with_no_start_time_fails_min_uptime() {
224        let cmd = ByCommand {
225            min_uptime: Some(0),
226            ..command()
227        };
228        assert!(!cmd.keeps(&bare_process(), &None));
229    }
230}