Bash 进阶脚本:错误处理、调试与系统监控

FreeGuideOnline 最新 2026-06-12

Bash 脚本编程进阶:从错误处理到系统监控

在掌握了 Bash 基础语法后,脚本的健壮性与实用性成为下一个关键目标。本教程聚焦于三个核心进阶主题:错误处理(让脚本优雅应对异常)、调试技巧(高效定位问题)以及系统监控(编写实用的运维脚本)。通过大量可直接运行的代码示例,你将学会如何构建生产级 Shell 脚本。


1. 健壮脚本的基石:错误处理

让脚本在遇到意外时不会静默失败或产生破坏性后果,需要主动检查命令执行状态、处理退出码并制定错误响应策略。

1.1 退出状态码与 set -e

  • 每个命令执行后都会返回一个退出码($?),0 表示成功,非 0 表示错误。
  • 脚本默认会忽略中间命令的失败,直到结束才返回最后一条命令的退出码。
  • 启用 set -e 可强制脚本在任意命令失败时立即退出,避免连锁错误。
#!/bin/bash
set -e          # 命令失败立即退出
cd /nonexistent # 脚本在这里终止,后续命令不会执行
echo "这行不会被执行"
  • 注意事项:管道中只有最后一个命令的退出码会被考虑,若需要管道任意环节失败都退出,需配合 set -o pipefail
set -eo pipefail   # 严格模式:单命令失败或管道任意环节失败均退出

1.2 自定义错误处理函数与 trap

仅依赖 set -e 还不够,我们希望捕获错误时执行清理操作(如删除临时文件)或打印友好日志。trap 命令可以捕获信号和脚本退出事件。

#!/bin/bash
set -e

cleanup() {
    echo "捕获到错误或脚本退出,执行清理..."
    rm -f /tmp/myapp_temp.*
}
trap cleanup ERR EXIT   # ERR 捕获任何导致脚本退出的错误(要求 set -e),EXIT 确保正常退出也清理

# 模拟一个可能失败的操作
false   # 故意执行失败,触发 trap ERR
  • 常用 trap 信号:ERR(错误退出)、EXIT(脚本结束)、INT(Ctrl+C 中断)、TERM
  • 避免重复 trap:若函数内多次设置 trap,可使用 trap - ERR EXIT 取消之前的捕获。

1.3 手动检查关键命令与重试逻辑

并非所有命令都适合用 set -e 一刀切,部分场景需要更细粒度的控制。

# 尝试创建目录,失败则打印错误并退出
if ! mkdir -p /important/dir; then
    echo "错误:无法创建目录" >&2
    exit 1
fi

# 简单的重试函数
retry() {
    local max_attempts=$1; shift
    local cmd=$@
    for ((i=1; i<=max_attempts; i++)); do
        if $cmd; then
            return 0
        fi
        echo "第 $i 次尝试失败,等待重试..." >&2
        sleep 2
    done
    echo "命令失败,已达最大重试次数" >&2
    return 1
}
retry 3 wget -q https://example.com/file.tar.gz

2. 调试技术:让脚本透明化

程序员的日常不只是写代码,更是调试代码。Bash 提供了多层次的调试手段,从简单的打印到专业的跟踪工具。

2.1 内置调试选项

选项 作用 使用方式
-x 执行前打印每条命令及展开后的参数 bash -x script.sh 或在脚本内 set -x
-v 打印读入的原始行 bash -v script.sh
-n 仅检查语法,不执行 bash -n script.sh
-u 使用未定义变量时报错退出(强烈推荐) set -u
  • 选择性调试:用 set -xset +x 包裹可疑区域,避免输出过多噪音。
echo "正常区域"
set -x           # 开启详细跟踪
complex_logic
set +x           # 关闭跟踪
echo "继续"

2.2 结合 PS4 变量增强调试输出

PS4-x 模式下的提示符,默认为 +。可以自定义为包含行号、函数名、时间戳等信息,极大提升调试效率。

export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+(${FUNCNAME[0]})}: '
set -x
# 输出示例:+ [myscript.sh:12] main: echo hello
  • 常用 PS4 变量:BASH_SOURCE(脚本名)、LINENO(行号)、FUNCNAME(函数调用栈)。

2.3 日志函数代替 echo

