use crate::core::{
resolve_in_dir, sort_processes, with_ports, NameMatch, PortInfo, Process, SortKey, StatusFilter,
};
use crate::error::{ProcError, Result};
use crate::out_println;
use crate::ui::{format_duration, format_memory, Printer};
use clap::Args;
use colored::*;
use serde::Serialize;
use std::collections::HashSet;
use std::path::PathBuf;
#[derive(Args, Debug)]
pub struct ForCommand {
pub file: String,
#[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
pub in_dir: Option<String>,
#[arg(long = "by", short = 'b')]
pub by_name: Option<String>,
#[arg(long)]
pub cmd: bool,
#[arg(long)]
pub min_cpu: Option<f32>,
#[arg(long)]
pub min_mem: Option<f64>,
#[arg(long, value_enum, ignore_case = true)]
pub status: Option<StatusFilter>,
#[arg(long)]
pub min_uptime: Option<u64>,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long, short = 'v')]
pub verbose: bool,
#[arg(long, short = 'w')]
pub wide: bool,
#[arg(long, short = 's', value_enum, default_value_t = SortKey::Cpu)]
pub sort: SortKey,
#[arg(long, short = 'n')]
pub limit: Option<usize>,
}
impl ForCommand {
pub fn execute(&self) -> Result<()> {
let file_path = self.resolve_path(&self.file)?;
let exe_processes = Process::find_by_exe_path(&file_path)?;
let open_file_procs = Process::find_by_open_file(&file_path)?;
let mut seen_pids = HashSet::new();
let mut processes: Vec<Process> = Vec::new();
for proc in exe_processes.into_iter().chain(open_file_procs) {
if seen_pids.insert(proc.pid) {
processes.push(proc);
}
}
Process::measure_cpu(&mut processes);
self.apply_filters(&mut processes)?;
sort_processes(&mut processes, self.sort);
if let Some(limit) = self.limit {
processes.truncate(limit);
}
if processes.is_empty() {
return Err(ProcError::ProcessNotFound(self.file.clone()));
}
let results = with_ports(processes)?;
if self.json {
self.print_json(&results)?;
} else {
self.print_human(&results);
}
Ok(())
}
fn resolve_path(&self, path: &str) -> Result<PathBuf> {
let expanded = if let Some(stripped) = path.strip_prefix("~/") {
if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(stripped)
} else {
PathBuf::from(path)
}
} else if path == "~" {
PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
} else {
PathBuf::from(path)
};
let absolute = if expanded.is_relative() {
crate::core::filters::current_dir_for(&expanded)?.join(expanded)
} else {
expanded
};
let canonical = absolute
.canonicalize()
.map_err(|_| ProcError::InvalidInput(format!("File not found: {}", path)))?;
#[cfg(windows)]
let canonical = PathBuf::from(crate::core::filters::strip_verbatim_prefix(
&canonical.to_string_lossy(),
));
Ok(canonical)
}
fn apply_filters(&self, processes: &mut Vec<Process>) -> Result<()> {
let in_dir_filter = resolve_in_dir(&self.in_dir)?;
let in_dir_has_any = in_dir_filter.as_ref().is_some_and(|dir_path| {
processes
.iter()
.any(|p| crate::core::filters::matches_in_filter(p, dir_path))
});
crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
processes.retain(|p| {
if let Some(ref dir_path) = in_dir_filter {
if !crate::core::filters::matches_in_filter(p, dir_path) {
return false;
}
}
if let Some(ref name) = self.by_name {
if !crate::core::matches_by_filter(p, name, NameMatch::from_flag(self.cmd)) {
return false;
}
}
if let Some(min_cpu) = self.min_cpu {
if p.cpu_percent < min_cpu {
return false;
}
}
if let Some(min_mem) = self.min_mem {
if p.memory_mb < min_mem {
return false;
}
}
if let Some(status) = self.status {
if !status.matches(p.status) {
return false;
}
}
if let Some(min_uptime) = self.min_uptime {
if let Some(start_time) = p.start_time {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
if now.saturating_sub(start_time) < min_uptime {
return false;
}
} else {
return false;
}
}
true
});
Ok(())
}
fn print_human(&self, results: &[(Process, Vec<PortInfo>)]) {
let count = results.len();
let file_display = &self.file;
if count == 1 {
let (proc, ports) = &results[0];
out_println!(
"{} Found 1 process for {}",
"✓".green().bold(),
file_display.cyan().bold()
);
out_println!();
out_println!(
" {} {} (PID {})",
"Process:".bright_black(),
proc.name.white().bold(),
proc.pid.to_string().cyan()
);
if let Some(ref cwd) = proc.cwd {
out_println!(" {} {}", "Directory:".bright_black(), cwd);
}
if let Some(ref path) = proc.exe_path {
out_println!(" {} {}", "Path:".bright_black(), path.bright_black());
}
for port_info in ports {
let addr = port_info.address.as_deref().unwrap_or("*");
out_println!(
" {} :{} ({} on {})",
"Listening:".bright_black(),
port_info.port.to_string().cyan(),
format!("{:?}", port_info.protocol).to_uppercase(),
addr
);
}
out_println!(
" {} {:.1}% CPU, {}",
"Resources:".bright_black(),
proc.cpu_percent,
format_memory(proc.memory_mb)
);
if let Some(uptime) = proc.uptime_secs {
out_println!(" {} {}", "Uptime:".bright_black(), format_duration(uptime));
}
if self.verbose {
if let Some(ref cmd) = proc.command {
out_println!(" {} {}", "Command:".bright_black(), cmd.bright_black());
}
}
out_println!();
} else {
let processes: Vec<Process> = results.iter().map(|(p, _)| p.clone()).collect();
Printer::from_flags(false, self.verbose)
.wide(self.wide)
.print_processes_as("for", &processes, Some(&format!("for {}", file_display)));
let listening: Vec<_> = results
.iter()
.flat_map(|(proc, ports)| ports.iter().map(move |p| (proc, p)))
.collect();
if !listening.is_empty() {
out_println!(" {}", "Listening:".bright_black());
for (proc, port_info) in listening {
let addr = port_info.address.as_deref().unwrap_or("*");
out_println!(
" {} :{} ({} on {}) {} [PID {}]",
"→".bright_black(),
port_info.port.to_string().cyan(),
format!("{:?}", port_info.protocol).to_uppercase(),
addr,
proc.name.white(),
proc.pid.to_string().cyan()
);
}
out_println!();
}
}
}
fn print_json(&self, results: &[(Process, Vec<PortInfo>)]) -> Result<()> {
let printer = Printer::from_flags(true, self.verbose);
let items: Vec<ProcessForJson> = results
.iter()
.map(|(proc, ports)| ProcessForJson {
process: proc,
ports,
})
.collect();
printer.print_json(&ForOutput {
action: "for",
success: true,
count: items.len(),
results: &items,
});
Ok(())
}
}
#[derive(Serialize)]
struct ProcessForJson<'a> {
process: &'a Process,
ports: &'a [PortInfo],
}
#[derive(Serialize)]
struct ForOutput<'a> {
action: &'static str,
success: bool,
count: usize,
results: &'a [ProcessForJson<'a>],
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ProcessStatus;
fn command() -> ForCommand {
ForCommand {
file: "x".to_string(),
in_dir: None,
by_name: None,
cmd: false,
min_cpu: None,
min_mem: None,
status: None,
min_uptime: None,
json: false,
verbose: false,
wide: false,
sort: SortKey::Cpu,
limit: None,
}
}
fn bare_process() -> Process {
Process {
pid: 1,
name: "x".to_string(),
exe_path: None,
cwd: None,
command: None,
argv0: None,
raw: None,
cpu_percent: 0.0,
memory_mb: 0.0,
status: ProcessStatus::Sleeping,
user: None,
parent_pid: None,
start_time: None,
uptime_secs: None,
}
}
#[test]
fn a_process_with_no_working_directory_fails_in() {
let cmd = ForCommand {
in_dir: Some("/".to_string()),
..command()
};
let mut processes = vec![bare_process()];
cmd.apply_filters(&mut processes).unwrap();
assert!(processes.is_empty());
let mut processes = vec![bare_process()];
command().apply_filters(&mut processes).unwrap();
assert_eq!(processes.len(), 1);
}
#[test]
fn a_process_with_no_start_time_fails_min_uptime() {
let cmd = ForCommand {
min_uptime: Some(0),
..command()
};
let mut processes = vec![bare_process()];
cmd.apply_filters(&mut processes).unwrap();
assert!(processes.is_empty());
}
}