#!/usr/bin/env bash ######################################################################################### #### process-top.sh — Show top CPU/memory consumers, zombies, and long-running procs #### #### Provides a quick snapshot of process health and resource usage #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### ./process-top.sh #### #### ./process-top.sh --top 20 --section cpu,zombies #### #### ./process-top.sh --min-runtime 48 #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── SECTIONS="${SECTIONS:-all}" VERBOSE="${VERBOSE:-false}" COLOR="${COLOR:-auto}" TOP_N="${TOP_N:-10}" MIN_RUNTIME="${MIN_RUNTIME:-24}" # ── State ───────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME # ── Colors ──────────────────────────────────────────────────────────── setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" return fi if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" fi } # ── Logging ─────────────────────────────────────────────────────────── log() { echo -e "${CYAN}[INFO]${RESET} $*"; } verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; } # ── Helpers ─────────────────────────────────────────────────────────── section_header() { echo "" echo -e " ${BOLD}${CYAN}── $1 ──${RESET}" echo "" } field() { printf " ${BOLD}%-22s${RESET} %s\n" "$1" "$2" } field_color() { printf " ${BOLD}%-22s${RESET} %b\n" "$1" "$2" } should_show() { [[ "$SECTIONS" == "all" ]] || [[ ",$SECTIONS," == *",$1,"* ]] } value_color() { local value="$1" local int_val int_val=$(printf "%.0f" "$value" 2>/dev/null || echo "0") if [[ "$int_val" -ge 80 ]]; then echo "$YELLOW" else echo "$GREEN" fi } # ══════════════════════════════════════════════════════════════════════ # TOP CPU # ══════════════════════════════════════════════════════════════════════ show_cpu() { section_header "Top ${TOP_N} by CPU Usage" printf " ${BOLD}%-8s %-12s %6s %6s %10s %s${RESET}\n" "PID" "USER" "CPU%" "MEM%" "TIME" "COMMAND" printf " %s\n" "$(printf '%.0s─' {1..72})" ps -eo pid,user,pcpu,pmem,etime,comm --sort=-pcpu --no-headers 2>/dev/null | head -n "$TOP_N" | while IFS= read -r line; do local pid user cpu mem etime comm pid=$(echo "$line" | awk '{print $1}') user=$(echo "$line" | awk '{print $2}') cpu=$(echo "$line" | awk '{print $3}') mem=$(echo "$line" | awk '{print $4}') etime=$(echo "$line" | awk '{print $5}') comm=$(echo "$line" | awk '{print $6}') local cc mc cc=$(value_color "$cpu") mc=$(value_color "$mem") printf " %-8s %-12s %b%5s%%%b %b%5s%%%b %10s %s\n" \ "$pid" "$user" "$cc" "$cpu" "$RESET" "$mc" "$mem" "$RESET" "$etime" "$comm" done } # ══════════════════════════════════════════════════════════════════════ # TOP MEMORY # ══════════════════════════════════════════════════════════════════════ show_memory() { section_header "Top ${TOP_N} by Memory Usage" printf " ${BOLD}%-8s %-12s %6s %6s %10s %s${RESET}\n" "PID" "USER" "CPU%" "MEM%" "TIME" "COMMAND" printf " %s\n" "$(printf '%.0s─' {1..72})" ps -eo pid,user,pcpu,pmem,etime,comm --sort=-pmem --no-headers 2>/dev/null | head -n "$TOP_N" | while IFS= read -r line; do local pid user cpu mem etime comm pid=$(echo "$line" | awk '{print $1}') user=$(echo "$line" | awk '{print $2}') cpu=$(echo "$line" | awk '{print $3}') mem=$(echo "$line" | awk '{print $4}') etime=$(echo "$line" | awk '{print $5}') comm=$(echo "$line" | awk '{print $6}') local cc mc cc=$(value_color "$cpu") mc=$(value_color "$mem") printf " %-8s %-12s %b%5s%%%b %b%5s%%%b %10s %s\n" \ "$pid" "$user" "$cc" "$cpu" "$RESET" "$mc" "$mem" "$RESET" "$etime" "$comm" done } # ══════════════════════════════════════════════════════════════════════ # ZOMBIES # ══════════════════════════════════════════════════════════════════════ show_zombies() { section_header "Zombie Processes" local zombie_list zombie_list=$(ps -eo pid,ppid,user,stat,comm --no-headers 2>/dev/null | awk '$4 ~ /^Z/') if [[ -z "$zombie_list" ]]; then echo -e " ${GREEN}No zombie processes found${RESET}" return fi printf " ${BOLD}%-8s %-8s %-12s %-6s %s${RESET}\n" "PID" "PPID" "USER" "STAT" "COMMAND" printf " %s\n" "$(printf '%.0s─' {1..55})" echo "$zombie_list" | while IFS= read -r line; do local pid ppid user stat comm pid=$(echo "$line" | awk '{print $1}') ppid=$(echo "$line" | awk '{print $2}') user=$(echo "$line" | awk '{print $3}') stat=$(echo "$line" | awk '{print $4}') comm=$(echo "$line" | awk '{print $5}') printf " ${RED}%-8s${RESET} %-8s %-12s ${RED}%-6s${RESET} %s\n" \ "$pid" "$ppid" "$user" "$stat" "$comm" done } # ══════════════════════════════════════════════════════════════════════ # LONG-RUNNING # ══════════════════════════════════════════════════════════════════════ etime_to_hours() { local etime="$1" local days=0 hours=0 mins=0 if [[ "$etime" == *-* ]]; then days=${etime%%-*} etime=${etime#*-} fi local parts IFS=: read -ra parts <<< "$etime" local num_parts=${#parts[@]} if [[ "$num_parts" -eq 3 ]]; then hours=${parts[0]} mins=${parts[1]} elif [[ "$num_parts" -eq 2 ]]; then hours=${parts[0]} mins=${parts[1]} elif [[ "$num_parts" -eq 1 ]]; then mins=${parts[0]} fi # Remove leading zeros days=$((10#$days)) hours=$((10#$hours)) mins=$((10#$mins)) echo $(( days * 24 + hours + (mins > 30 ? 1 : 0) )) } show_long_running() { section_header "Long-Running Processes (> ${MIN_RUNTIME}h)" printf " ${BOLD}%-8s %-12s %10s %6s %s${RESET}\n" "PID" "USER" "ELAPSED" "MEM%" "COMMAND" printf " %s\n" "$(printf '%.0s─' {1..62})" local found=0 ps -eo pid,user,etime,pmem,comm --no-headers 2>/dev/null | while IFS= read -r line; do local pid user etime mem comm pid=$(echo "$line" | awk '{print $1}') user=$(echo "$line" | awk '{print $2}') etime=$(echo "$line" | awk '{print $3}') mem=$(echo "$line" | awk '{print $4}') comm=$(echo "$line" | awk '{print $5}') local run_hours run_hours=$(etime_to_hours "$etime") if [[ "$run_hours" -ge "$MIN_RUNTIME" ]]; then printf " %-8s %-12s %10s %5s%% %s\n" "$pid" "$user" "$etime" "$mem" "$comm" found=1 fi done if [[ "$found" -eq 0 ]]; then echo -e " ${GREEN}No processes running longer than ${MIN_RUNTIME} hours${RESET}" fi } # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════ print_summary() { echo "" echo -e " ${BOLD}══════════════════════════════════════════${RESET}" echo -e " ${BOLD}Process Summary${RESET}" echo -e " ${BOLD}══════════════════════════════════════════${RESET}" local total_procs zombie_count long_count total_procs=$(ps -e --no-headers 2>/dev/null | wc -l) zombie_count=$(ps -eo stat --no-headers 2>/dev/null | awk '/^Z/{c++} END{print c+0}') long_count=$(ps -eo etime --no-headers 2>/dev/null | while IFS= read -r etime; do etime=$(echo "$etime" | xargs) local h h=$(etime_to_hours "$etime") if [[ "$h" -ge "$MIN_RUNTIME" ]]; then echo "1" fi done | wc -l) field "Total processes:" "$total_procs" if [[ "$zombie_count" -gt 0 ]]; then field_color "Zombie processes:" "${RED}${zombie_count}${RESET}" else field_color "Zombie processes:" "${GREEN}0${RESET}" fi field "Long-running (>${MIN_RUNTIME}h):" "$long_count" echo "" } # ══════════════════════════════════════════════════════════════════════ # USAGE # ══════════════════════════════════════════════════════════════════════ usage() { cat <&2 echo "Run ${SCRIPT_NAME} --help for usage" >&2 exit 1 ;; esac done } # ══════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════ main() { parse_args "$@" setup_colors echo "" echo -e "${BOLD}Process Monitor — $(hostname -f 2>/dev/null || hostname)${RESET}" echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}" should_show "cpu" && show_cpu should_show "memory" && show_memory should_show "zombies" && show_zombies should_show "long-running" && show_long_running print_summary } main "$@"