diff --git a/profile.d/processes.sh b/profile.d/processes.sh index 1bda577..5cf1a98 100644 --- a/profile.d/processes.sh +++ b/profile.d/processes.sh @@ -81,12 +81,17 @@ ppg() local pattern="$*" + # pgrep reads the pattern as an extended regular expression, the fallback + # below as a plain substring. Passed through ENVIRON, not awk -v: besides + # escape handling, a pattern on awk's own command line would match the awk + # process in the ps listing. if command -v pgrep >/dev/null 2>&1; then pgrep -af -- "$pattern" return $? fi - ps -ef | awk -v pattern="$pattern" ' + ps -ef | PPG_PATTERN="$pattern" awk ' + BEGIN { pattern = ENVIRON["PPG_PATTERN"] } NR == 1 { print next @@ -196,11 +201,20 @@ ppn() return 1 fi - # -e: select all processes - # -o: specify custom output columns (PID and Command name) - # grep -w: ensures exact word matching so 'bash' doesn't match 'dbash' - # shellcheck disable=SC2009 # pgrep do not offer the -w switch - ps -eo pid,comm | grep -w "$1" + # ps prints the PID and the command name (comm, 15 characters at most, no + # arguments); awk keeps the header and the lines whose whole name equals + # the argument. grep -w was wrong: it also matched inside the PID column, + # and in names such as "kde-bash" ("-" is not a word character). + ps -eo pid,comm | PPN_NAME="$1" awk ' + BEGIN { name = ENVIRON["PPN_NAME"] } + NR == 1 { print; next } + { + line = $0 + sub(/^ *[0-9]+ +/, "", line) + if (line == name) { print; found = 1 } + } + END { exit found ? 0 : 1 } + ' } export -f ppn # ------------------------------------------------------------------------------ @@ -259,7 +273,8 @@ gpid() if command -v pgrep >/dev/null 2>&1; then result=$(pgrep -d ' ' -x -- "$proc_name") else - result=$(ps -eo pid=,comm= | awk -v proc="$proc_name" ' + result=$(ps -eo pid=,comm= | GPID_NAME="$proc_name" awk ' + BEGIN { proc = ENVIRON["GPID_NAME"] } $2 == proc { if (out != "") { out = out " " @@ -288,25 +303,60 @@ export -f gpid # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# Internal helper: take the kill-style signal arguments (-9, -KILL, -SIGHUP) +# out of an argument list. getopt rejects them as unknown options, so they must +# be removed before it runs. They are recognised anywhere before a "--" +# (digits, or upper-case names, which cannot clash with the lower-case options +# of the callers); everything after "--" is left untouched. +# Usage: _proc_split_signals [args...] +_proc_split_signals() +{ + local -n _pss_sigs="$1" _pss_rest="$2" + shift 2 + _pss_sigs=() + _pss_rest=() + local a + while [[ $# -gt 0 ]]; do + a="$1" + shift + if [[ $a == "--" ]]; then + _pss_rest+=(-- "$@") + break + fi + if [[ $a =~ ^-([0-9]+|SIG[A-Z0-9+-]+|[A-Z][A-Z0-9+-]*)$ ]]; then + _pss_sigs+=("$a") + else + _pss_rest+=("$a") + fi + done +} +export -f _proc_split_signals +# ------------------------------------------------------------------------------ + + # ------------------------------------------------------------------------------ # Kill all processes owned by the given users (kill user) -# Usage: ku +# Usage: ku [options] +# The users are validated first: nothing is killed if one of them is unknown. +# The processes of root and of the current user are never killed (it would take +# the session down), except in dry-run mode. ku() { - local PARSED local dry_run=0 - local -a signal_opt=() + local -a signal_opt=() _ku_sigs=() _ku_rest=() - PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'ku' -- "$@") + _proc_split_signals _ku_sigs _ku_rest "$@" + + local PARSED + PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'ku' -- "${_ku_rest[@]}") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"ku --help\" to display usage." return 1 fi - - local -a _pargs=() - readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0') - set -- "${_pargs[@]}" + # Restore the getopt-normalised (shell-quoted) argument list + eval set -- "$PARSED" while true; do case "$1" in -h|--help) @@ -317,7 +367,9 @@ ku() printf "\t-n, --dry-run\t\tDisplay commands without executing them\n" printf "\t-s, --signal SIG\tSignal to send (overrides KU_DEFAULT_SIGNAL)\n" printf "\t --signal=SIG\tSame as above\n" - printf "\t -SIG / -NUM\t\tSignal format compatible with kill\n" + printf "\t -SIG / -NUM\t\tSignal format compatible with kill\n\n" + printf "Users are given by name or numeric ID. The processes of root and of the\n" + printf "current user are never killed (except with --dry-run).\n" return 0 ;; -n|--dry-run) @@ -332,113 +384,134 @@ ku() shift break ;; - -*) - signal_opt=("$1") - shift - ;; *) - break - ;; - esac - done - - # Accept kill-style signal forms not handled by getopt, before usernames. - while [[ $# -gt 0 ]]; do - case "$1" in - -[0-9]*|-SIG*|-[[:alpha:]]*) - signal_opt=("$1") - shift - ;; - --) - shift - break - ;; - -*) - disp E "Unknown option: $1, use \"ku --help\" to display usage." + disp E "Invalid options, use \"ku --help\" to display usage." return 1 ;; - *) - break - ;; esac done - if [[ -z "$1" ]]; then + # A kill-style signal (-9, -KILL) applies unless -s/--signal was given + if [[ ${#signal_opt[@]} -eq 0 && ${#_ku_sigs[@]} -gt 0 ]]; then + signal_opt=("${_ku_sigs[-1]}") + fi + + if [[ $# -eq 0 ]]; then disp E "Usage: ku [options] " return 1 fi - local u + # First pass: resolve every user to a name (killall only takes names, so + # numeric IDs are translated) and check it, before anything is killed. + local u uid uname + local -a unames=() for u in "$@"; do - if ! id "$u" >/dev/null 2>&1; then + if ! uid=$(id -u -- "$u" 2>/dev/null); then disp E "User '$u' does not exist." return 1 + fi + uname=$(id -un -- "$u") + if (( ! dry_run )) && [[ $uid -eq 0 || $uid -eq $EUID ]]; then + disp E "Safety abort: refusing to kill the processes of '$uname' (root or current user)." + return 1 + fi + unames+=("$uname") + done + + # Second pass: kill. The failure of one user does not skip the others. + local rc=0 + local -a cmd + for uname in "${unames[@]}"; do + # killall (psmisc) preferred; fall back to pkill (procps-ng). + if command -v killall >/dev/null 2>&1; then + cmd=(killall) + if [[ ${#signal_opt[@]} -gt 0 ]]; then + cmd+=("${signal_opt[@]}") + elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then + cmd+=("-${KU_DEFAULT_SIGNAL}") + fi + cmd+=(-u "$uname") + elif command -v pkill >/dev/null 2>&1; then + cmd=(pkill) + # Translate killall's -s SIGNAME form to pkill's -SIGNAME form. + if [[ ${#signal_opt[@]} -eq 2 && "${signal_opt[0]}" == "-s" ]]; then + cmd+=("-${signal_opt[1]}") + elif [[ ${#signal_opt[@]} -gt 0 ]]; then + cmd+=("${signal_opt[@]}") + elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then + cmd+=("-${KU_DEFAULT_SIGNAL}") + fi + cmd+=(-u "$uname") else - local cmd + disp E "ku: neither 'killall' (psmisc) nor 'pkill' (procps) is available." + return 1 + fi - # killall (psmisc) preferred; fall back to pkill (procps-ng). - if command -v killall >/dev/null 2>&1; then - cmd=(killall) - if [[ ${#signal_opt[@]} -gt 0 ]]; then - cmd+=("${signal_opt[@]}") - elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then - cmd+=("-${KU_DEFAULT_SIGNAL}") - fi - cmd+=(-u "$u") - elif command -v pkill >/dev/null 2>&1; then - cmd=(pkill) - # Translate killall's -s SIGNAME form to pkill's -SIGNAME form. - if [[ ${#signal_opt[@]} -eq 2 && "${signal_opt[0]}" == "-s" ]]; then - cmd+=("-${signal_opt[1]}") - elif [[ ${#signal_opt[@]} -gt 0 ]]; then - cmd+=("${signal_opt[@]}") - elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then - cmd+=("-${KU_DEFAULT_SIGNAL}") - fi - cmd+=(-u "$u") - else - disp E "ku: neither 'killall' (psmisc) nor 'pkill' (procps) is available." - return 1 - fi - - if (( dry_run )); then - printf "DRY-RUN: " - printf "%q " "${cmd[@]}" - printf "\n" - else - "${cmd[@]}" - fi + if (( dry_run )); then + printf "DRY-RUN: " + printf "%q " "${cmd[@]}" + printf "\n" + else + "${cmd[@]}" || rc=1 fi done + return "$rc" } export -f ku # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ +# Internal helper of kt: send a signal to a process and, first, to all its +# descendants (depth first, so a parent never outlives the children it could +# respawn from). A failure does not stop the others; the status is non-zero if +# any kill failed. +# Usage: _kt_tree [kill_args...] +_kt_tree() +{ + local dry="$1" pid="$2" child rc=0 + shift 2 + for child in $(pgrep -P "$pid" 2>/dev/null); do + _kt_tree "$dry" "$child" "$@" || rc=1 + done + + if (( dry )); then + printf "DRY-RUN: " + printf "%q " kill "$@" "$pid" + printf "\n" + else + kill "$@" "$pid" || rc=1 + fi + return "$rc" +} +export -f _kt_tree +# ------------------------------------------------------------------------------ + + # ------------------------------------------------------------------------------ # Kill all children of a process then the process (kill tree) -# Usage: kt [kill_options] +# Usage: kt [options] kt() { - local PARSED local dry_run=0 - local -a pre_kill_opts=() + local -a kill_args=() _kt_sigs=() _kt_rest=() - PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'kt' -- "$@") + _proc_split_signals _kt_sigs _kt_rest "$@" + + local PARSED + PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'kt' -- "${_kt_rest[@]}") # shellcheck disable=SC2181 # getopt return code is checked immediately after if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"kt --help\" to display usage." return 1 fi - - local -a _pargs=() - readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0') - set -- "${_pargs[@]}" + # Restore the getopt-normalised (shell-quoted) argument list + eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "kt: Kill all children of a process then the process (kill tree).\n\n" - printf "Usage: kt [options] [kill_options]\n\n" + printf "Usage: kt [options] \n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-n, --dry-run\t\tDisplay kill commands without executing them\n" @@ -452,79 +525,53 @@ kt() shift ;; -s|--signal) - pre_kill_opts+=(-s "$2") + kill_args=(-s "$2") shift 2 ;; --) shift break ;; - -*) - pre_kill_opts+=("$1") - shift - ;; *) - break - ;; - esac - done - - # Accept kill-style signal forms not handled by getopt, before the PID. - while [[ $# -gt 0 ]]; do - case "$1" in - -[0-9]*|-SIG*|-[[:alpha:]]*) - pre_kill_opts+=("$1") - shift - ;; - --) - shift - break - ;; - -*) - disp E "Unknown option: $1, use \"kt --help\" to display usage." + disp E "Invalid options, use \"kt --help\" to display usage." return 1 ;; - *) - break - ;; esac done - if [[ -z "$1" ]]; then - disp E "Usage: kt [options] [kill_options]" + # A kill-style signal (-9, -KILL) applies unless -s/--signal was given + if [[ ${#kill_args[@]} -eq 0 && ${#_kt_sigs[@]} -gt 0 ]]; then + kill_args=("${_kt_sigs[-1]}") + fi + + if [[ $# -eq 0 ]]; then + disp E "Usage: kt [options] " + return 1 + fi + if [[ $# -gt 1 ]]; then + disp E "Unexpected argument: $2. Usage: kt [options] " return 1 fi local parent_pid="$1" - shift - - local -a kill_opts=("${pre_kill_opts[@]}" "$@") - + if [[ ! $parent_pid =~ ^[0-9]+$ ]]; then + disp E "Invalid PID: '$parent_pid'." + return 1 + fi if [[ "$parent_pid" == "0" || "$parent_pid" == "1" ]]; then disp E "Safety abort: Refusing to kill PID $parent_pid (system critical)." return 1 fi - - local children_pids - children_pids=$(pgrep -P "$parent_pid" 2>/dev/null || true) - - local pid - for pid in $children_pids; do - if (( dry_run )); then - kt --dry-run "$pid" "${kill_opts[@]}" || break - else - kt "$pid" "${kill_opts[@]}" || break - fi - done - - if (( dry_run )); then - local cmd=(kill "${kill_opts[@]}" "$parent_pid") - printf "DRY-RUN: " - printf "%q " "${cmd[@]}" - printf "\n" - else - kill "${kill_opts[@]}" "$parent_pid" + if [[ "$parent_pid" == "$$" || "$parent_pid" == "$BASHPID" ]]; then + disp E "Safety abort: Refusing to kill the current shell (PID $parent_pid)." + return 1 fi + if ! command -v pgrep >/dev/null 2>&1; then + disp E "kt: 'pgrep' (procps) is required to find the children." + return 1 + fi + + _kt_tree "$dry_run" "$parent_pid" "${kill_args[@]}" } export -f kt # ------------------------------------------------------------------------------