Skip to main content

procutils_skill/
lib.rs

1use clap::Parser;
2use procutils_common::{
3    MAX_TERM_WIDTH,
4    man::ManContent,
5    procmatch::{MatchOptions, find_matching_processes},
6    signal::{all_signals, parse_signum_any},
7};
8use std::process::ExitCode;
9
10pub const MAN: ManContent = ManContent {
11    description: Some(include_str!("../man/description.man")),
12    extra_sections: &[
13        ("EXAMPLES", include_str!("../man/examples.man")),
14        ("NOTES", include_str!("../man/notes.man")),
15        ("DIVERGENCES", include_str!("../man/divergences.man")),
16        ("SEE ALSO", include_str!("../man/see_also.man")),
17    ],
18};
19
20/// Lift the first `-SIG` / `-N` / `-NAME` argument out of `argv` and
21/// rewrite it as `--signal SIG`, so the rest of the args parse normally
22/// under clap. The signal can appear anywhere on the command line.
23/// `argv[0]` is preserved untouched.
24pub fn preprocess_argv(argv: Vec<String>) -> Vec<String> {
25    let mut out = Vec::with_capacity(argv.len() + 1);
26    let mut iter = argv.into_iter();
27    if let Some(arg0) = iter.next() {
28        out.push(arg0);
29    }
30    let mut found = false;
31    for arg in iter {
32        if !found
33            && arg.starts_with('-')
34            && !arg.starts_with("--")
35            && arg.len() > 1
36            && parse_signum_any(&arg[1..]).is_some()
37        {
38            out.push("--signal".to_string());
39            out.push(arg[1..].to_string());
40            found = true;
41        } else {
42            out.push(arg);
43        }
44    }
45    out
46}
47
48/// Send a signal to processes selected by user, tty, pid, or command.
49///
50/// `skill` is obsolete; prefer `pkill` for new use cases. The signal
51/// may be given as `--signal SIG` or as a short `-SIG` flag at any
52/// position; names with or without the `SIG` prefix and signal
53/// numbers are all accepted.
54#[derive(Parser)]
55#[command(
56    name = "skill",
57    version,
58    about,
59    disable_help_flag = true,
60    max_term_width = MAX_TERM_WIDTH,
61    override_usage = "skill [signal] [options] <expression>"
62)]
63pub struct Args {
64    /// Print help.
65    #[arg(long, action = clap::ArgAction::HelpLong)]
66    help: Option<bool>,
67
68    /// Signal to send (name or number). Default: TERM.
69    #[arg(long)]
70    signal: Option<String>,
71
72    /// List signal names.
73    #[arg(short = 'l', long)]
74    list: bool,
75
76    /// Print signal table (number + name).
77    #[arg(short = 'L', long)]
78    table: bool,
79
80    /// No-op mode: print matching PIDs instead of signaling.
81    #[arg(short = 'n', long = "no-action")]
82    no_action: bool,
83
84    /// Verbose: explain what is being done.
85    #[arg(short, long)]
86    verbose: bool,
87
88    /// Match processes by tty (repeatable).
89    #[arg(short = 't', long, value_name = "TTY")]
90    tty: Vec<String>,
91
92    /// Match processes by user name or UID (repeatable).
93    #[arg(short = 'u', long, value_name = "USER")]
94    user: Vec<String>,
95
96    /// Match processes by PID (repeatable).
97    #[arg(short = 'p', long, value_name = "PID")]
98    pid: Vec<i32>,
99
100    /// Match processes by command name (repeatable, exact match).
101    #[arg(short = 'c', long, value_name = "COMMAND")]
102    command: Vec<String>,
103
104    /// Free-form bare PIDs.
105    expr: Vec<String>,
106}
107
108pub fn run(args: Args) -> ExitCode {
109    if args.list {
110        let names: Vec<String> = all_signals().map(|(_, n)| n).collect();
111        println!("{}", names.join(" "));
112        return ExitCode::SUCCESS;
113    }
114
115    if args.table {
116        let mut line = String::new();
117        for (i, (num, name)) in all_signals().enumerate() {
118            line.push_str(&format!("{num:>2} {name}"));
119            if (i + 1) % 8 == 0 {
120                println!("{line}");
121                line.clear();
122            } else {
123                line.push('\t');
124            }
125        }
126        if !line.is_empty() {
127            println!("{}", line.trim_end());
128        }
129        return ExitCode::SUCCESS;
130    }
131
132    // Resolve the signal: --signal wins, otherwise default to TERM.
133    let signum: i32 = match args.signal.as_deref() {
134        Some(s) => match parse_signum_any(s) {
135            Some(n) => n,
136            None => {
137                eprintln!("skill: unknown signal: {s}");
138                return ExitCode::from(2);
139            }
140        },
141        None => libc::SIGTERM,
142    };
143
144    // Bare expression args become extra PID filters.
145    let mut pid_filter: Vec<i32> = args.pid.clone();
146    for s in &args.expr {
147        match s.parse::<i32>() {
148            Ok(p) => pid_filter.push(p),
149            Err(_) => {
150                eprintln!("skill: bare expression argument must be a PID: {s}");
151                return ExitCode::from(2);
152            }
153        }
154    }
155
156    let no_filter = pid_filter.is_empty()
157        && args.tty.is_empty()
158        && args.user.is_empty()
159        && args.command.is_empty();
160    if no_filter {
161        eprintln!("skill: no expression given");
162        eprintln!("Usage: skill [signal] [options] <expression>");
163        return ExitCode::from(2);
164    }
165
166    // Build a regex pattern from -c COMMAND values: matched as exact
167    // process names, OR'd together.
168    let pattern = if args.command.is_empty() {
169        String::new()
170    } else {
171        let alts: Vec<String> =
172            args.command.iter().map(|c| regex::escape(c)).collect();
173        format!("^({})$", alts.join("|"))
174    };
175
176    let opts = MatchOptions {
177        pattern,
178        full: false,
179        ignore_case: false,
180        exact: false,
181        inverse: false,
182        newest: false,
183        oldest: false,
184        older: None,
185        pid: if pid_filter.is_empty() {
186            None
187        } else {
188            Some(pid_filter)
189        },
190        parent: None,
191        pgroup: None,
192        group: None,
193        session: None,
194        terminal: if args.tty.is_empty() {
195            None
196        } else {
197            Some(args.tty.clone())
198        },
199        euid: if args.user.is_empty() {
200            None
201        } else {
202            Some(args.user.clone())
203        },
204        uid: None,
205        runstates: None,
206        env: None,
207    };
208
209    let matches = match find_matching_processes(&opts, "skill") {
210        Ok(m) => m,
211        Err(code) => return code,
212    };
213
214    if matches.is_empty() {
215        return ExitCode::from(1);
216    }
217
218    if args.no_action {
219        for p in &matches {
220            println!("{}", p.pid);
221        }
222        return ExitCode::SUCCESS;
223    }
224
225    let mut any_failed = false;
226    for p in &matches {
227        let rc = unsafe { libc::kill(p.pid as libc::pid_t, signum) };
228        if rc == 0 {
229            if args.verbose {
230                println!("{}: {}", p.pid, p.comm);
231            }
232        } else {
233            let errno = std::io::Error::last_os_error();
234            eprintln!("skill: ({}) - {errno}", p.pid);
235            any_failed = true;
236        }
237    }
238
239    if any_failed {
240        ExitCode::FAILURE
241    } else {
242        ExitCode::SUCCESS
243    }
244}