procutils-pkill 0.1.0-alpha.1

Signal processes based on name and other attributes
Documentation
use clap::Parser;
use procutils_common::{
    procmatch::{MatchOptions, ProcessInfo},
    signal::parse_signal,
};
use rustix::process::{Pid, Signal};
use std::process::ExitCode;

/// Signal processes based on name and other attributes.
#[derive(Parser)]
#[command(name = "pkill", version, about)]
pub struct Args {
    /// Signal to send (name or number).
    #[arg(long, default_value = "TERM")]
    signal: String,

    /// Suppress normal output; instead print a count of matching processes.
    #[arg(short, long)]
    count: bool,

    /// Display name and PID of the process being killed.
    #[arg(short, long)]
    echo: bool,

    /// Match against the full command line instead of just the process name.
    #[arg(short, long)]
    full: bool,

    /// Match processes case-insensitively.
    #[arg(short, long)]
    ignore_case: bool,

    /// Select only the newest (most recently started) of the matching processes.
    #[arg(short, long)]
    newest: bool,

    /// Select only the oldest (least recently started) of the matching processes.
    #[arg(short, long)]
    oldest: bool,

    /// Select processes older than the given number of seconds.
    #[arg(short = 'O', long)]
    older: Option<f64>,

    /// Only match processes whose parent process ID is listed.
    #[arg(short = 'P', long, value_delimiter = ',')]
    parent: Option<Vec<i32>>,

    /// Only match processes in the process group IDs listed.
    #[arg(short = 'g', long = "pgroup", value_delimiter = ',')]
    pgroup: Option<Vec<i32>>,

    /// Only match processes whose real group ID is listed.
    #[arg(short = 'G', long = "group", value_delimiter = ',')]
    group: Option<Vec<u32>>,

    /// Only match processes whose process session ID is listed.
    #[arg(short = 's', long, value_delimiter = ',')]
    session: Option<Vec<i32>>,

    /// Only match processes whose controlling terminal is listed.
    #[arg(short = 't', long = "terminal", value_delimiter = ',')]
    terminal: Option<Vec<String>>,

    /// Only match processes whose effective user ID is listed.
    #[arg(short = 'u', long = "euid", value_delimiter = ',')]
    euid: Option<Vec<String>>,

    /// Only match processes whose real user ID is listed.
    #[arg(short = 'U', long = "uid", value_delimiter = ',')]
    uid: Option<Vec<String>>,

    /// Only match processes whose names (or command lines if -f) exactly match the pattern.
    #[arg(short = 'x', long)]
    exact: bool,

    /// Match only processes which match the process state.
    #[arg(short = 'r', long, value_delimiter = ',')]
    runstates: Option<Vec<char>>,

    /// Extended regular expression pattern.
    pattern: Option<String>,
}

fn send_signal(proc: &ProcessInfo, signal: Signal) -> bool {
    let Some(pid) = Pid::from_raw(proc.pid) else {
        return false;
    };
    rustix::process::kill_process(pid, signal).is_ok()
}

pub fn run(args: Args) -> ExitCode {
    let signal = match parse_signal(&args.signal) {
        Some(s) => s,
        None => {
            eprintln!("pkill: unknown signal: {}", args.signal);
            return ExitCode::from(2);
        }
    };

    let opts = MatchOptions {
        pattern: args.pattern.clone().unwrap_or_default(),
        full: args.full,
        ignore_case: args.ignore_case,
        pid: None,
        exact: args.exact,
        inverse: false,
        newest: args.newest,
        oldest: args.oldest,
        older: args.older,
        parent: args.parent,
        pgroup: args.pgroup,
        group: args.group,
        session: args.session,
        terminal: args.terminal,
        euid: args.euid,
        uid: args.uid,
        runstates: args.runstates,
    };

    if args.pattern.is_none() && !opts.has_filter() {
        eprintln!("pkill: pattern is required");
        return ExitCode::from(2);
    }

    let matches = match procutils_common::procmatch::find_matching_processes(
        &opts, "pkill",
    ) {
        Ok(m) => m,
        Err(code) => return code,
    };

    if matches.is_empty() {
        return ExitCode::from(1);
    }

    if args.count {
        println!("{}", matches.len());
        return ExitCode::SUCCESS;
    }

    let mut any_failed = false;
    for proc in &matches {
        if send_signal(proc, signal) {
            if args.echo {
                println!("{} killed (pid {})", proc.comm, proc.pid);
            }
        } else {
            any_failed = true;
        }
    }

    if any_failed {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}