强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

11 - 错误处理

11 - 错误处理

11.1 退出码(Exit Code)

每条命令执行后都会返回一个退出码(0-255),其中 0 表示成功,非 0 表示失败。

退出码含义常见场景
0成功命令正常完成
1一般错误通用失败
2误用 Shell 命令参数错误、语法错误
126权限不足文件不可执行
127命令未找到PATH 中找不到命令
128+N被信号 N 杀死130=Ctrl+C (SIGINT), 137=SIGKILL, 143=SIGTERM
128无效退出码参数exit 参数无效
255退出码超出范围exit 256
# 检查退出码
ls /etc/hosts
echo "退出码: $?"  # 0

ls /nonexistent
echo "退出码: $?"  # 2 或 1

# 自定义退出码
readonly EXIT_SUCCESS=0
readonly EXIT_ERROR=1
readonly EXIT_USAGE=2
readonly EXIT_CONFIG=3
readonly EXIT_DEPENDENCY=4

# 使用自定义退出码
check_prerequisites() {
    command -v docker &>/dev/null || { echo "需要安装 Docker" >&2; return $EXIT_DEPENDENCY; }
    command -v git &>/dev/null || { echo "需要安装 Git" >&2; return $EXIT_DEPENDENCY; }
    return $EXIT_SUCCESS
}

11.2 严格模式

# 推荐的严格模式开头
set -euo pipefail

# 等价于:
# set -e  → 命令失败立即退出
# set -u  → 使用未定义变量报错
# set -o pipefail → 管道中任一命令失败则整体失败

set -e 的注意事项

set -e

# ✅ 失败的命令会导致脚本退出
ls /nonexistent   # 脚本在这里终止

# ⚠️ 以下场景 set -e 不会触发退出:

# 1. if 条件中的命令
if ls /nonexistent; then  # 不会退出
    echo "不会执行"
fi

# 2. || 或 && 后的命令
ls /nonexistent || echo "失败了"  # 不会退出

# 3. 子 Shell 中的失败
(result=$(ls /nonexistent))  # 子 Shell 失败,但父 Shell 可能不退出

# 4. 管道中除最后一个命令外的失败
false | echo "hello"  # echo 成功,$?=0
# 解决:set -o pipefail

# 5. 函数返回值被检查时
my_func() {
    ls /nonexistent
    return 0
}
if my_func; then  # 函数返回 0,不会退出
    echo "不会触发 -e"
fi

# 6. 变量赋值右侧的命令失败
var=$(false)  # 这里会触发 -e 退出

set -u 的注意事项

set -u

# ❌ 使用未定义变量会报错
echo "$undefined_var"  # unbound variable

# ✅ 提供默认值
echo "${undefined_var:-默认值}"

# ✅ 检查变量是否设置
if [[ -v my_var ]]; then
    echo "$my_var"
fi

# ⚠️ 特殊变量不会触发 -u
echo "$@"  # 即使没有参数也不会报错
echo "$*"  # 同上
echo "$1"  # ⚠️ 如果没有第一个参数会报错

11.3 trap 信号处理

trap 是 Bash 的事件处理机制,可以在接收到信号或退出时执行清理代码。

常用信号

信号编号说明触发方式
SIGHUP1挂起终端关闭
SIGINT2中断Ctrl+C
SIGQUIT3退出Ctrl+\
SIGTERM15终止kill 命令
SIGKILL9强制终止kill -9(无法捕获)
EXIT0脚本退出脚本结束时
ERR-命令出错命令返回非 0
DEBUG-调试每条命令执行前
RETURN-函数返回函数/脚本返回时
# 基本用法
trap 'echo "收到 Ctrl+C"' INT
trap 'echo "脚本退出"' EXIT

# 清理临时文件
cleanup() {
    local exit_code=$?
    echo "清理临时文件..."
    rm -rf "$TEMP_DIR" 2>/dev/null
    exit "$exit_code"
}
trap cleanup EXIT INT TERM

# 创建临时目录
TEMP_DIR=$(mktemp -d)

# 捕获 ERR 信号(命令失败时)
trap 'echo "错误发生在第 $LINENO 行,命令: $BASH_COMMAND"' ERR

# 禁用 trap
trap - INT    # 移除 INT 处理
trap '' INT   # 忽略 INT 信号(Ctrl+C 无效)

trap 常用模式

# 模式一:创建临时文件/目录,自动清理
create_temp() {
    local tmpdir
    tmpdir=$(mktemp -d)
    trap "rm -rf '$tmpdir'" EXIT INT TERM
    echo "$tmpdir"
}

# 模式二:后台进程清理
PID_FILE="/tmp/myapp.pid"
cleanup() {
    [[ -f "$PID_FILE" ]] && {
        kill "$(cat "$PID_FILE")" 2>/dev/null
        rm -f "$PID_FILE"
    }
}
trap cleanup EXIT

# 写入 PID
echo $$ > "$PID_FILE"

# 模式三:锁文件清理
LOCK_FILE="/tmp/myapp.lock"
acquire_lock() {
    if [[ -f "$LOCK_FILE" ]]; then
        local old_pid
        old_pid=$(cat "$LOCK_FILE")
        if kill -0 "$old_pid" 2>/dev/null; then
            echo "另一个实例正在运行 (PID: $old_pid)" >&2
            exit 1
        fi
        echo "清除过期锁文件" >&2
    fi
    echo $$ > "$LOCK_FILE"
    trap "rm -f '$LOCK_FILE'" EXIT
}
acquire_lock

11.4 业务场景:健壮的脚本框架

#!/bin/bash
# robust_script.sh —— 带完整错误处理的脚本框架
set -euo pipefail

# ---- 全局配置 ----
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
readonly LOCK_FILE="/tmp/${SCRIPT_NAME}.lock"

# ---- 日志函数 ----
log() {
    local level="$1"
    shift
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*"
    echo "$msg" | tee -a "$LOG_FILE" >&2
}

log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@"; }
log_error() { log "ERROR" "$@"; }
log_debug() { log "DEBUG" "$@"; }

# ---- 错误处理 ----
error_handler() {
    local line=$1
    local code=$2
    log_error "脚本在第 $line 行失败,退出码: $code"
    log_error "失败命令: ${BASH_COMMAND}"
    exit "$code"
}

trap 'error_handler ${LINENO} $?' ERR

# ---- 清理函数 ----
TEMP_FILES=()
TEMP_DIRS=()

register_temp_file() { TEMP_FILES+=("$1"); }
register_temp_dir()  { TEMP_DIRS+=("$1"); }

cleanup() {
    local exit_code=$?
    log_info "正在清理..."
    
    for file in "${TEMP_FILES[@]}"; do
        [[ -f "$file" ]] && rm -f "$file" && log_debug "删除文件: $file"
    done
    
    for dir in "${TEMP_DIRS[@]}"; do
        [[ -d "$dir" ]] && rm -rf "$dir" && log_debug "删除目录: $dir"
    done
    
    [[ -f "$LOCK_FILE" ]] && rm -f "$LOCK_FILE"
    
    log_info "清理完成,退出码: $exit_code"
    exit "$exit_code"
}

trap cleanup EXIT INT TERM

# ---- 锁机制 ----
acquire_lock() {
    if [[ -f "$LOCK_FILE" ]]; then
        local old_pid
        old_pid=$(<"$LOCK_FILE")
        if kill -0 "$old_pid" 2>/dev/null; then
            log_error "另一个实例正在运行 (PID: $old_pid)"
            exit 1
        fi
        log_warn "清除过期锁文件 (PID: $old_pid)"
    fi
    echo $$ > "$LOCK_FILE"
}

# ---- 重试机制 ----
retry() {
    local max_attempts="$1"
    local delay="$2"
    local attempt=1
    
    shift 2
    
    while ((attempt <= max_attempts)); do
        if "$@"; then
            return 0
        fi
        
        log_warn "第 $attempt/$max_attempts 次尝试失败"
        
        if ((attempt < max_attempts)); then
            log_info "等待 ${delay}s 后重试..."
            sleep "$delay"
        fi
        
        ((attempt++))
    done
    
    log_error "所有 $max_attempts 次尝试均失败"
    return 1
}

# ---- 主逻辑 ----
main() {
    acquire_lock
    
    log_info "脚本启动: $SCRIPT_NAME (PID: $$)"
    
    # 创建临时目录
    local tmpdir
    tmpdir=$(mktemp -d)
    register_temp_dir "$tmpdir"
    
    # 业务逻辑
    log_info "开始处理..."
    
    # 使用重试机制
    retry 3 5 curl -sf "https://api.example.com/health" -o "$tmpdir/response.json"
    
    # 创建临时文件
    local tmpfile="$tmpdir/processed.txt"
    touch "$tmpfile"
    register_temp_file "$tmpfile"
    
    echo "处理完成" > "$tmpfile"
    log_info "处理完成"
}

main "$@"

11.5 错误处理最佳实践

命令级别的错误处理

# 方式一:短路处理
cd /tmp || { echo "无法切换目录" >&2; exit 1; }

# 方式二:if 语句
if ! cd /tmp; then
    echo "无法切换目录" >&2
    exit 1
fi

# 方式三:retry 重试
retry 3 5 some_command

# 方式四:|| 提供备选方案
result=$(command_may_fail) || result="默认值"

函数级别的错误处理

# 函数返回错误码
validate_input() {
    local input="$1"
    
    [[ -z "$input" ]] && { echo "输入为空" >&2; return 1; }
    [[ ! -f "$input" ]] && { echo "文件不存在: $input" >&2; return 2; }
    [[ ! -r "$input" ]] && { echo "文件不可读: $input" >&2; return 3; }
    
    return 0
}

# 调用者检查错误
if ! validate_input "$config_file"; then
    echo "输入验证失败" >&2
    exit 1
fi

11.6 注意事项

陷阱说明解决方案
set -e 在子 Shell子 Shell 中的失败可能不传播使用 set -o pipefail
trap ERR 在函数中默认不传播到函数调用者set -E(errtrace)
清理函数中的错误cleanup 本身可能失败`
kill -9 无法捕获SIGKILL 不可捕获确保使用 SIGTERM
退出码范围只有 0-255取模处理

11.7 扩展阅读