Bash 脚本中的有效错误处理策略

使用严格模式、陷阱、退出码和清晰的 stderr 消息,让 Bash 脚本安全失败并自行清理。

Bash 脚本中的有效错误处理策略

Bash 脚本错误处理很重要,因为一个静默失败的脚本可能会复制不完整的文件、部署损坏的代码或删除错误的路径。您希望脚本在关键步骤失败时停止,解释发生了什么,并在退出前清理临时文件。

以下模式涵盖了您最常需要的部分:严格模式、显式检查、trap 和简单的错误报告。

基础:理解退出状态

在 Unix 世界中,每个执行的命令都会返回一个退出状态(或退出码),这是一个表示操作结果的整数值。该状态会立即存储在特殊变量 $? 中。

  • 退出码 0: 按照惯例,这表示成功(或 'true')。
  • 退出码 1–255: 这些表示失败(或 'false')。特定的代码通常与特定类型的失败相关(例如,1 表示一般错误,127 表示命令未找到)。

可靠的脚本必须检查关键命令的退出状态,并在脚本失败时返回一个有意义的非零代码。

核心策略 1:防御性脚本三要素

对于任何重要的自动化脚本,您应该在 shebang 行(#!/bin/bash)之后立即应用三个基本选项。这些选项强制执行严格、可预测的行为。

1. 失败时立即退出(set -e

set -e 选项(或 set -o errexit)规定,如果任何命令失败(返回非零退出状态),脚本必须立即退出。

这通常被称为“快速失败”原则,防止脚本使用不完整或失败的前提条件继续执行可能具有破坏性的操作。

#!/bin/bash
set -e

echo "开始处理..."
mkdir /tmp/test_dir
cp non_existent_file /tmp/test_dir/ # 此命令失败(退出码 > 0)

echo "此行不会被执行。" # 脚本在此处退出

警告:set -e 的注意事项

set -e 在几种常见情况下不会触发退出,包括由 ifwhile 测试的命令、大多数 &&|| 列表中的命令,以及使用 ! 反转状态的命令。将其视为安全网,而不是替代围绕预期失败的清晰检查。

2. 将未设置变量视为错误(set -u

set -u 选项(或 set -o nounset)确保脚本将任何未设置变量的使用视为错误,导致脚本立即退出(类似于 set -e)。这可以防止变量名拼写错误导致空字符串传递给关键命令的细微错误。

#!/bin/bash
set -u

# echo "变量是:$UNDEFINED_VAR" # 脚本失败并在此处退出

MY_VAR="已定义"
echo "变量是:${MY_VAR}"

3. 处理命令管道(set -o pipefail

默认情况下,命令管道(command1 | command2 | command3)仅报告最后一个命令(command3)的退出状态。如果 command1 失败但 command3 成功,$? 将为 0,从而掩盖失败。

set -o pipefail 改变了这种行为,确保如果管道中的任何命令失败,管道返回非零状态。这对于可靠的数据处理至关重要。

#!/bin/bash
set -o pipefail

# 命令 `false` 总是退出 1
# 如果没有 pipefail,此行将返回 0,因为 `cat` 成功。
false | cat # 由于 pipefail 返回 1

if [ $? -ne 0 ]; then
    echo "管道失败。"
fi

最佳实践:头部

始终使用组合的防御性选项启动健壮的脚本:

#!/bin/bash
set -euo pipefail

核心策略 2:手动检查和条件执行

虽然 set -e 处理大多数失败,但您通常需要手动检查命令状态,特别是当失败是预期的或需要特定日志记录时。

if 语句检查

检查命令是否成功的标准方法是在 if 块中捕获其退出状态。此方法覆盖 set -e 行为,允许您显式处理错误。

#!/bin/bash
set -euo pipefail

TEMP_FILE="/tmp/data_processing_$$/config.dat"

# 尝试创建目录;显式处理失败
if ! mkdir -p "$(dirname "$TEMP_FILE")"; then
    echo "[ERROR] 无法创建临时目录。" >&2
    exit 1
fi

# 尝试获取数据
if ! curl -sSf https://api.example.com/data > "$TEMP_FILE"; then
    echo "[ERROR] 从 API 获取数据失败。" >&2
    exit 2
fi

echo "数据成功检索。"

提示: curl-sSf 标志(静默、失败、显示错误)强制 curl 在 HTTP 错误时返回非零退出码,使错误处理更容易。

使用短路运算符(&&||

这些逻辑运算符提供了基于成功(&&)或失败(||)链接命令的简洁方式。

  • command1 && command2:仅在 command1 成功时运行 command2
  • command1 || command2:仅在 command1 失败时运行 command2
# 示例:创建目录并复制文件,如果任一步骤失败则失败
mkdir logs && cp /var/log/syslog logs/system.log

# 示例:尝试备份,如果备份失败则记录错误并退出
pg_dump database > backup.sql || { echo "备份失败!" >&2; exit 10; }

高级策略 3:使用 trap 保证清理

当脚本处理临时文件、锁文件或已建立的网络连接时,突然退出(无论是成功还是由于错误)可能会使系统处于不一致状态。trap 命令允许您定义一个命令或函数,在脚本接收到特定信号时执行。

EXIT 信号

EXIT 信号对于一般清理最有用。无论退出是成功的、手动的 exit 调用,还是由 set -e 触发的退出,当脚本退出时都会运行被捕获的命令。

#!/bin/bash

TEMP_DIR=$(mktemp -d)

# 清理函数定义
cleanup() {
    EXIT_CODE=$?
    echo "清理临时目录:${TEMP_DIR}"
    rm -rf "$TEMP_DIR"
    # 如果脚本因失败退出,恢复失败代码
    if [ $EXIT_CODE -ne 0 ]; then
        exit $EXIT_CODE
    fi
}

# 设置陷阱:脚本退出时运行 'cleanup' 函数
trap cleanup EXIT

# --- 主脚本逻辑 ---

echo "在 ${TEMP_DIR} 中处理数据"

# 模拟成功操作...
# ... 脚本继续 ...

# 模拟触发 set -e 的关键失败
false

# 此行不可达,但清理仍然保证运行。
echo "完成。"

处理特定信号(TERMINT

您还可以捕获特定的终止信号,如 TERM(终止请求)或 INT(中断,通常是 Ctrl+C),以确保在用户或调度程序取消作业时优雅关闭。

trap 'echo "脚本被用户中断(Ctrl+C)。中止清理。" >&2; exit 130' INT

策略 4:自定义错误报告和日志记录

专业脚本应使用专用的错误函数来集中报告,确保一致性和正确的输出通道。

将错误重定向到标准错误(>&2

错误消息应始终打印到标准错误(stderr 或文件描述符 2),以便标准输出(stdout 或文件描述符 1)保持干净,用于数据或成功结果。

die 函数模式

创建一个函数,通常命名为 dieerror_exit,用于处理记录消息、清理(如果不使用陷阱)以及以指定代码退出。

# 打印错误消息并退出的函数
die() {
    local msg=$1
    local code=${2:-1}
    echo "$(date +'%Y-%m-%d %H:%M:%S') [FATAL]: ${msg}" >&2
    exit "$code"
}

# 示例用法:

REQUIRED_VAR="$1"

if [ -z "$REQUIRED_VAR" ]; then
    die "缺少必需参数(数据库名称)。" 3
fi

# ... 稍后在脚本中 ...

if ! validate_checksum "$FILE"; then
    die "$FILE 的校验和验证失败。" 5
fi

让失败变得平淡

对于可靠的 Bash 脚本错误处理,从 set -euo pipefail 开始每个非平凡的脚本,在您预期命令可能失败的地方使用 if ! command; then ...; fi,并将错误发送到 stderr。如果您的脚本创建临时文件、锁文件或部分输出,请在风险工作开始前添加 trap cleanup EXIT

这种组合使小型自动化任务可预测,并使生产失败更容易诊断。