在脚本中直接使用 echo 难以统一控制输出格式和级别,封装一个简单的日志函数更实用。

log() {
    local level=$1; shift
    local msg="[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $*"
    case $level in
        ERROR) echo -e "\033[31m${msg}\033[0m" ;;   # 红色
        WARN)  echo -e "\033[33m${msg}\033[0m" ;;   # 黄色
        INFO)  echo -e "\033[32m${msg}\033[0m" ;;   # 绿色
        DEBUG) [[ $DEBUG -eq 1 ]] && echo -e "\033[36m${msg}\033[0m" ;; # 仅在DEBUG=1时输出
    esac
}
DEBUG=1
log INFO "服务启动成功"
log DEBUG "变量 X 的值为 $X"
log ERROR "文件未找到"
  • 可以将日志同时写入文件,通过 tee 或重定向扩展。

2.4 利用 shellcheck 静态检查

许多运行时错误可以通过静态工具提前发现。shellcheck 是必装的 Bash 代码质量工具。

# 安装后使用
shellcheck my_script.sh
  • 它能检测常见问题:未引用的变量、rm -rf $DIR/ 的危险、错误的 [ $var = "val" ] 写法等。

3. 系统监控脚本实战

真正让 Bash 在运维中大放异彩的是系统监控。我们将编写几个可以直接使用的监控脚本,涵盖 CPU、内存、磁盘、进程等。

3.1 基础指标收集框架

首先建立一个脚本模板,实现定时采样与报警。

#!/bin/bash
set -euo pipefail

# 配置项
LOG_DIR="/var/log/monitor"
INTERVAL=5          # 采样间隔(秒)
CPU_THRESHOLD=80    # CPU 使用率报警阈值
MEM_THRESHOLD=90    # 内存使用率报警阈值

mkdir -p "$LOG_DIR"

# 通用报警函数(可替换为发送邮件或消息推送)
alert() {
    local msg="$1"
    echo "[ALERT] $(date) $msg" | tee -a "$LOG_DIR/alerts.log"
    # 在这里加上你的通知逻辑,例如:curl -X POST -d "text=$msg" webhook_url
}
while true; do
    # 收集指标逻辑将放在这里
    sleep "$INTERVAL"
done

3.2 CPU 和内存监控

借助 /proc/stattop 命令的批处理模式获取数据。

# 获取 CPU 总利用率(用户态+系统态)—— 需要两次采样计算差值
get_cpu_usage() {
    local cpu_line=($(grep '^cpu ' /proc/stat))
    local idle1=${cpu_line[4]}
    local total1=0
    for val in "${cpu_line[@]:1}"; do
        ((total1 += val))
    done
    sleep 1
    cpu_line=($(grep '^cpu ' /proc/stat))
    local idle2=${cpu_line[4]}
    local total2=0
    for val in "${cpu_line[@]:1}"; do
        ((total2 += val))
    done
    local delta_idle=$((idle2 - idle1))
    local delta_total=$((total2 - total1))
    echo $((100 * (delta_total - delta_idle) / delta_total))
}

# 获取内存使用百分比
get_mem_usage() {
    local mem_info=($(free -b | awk '/Mem:/ {print $2,$3}'))
    local total=${mem_info[0]}
    local used=${mem_info[1]}
    echo $((100 * used / total))
}

在监控循环中调用:

while true; do
    cpu=$(get_cpu_usage)
    mem=$(get_mem_usage)
    log INFO "CPU: ${cpu}%, Memory: ${mem}%"

    if [[ $cpu -ge $CPU_THRESHOLD ]]; then
        alert "CPU 使用率过高: ${cpu}% (阈值: ${CPU_THRESHOLD}%)"
    fi
    if [[ $mem -ge $MEM_THRESHOLD ]]; then
        alert "内存使用率过高: ${mem}% (阈值: ${MEM_THRESHOLD}%)"
    fi
    sleep "$INTERVAL"
done

3.3 磁盘空间与 inode 监控

df 命令可以直接输出使用百分比,但要注意去除百分号并比较数值。

check_disk() {
    local threshold=85
    # 监控根分区和任何数据分区
    df -h / /data 2>/dev/null | awk 'NR>1 {print $5" "$6}' | while read usage mount; do
        local percent=${usage%\%}
        if [[ $percent -ge $threshold ]]; then
            alert "磁盘 $mount 使用率 ${percent}% 已超过阈值"
        fi
    done

    # Inode 使用检查(防止小文件耗尽 inode 导致“磁盘满”假象)
    df -i / | awk 'NR==2 {print $5" "$6}' | {
        read usage mount
        local ip=${usage%\%}
        if [[ $ip -ge 85 ]]; then
            alert "磁盘 $mount Inode 使用率 ${ip}% 过高"
        fi
    }
}

3.4 关键进程存活监控

监控指定进程名是否存在,若不存在则触发报警并尝试自动重启(可选)。

PROCESSES=("nginx" "sshd" "mysql")

check_processes() {
    for proc in "${PROCESSES[@]}"; do
        if ! pgrep -x "$proc" > /dev/null; then
            alert "进程 $proc 未运行!"
            # 自动重启逻辑(谨慎使用)
            # case $proc in
            #     nginx) systemctl restart nginx ;;
            #     mysql) systemctl restart mysql ;;
            # esac
        fi
    done
}

3.5 网络连通性与服务端口检测

通过 pingnc (或 curl) 检测外部连通性与服务可用性。

TARGETS=("8.8.8.8" "1.1.1.1")
SERVICES=("https://example.com" "tcp://db.internal:3306")

check_network() {
    # ping 检查
    for ip in "${TARGETS[@]}"; do
        if ! ping -c 1 -W 1 "$ip" > /dev/null; then
            alert "Ping $ip 失败"
        fi
    done

    # 服务端口检查 (需要 nc 命令)
    for srv in "${SERVICES[@]}"; do
        if [[ $srv == tcp://* ]]; then
            local host=${srv#tcp://}
            local port=${host#*:}
            host=${host%:*}
            if ! nc -zv -w2 "$host" "$port" &>/dev/null; then
                alert "TCP 端口不可达: $host:$port"
            fi
        elif [[ $srv == https://* ]]; then
            if ! curl -f -s -o /dev/null --connect-timeout 3 "$srv"; then
                alert "HTTP 服务异常: $srv"
            fi
        fi
    done
}

3.6 将所有碎片组合成完整监控脚本

将上述函数集成到一个脚本中,加上适当的主循环和日志记录,一个轻量级实时监控守护进程就诞生了。

#!/bin/bash
set -euo pipefail
source ./logger.sh   # 引用之前的日志函数

# 配置...(同上)
LOG_DIR="/var/log/monitor"
INTERVAL=10
CPU_THRESHOLD=80
MEM_THRESHOLD=90
# ... 所有函数定义(get_cpu_usage, get_mem_usage, check_disk, check_processes, check_network)

main() {
    # 以守护进程方式运行更好的选择是使用系统服务,这里仅示意
    while true; do
        cpu=$(get_cpu_usage)
        mem=$(get_mem_usage)
        log INFO "CPU: ${cpu}%, Mem: ${mem}%"
        [[ $cpu -ge $CPU_THRESHOLD ]] && alert "CPU 使用 ${cpu}% 超出阈值"
        [[ $mem -ge $MEM_THRESHOLD ]] && alert "内存使用 ${mem}% 超出阈值"

        check_disk
        check_processes
        check_network

        sleep "$INTERVAL"
    done
}

main "$@"

4. 进阶技巧与最佳实践

  • 配置与代码分离:将阈值、检查项、通知方式等提取到单独的 .conf 文件,并用 source 加载。
  • 守护进程化:使用 systemdsupervisor 管理监控脚本,避免手动 nohup
  • 性能考虑:频繁调用外部命令(如 awkgrepsleep)在长时间循环中会有开销。对于极高频监控,可考虑纯 Bash 内置操作或减少外部调用。
  • 脚本自检:在脚本开头添加 check_root 或依赖检查函数。
require() { command -v "$1" >/dev/null 2>&1 || { echo "需要 $1 但未安装。退出。" >&2; exit 1; } }
require nc
require wget

结语

Bash 进阶编程的核心是让脚本具备应对真实环境复杂性的能力。通过严谨的错误处理、高效的调试手段以及实用的监控逻辑,你可以将简单的命令行脚本提升为可靠的自动化工具。下一步建议:阅读 man bash 中关于 [[、数组、重定向的进阶用法,并动手为自己所在的环境编写一套定制化监控解决方案。