В статье я рассказываю, как из набора команд (
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 в репозитории.
Ссылки:
