В статье я рассказываю, как из набора команд (
free,df,ip,ps,uptime) собрать CLI‑утилиту на Bash, добавить аргументы, цветовую индикацию и упаковать всё в AUR как пакет system-monitor
Я хотел посмотреть, насколько далеко я смогу продвинуть чистый Bash, прежде чем он рухнет под собственным синтаксисом. Поэтому, естественно, я решил написать инструмент мониторинга системы — на Bash.
Идея
Я хотел сделать скрипт, который показывает:
Загрузку процессора и количество ядер;
Использование оперативной памяти и процент;
Дисковое пространство для
/;Сетевой трафик (RX/TX);
Количество процессов.
По сути, «набор инструментов ленивого администратора Linux» в одном файле.
./system-monitor.sh ==== SYSTEM MONITOR ==== Time: Вт 30 дек 2025 18:31:05 MSK Uptime: 5 days, 1 hour, 12 minutes ==== CPU LOAD ==== CPU Cores: 4 Load Average: 1min: 2.91 | 5min: 1.89 | 15min: 1.50 System Load: 72% ==== MEMORY USAGE ==== Total: 7.44 GB Used: 4.10 GB Available: 3.34 GB Usage: 55.1% ==== DISK SPACE ==== Filesystem: /dev/sda1 (/) Used: 159.3 GB Available: 138.2 GB Usage: 54% ==== NETWORK STATISTICS ==== Interface: wlan0 Received: 8943.44 MB Transmitted: 1185.45 MB ==== PROCESSES ==== Total Processes: 281
И так мы получаем краткую сводку о системе, которая представляет перед собой комбинацию обычных команд для диагностики системы: ip, df, free,top, uptime.
Особенности
Я решил сделать не просто скрипт, который просто выводит текст на терминале. В тот скрипт было решено сдeлать следующее:
Аргументы командной строки (--help, --brief, --no-color, --version, -i);
Цветной вывод (для предупреждений);
Режим «непрерывного» мониторинга;
Безопасное завершение скрипта (Ctrl+C);
Проверки зависимостей.
Как это устроено внутри
CPU: Работаем с Load Average
Для процессора я решил не вычислять мгновенную загрузку (которая скачет каждую секунду), а брать средние значения из /proc/loadavg.
# --- CPU Metrics Collection --- get_cpu_metrics() { local loadavg loadavg=$(cat /proc/loadavg 2>/dev/null) if [ -n "$loadavg" ]; then CPU_LOAD_1MIN=$(echo "$loadavg" | awk '{print $1}') CPU_LOAD_5MIN=$(echo "$loadavg" | awk '{print $2}') CPU_LOAD_15MIN=$(echo "$loadavg" | awk '{print $3}') CPU_CORES=$(nproc 2>/dev/null || echo "1") # Calculate load as percentage of CPU cores local load_percent load_percent=$(echo "scale=0; ($CPU_LOAD_1MIN * 100) / $CPU_CORES" | bc 2>/dev/null) CPU_LOAD_PERCENT=${load_percent:-0} else CPU_LOAD_1MIN="N/A" CPU_LOAD_5MIN="N/A" CPU_LOAD_15MIN="N/A" CPU_LOAD_PERCENT="N/A" CPU_CORES="N/A" fi }
Примечание:
Тут используется
nproc, чтобы понимать масштаб трагедии (Load Average 2.0 на 1 ядре и на 16 ядрах — это всё-таки разные вещи);Математика происходит через
bc, так как нужно умножать и делить значения с плавающей точкой изloadavg.
Да, зависимость от
bc— компромисс. Я предпочёл читаемость и предсказуемость арифметики попыткам эмулировать float-математику в чистом Bash.
Колдуем над free
С памятью, конечно, ситуация чуть сложнее. Я использую free -b (в байтах), чтобы получить максимально точные цифры, а потом перевожу их в ГБ для читаемости.
# --- Memory Metrics Collection --- get_memory_metrics() { local mem_info mem_info=$(free -b 2>/dev/null | grep Mem) if [ -n "$mem_info" ]; then MEM_TOTAL=$(echo "$mem_info" | awk '{printf "%.2f", $2/1024/1024/1024}') MEM_USED=$(echo "$mem_info" | awk '{printf "%.2f", $3/1024/1024/1024}') MEM_AVAILABLE=$(echo "$mem_info" | awk '{printf "%.2f", $7/1024/1024/1024}') MEM_PERCENT=$(echo "scale=1; ($MEM_USED * 100) / $MEM_TOTAL" | bc 2>/dev/null) else MEM_TOTAL="N/A" MEM_USED="N/A" MEM_AVAILABLE="N/A" MEM_PERCENT="N/A" fi }
awk — древнее проклятие. Это всегда работает, но никогда не по той причине, которую вы ожидаете.
Сеть: Динамический поиск интерфейса
Чтобы не заставлять пользователя вводить имя сетевой карты, скрипт сам ищет «точку выхода» в интернет.
# --- Network Metrics Collection --- get_network_metrics() { # Get primary network interface (route to Google DNS) local interface interface=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $5}' | head -1) if [ -n "$interface" ] && [ "$interface" != "dev" ]; then NET_INTERFACE="$interface" local rx_bytes rx_bytes=$(cat "/sys/class/net/$interface/statistics/rx_bytes" 2>/dev/null) local tx_bytes tx_bytes=$(cat "/sys/class/net/$interface/statistics/tx_bytes" 2>/dev/null) NET_RX_MB=$(echo "scale=2; ${rx_bytes:-0} / 1024 / 1024" | bc 2>/dev/null) NET_TX_MB=$(echo "scale=2; ${tx_bytes:-0} / 1024 / 1024" | bc 2>/dev/null) else NET_INTERFACE="N/A" NET_RX_MB="N/A" NET_TX_MB="N/A" fi }
После того как интерфейс найден, мы идем в системные файлы /sys/class/net/$interface/statistics/. Это стандартный путь в Linux, где ядро хранит счетчики байтов rx_bytes (получено) и tx_bytes (отправлено).
Важное уточнение: скрипт показывает общий объем переданных данных (Traffic Total), а не текущую скорость. Для расчета скорости пришлось бы делать замеры с дельтой по времени, что усложнило бы логику, поэтому пока ограничился счетчиками
Диск и процессы
Для диска используется df -B1 /, что дает размер корневого раздела в байтах. Это позволяет избежать проблем с округлением, которые бывают при df -h. А количество процессов считается через ps -e | wc -l, что является самым быстрым способом подсчета всех активных PID в системе.
# --- Disk Metrics Collection --- get_disk_metrics() { local disk_info disk_info=$(df -B1 / 2>/dev/null | awk 'NR==2') if [ -n "$disk_info" ]; then DISK_USED=$(echo "$disk_info" | awk '{printf "%.1f", $3/1024/1024/1024}') DISK_AVAILABLE=$(echo "$disk_info" | awk '{printf "%.1f", $4/1024/1024/1024}') DISK_PERCENT=$(echo "$disk_info" | awk '{print $5}' | sed 's/%//') DISK_FILESYSTEM=$(echo "$disk_info" | awk '{print $1}') DISK_MOUNT=$(echo "$disk_info" | awk '{print $6}') else DISK_USED="N/A" DISK_AVAILABLE="N/A" DISK_PERCENT="N/A" DISK_FILESYSTEM="N/A" DISK_MOUNT="N/A" fi }
Работа с цветом и порогами
Чтобы реализовать индикацию в виде предупреждений или критических состояний я написал функцию colorize().
#--- COLOR FOR OUTPUT --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color # --- Colorize Output Based on Thresholds --- colorize() { local value=$1 local warning=$2 local critical=$3 #SKip coloring if disabled if [ "$USE_COLORS" = false ]; then echo "$value" return fi # Handle empty or N/A values if [ -z "$value" ] || [ "$value" = "N/A" ]; then echo -e "${YELLOW}N/A${NC}" # Check if value exceeds critical threshold (requires bc for float comparison) elif [ "$(echo "$value >= $critical" | bc 2>/dev/null)" = "1" ] 2>/dev/null; then echo -e "${RED}$value${NC}" # Check if value exceeds warning threshold elif [ "$(echo "$value >= $warning" | bc 2>/dev/null)" = "1" ] 2>/dev/null; then echo -e "${YELLOW}$value${NC}" else echo -e "${GREEN}$value${NC}" fi }
Тут пришлось использовать bc, так как Bash не умеет сравнивать числа с плавающей запятой напрямую.
Обработка аргументов и «безопасный» выход
Чтобы скрипт не был «одноразовым», я добавил полноценный парсинг аргументов через case. Это позволяет комбинировать флаги, например, запустить мониторинг без цвета: ./system-monitor.sh -i 2 --no-color.
#--- Command Line Arguments Parser --- parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in -i) if [[ ! $2 =~ ^[0-9]+$ ]] || [ "$2" -lt 1 ]; then echo -e "${RED}Error: Interval must be a positive integer${NC}" exit 1 fi INTERVAL=$2 shift 2 ;; --brief) BRIEF_MODE=true USE_COLORS=false shift ;; --no-color) USE_COLORS=false shift ;; --version) echo "${SCRIPT_NAME} version ${VERSION}" exit 0 ;; --help) show_help exit 0 ;; *) echo -e "${RED}Unknown argument: $1${NC}" show_help exit 1 ;; esac done }
Отдельное внимание уделил функции cleanup. Если вы запускаете бесконечный цикл мониторинга, важно, чтобы по нажатию Ctrl+C скрипт не просто обрывался, а корректно завершался. Для этого используется trap, который перехватывает сигнал SIGINT и выполняет финальный блок кода.
# --- Signal Handler for Clean Shutdown --- cleanup() { EXIT_SIGNAL=true echo -e "\n${YELLOW}Monitoring stopped. Goodbye!${NC}" exit 0 } trap cleanup INT TERM
Машиночитаемый вывод (он же brief-mode)
Иногда нужно не смотреть на экран глазами, а передать данные в Zabbix, Polybar, Prometheus (или через что вы мониторите систему). Для этого я сделал режим --brief, который выводит «сырые» данные в формате key=value
# --- Machine-Readable Output Format --- print_brief_stats() { get_cpu_metrics get_memory_metrics get_disk_metrics get_network_metrics get_process_metrics echo "timestamp=$(date +%s)" echo "cpu_load_1min=$CPU_LOAD_1MIN" echo "cpu_load_5min=$CPU_LOAD_5MIN" echo "cpu_load_15min=$CPU_LOAD_15MIN" echo "cpu_cores=$CPU_CORES" echo "cpu_load_percent=$CPU_LOAD_PERCENT" echo "mem_total_gb=$MEM_TOTAL" echo "mem_used_gb=$MEM_USED" echo "mem_available_gb=$MEM_AVAILABLE" echo "mem_usage_percent=$MEM_PERCENT" echo "disk_filesystem=$DISK_FILESYSTEM" echo "disk_mount=$DISK_MOUNT" echo "disk_used_gb=$DISK_USED" echo "disk_available_gb=$DISK_AVAILABLE" echo "disk_usage_percent=$DISK_PERCENT" echo "network_interface=$NET_INTERFACE" echo "network_rx_mb=$NET_RX_MB" echo "network_tx_mb=$NET_TX_MB" echo "process_count=$PROCESS_COUNT" }
Пример вывод brief-mode
system-monitor --brief timestamp=1767112871 cpu_load_1min=3.11 cpu_load_5min=2.27 cpu_load_15min=1.98 cpu_cores=4 cpu_load_percent=77 mem_total_gb=7.44 mem_used_gb=3.68 mem_available_gb=3.75 mem_usage_percent=49.4 disk_filesystem=/dev/sda1 disk_mount=/ disk_used_gb=159.3 disk_available_gb=138.2 disk_usage_percent=54 network_interface=wlan0 network_rx_mb=8961.65 network_tx_mb=1194.35 process_count=277
Превращаем скрипт в утилиту
После написания и тестирования скрипта хотелось бы его упаковать, чтобы скачать из репозитория. Поскольку я сам сижу на CachyOS (Based-on Arch), выбор пал на AUR (Arch User Repository). AUR — это уникальная штука. В отличие от репозиториев Debian или RHEL, куда «простому смертному» (то есть такие, как я) попасть сложно, AUR позволяет любому пользователю поделиться своим решением с сообществом.
С помощью PKGBUILD скрипт при установке переименовывается из system-monitor.sh в лаконичный system-monitor и кладется в /usr/bin/. Теперь мне не нужно помнить, в какой папке лежит файл, — он доступен из любой точки терминала. Я четко прописал depends (bash, gawk, procps-ng). Если кто-то решит поставить мой монитор на «голый» Arch, менеджер пакетов сам доставит всё необходимое.
В поле optdepends я вынес bc. Скрипт запустится и без него, но для высокой точности вычислений пакет менеджер предложит его доустановить.
При обновлении версии процесс стандартный: я создаю git-тег в репозитории, обновляю pkgver в PKGBUILD и пересчитываю sha256sums через makepkg -g. Это позволяет гарантировать, что пакет в AUR всегда соответствует конкретному релизу в GitHub, а не «плавающему» состоянию репозитория.
# Maintainer: DanLin (DanLinX2004X on GitHub) # Github repo: https://github.com/DanLinX2004X/system-monitor pkgname=system-monitor pkgver=1.1.0 pkgrel=1 pkgdesc="A powerful Bash script for real-time system monitoring (CPU, memory, disk, network, processes)" arch=('any') url="https://github.com/DanLinX2004X/system-monitor" license=('GPL3') depends=('bash' 'coreutils' 'procps-ng' 'util-linux' 'grep' 'gawk') optdepends=('bc: for precise calculations') source=("$pkgname-$pkgver.tar.gz::https://github.com/DanLinX2004X/system-monitor/archive/v$pkgver.tar.gz") sha256sums=('9587d15a6f0e6fc75f888f8fc91c54e823415fbb10de863f77174423cf220d46') package() { cd "$srcdir/$pkgname-$pkgver" install -Dm755 system-monitor.sh "$pkgdir/usr/bin/system-monitor" install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" install -Dm644 README.ru.md "$pkgdir/usr/share/doc/$pkgname/README.ru.md" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" }
Выводы
В результате получилась легкая утилита без лишних зависимостей, которая закрывает 90% моих потребностей в быстром диагностировании системы.
Главный вывод: Bash всё ещё нормально подходит для CLI‑утилит, если не забывать про модульность, обработку ошибок и здравый смысл в выборе задач.
Если хотите потыкать скрипт или предложить свои улучшения — буду рад звездочкам и PR в репозитории.
Ссылки: