DevOps Bootcamp · 课时

用于清理和处理信号的陷阱处理器

注册 EXIT、ERR 和 INT 陷阱,可靠地删除临时文件并回滚未完成的工作。

第 2 / 4 课13 个步骤

用于清理和处理信号的陷阱处理器 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。

陷阱处理程序为何重要

Bash 脚本退出时——无论是正常退出、因错误退出,还是因为用户按下了 Ctrl+C——都可能留下临时文件、写入一半的数据或损坏的状态。如果不进行清理,这些残留物会不断累积,并导致难以调试的问题。

陷阱处理程序通过注册一个函数或命令来解决这个问题:当收到特定信号或伪信号时,Bash 会自动执行该函数或命令。

  • EXIT——脚本因任何原因退出时都会触发
  • ERR——任何命令返回非零退出状态后触发
  • INT——用户发送 SIGINT(Ctrl+C)时触发
  • TERM——收到 SIGTERM 时触发(例如由 kill 发送)

编写良好的脚本会在最顶部注册这些陷阱,在任何有风险的操作开始之前完成注册。

trap 内建命令的语法

trap 内建命令会为一个或多个信号或伪信号注册处理程序。一般语法如下:

trap 'command_or_function' SIGNAL [SIGNAL...]

主要规则:

  • 第一个参数是要在信号触发时执行的带引号的 shell 代码字符串(或函数名称)。
  • 可以在处理程序之后列出多个信号。
  • trap '' SIGNAL 会忽略该信号(处理程序为空)。
  • trap - SIGNAL 会将该信号重置为默认行为。

同一 shell 中调用的函数会继承陷阱,但使用 ( ) 或 & 创建的子 shell不会继承陷阱。

#!/usr/bin/env bash
# Syntax examples — not a full script

# Register a cleanup function on EXIT
trap cleanup EXIT

# Inline handler for INT and TERM
trap 'echo "Interrupted!"; exit 1' INT TERM

# Ignore SIGHUP
trap '' HUP

# Reset SIGPIPE to default
trap - PIPE

EXIT 陷阱——您的安全网

EXIT 伪信号是最重要的陷阱。它会在 shell 因任何原因退出时触发:正常完成、exit N、未处理的错误或信号。因此,它非常适合用于删除临时文件。

最佳实践:

  • 使用 mktemp 创建临时文件,使名称唯一且不可预测。
  • 立即将路径存储到变量中。
  • 创建文件后立即注册 EXIT 陷阱,确保清理操作始终执行。

脚本的退出代码会被保留——除非您在处理程序中显式调用 exit,否则会忽略陷阱处理程序的退出代码。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/myapp.XXXXXX)

cleanup() {
    echo "Removing temp file: $TMPFILE" >&2
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Do real work
echo "Processing..." > "$TMPFILE"
sort "$TMPFILE" -o "$TMPFILE"
cp "$TMPFILE" /tmp/myapp_result.txt

echo "Done. Result saved."

处理多个临时文件和目录

实际脚本通常会创建多个临时资源——文件、目录、FIFO、锁文件。与其分别跟踪每个资源,不如将它们收集到数组中,并在一个清理函数中统一删除。

这种模式易于扩展:每次创建新的临时资源时,将其追加到数组中。无论脚本从何处退出,EXIT 陷阱都会处理完整列表。

#!/usr/bin/env bash
set -euo pipefail

CLEANUP_TARGETS=()

add_cleanup() {
    CLEANUP_TARGETS+=("$1")
}

cleanup() {
    echo "Running cleanup..." >&2
    for target in "${CLEANUP_TARGETS[@]:-}"; do
        rm -rf "$target"
        echo "Removed: $target" >&2
    done
}
trap cleanup EXIT

# Create and track temp resources
TMPDIR_WORK=$(mktemp -d /tmp/work.XXXXXX)
add_cleanup "$TMPDIR_WORK"

TMPFILE_LOG=$(mktemp /tmp/run.XXXXXX.log)
add_cleanup "$TMPFILE_LOG"

echo "Working in $TMPDIR_WORK" | tee "$TMPFILE_LOG"
touch "$TMPDIR_WORK/output.txt"
echo "Step complete."

ERR 陷阱——捕获失败

ERR 伪信号会在任何简单命令返回非零退出状态后触发,前提是 shell 选项 set -e(errexit)处于启用状态,或者该命令不在 if、while 或 ||/&& 复合结构中。

使用 ERR 陷阱可以:

  • 使用 $LINENO 和 $BASH_COMMAND 记录失败的行。
  • 在 EXIT 陷阱运行前触发回滚逻辑。
  • 向标准错误输出结构化的错误消息。

重要:ERR 陷阱在函数内部不会触发,除非您显式运行 set -E(errtrace),让函数从调用它的 shell 继承 ERR 陷阱。

#!/usr/bin/env bash
set -eEuo pipefail

on_error() {
    local exit_code=$?
    local line=$1
    echo "ERROR: command failed with exit code $exit_code at line $line" >&2
    echo "  Command: $BASH_COMMAND" >&2
}
trap 'on_error $LINENO' ERR

echo "Starting task..."
cp /nonexistent/file /tmp/   # This will fail
echo "This line is never reached."

INT 陷阱——优雅地处理 Ctrl+C

按下 Ctrl+C 会向前台进程组发送 SIGINT。如果没有陷阱,Bash 会立即退出,可能留下未完成的工作。

注册 INT 陷阱后,您可以:

  • 输出易于用户理解的取消消息。
  • 在交由 EXIT 陷阱处理之前执行必要的中间回滚。
  • 使用有意义的状态代码退出(130 是 SIGINT 的惯用代码:128 + 2)。

INT 处理程序完成后,调用 exit 130 以传递正确的退出代码。随后 EXIT 陷阱会自动运行。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/demo.XXXXXX)

cleanup() {
    rm -f "$TMPFILE"
    echo "Temp file removed." >&2
}

on_interrupt() {
    echo "" >&2
    echo "Caught SIGINT — cancelling gracefully." >&2
    exit 130   # EXIT trap (cleanup) runs automatically after this
}

trap cleanup EXIT
trap on_interrupt INT

echo "Running long task. Press Ctrl+C to cancel."
for i in $(seq 1 10); do
    echo "Step $i/10..."
    sleep 1
done
echo "All steps complete."

组合使用 EXIT、ERR 和 INT 陷阱

在生产脚本中,通常会同时注册全部三个陷阱,以覆盖所有退出路径:

  • EXIT——始终运行;负责清理文件。
  • ERR——记录失败的命令和行号。
  • INT——输出取消消息,并以代码 130 退出。

由于 EXIT 始终最后运行,清理逻辑只需放在一个位置。ERR 和 INT 处理程序可以专注于记录信息和设置状态,然后通过调用 exit 将实际清理工作交给 EXIT。

在顶部使用 set -eEuo pipefail,这样函数中的错误也会触发 ERR,同时 pipefail 可以捕获管道内部的失败。

#!/usr/bin/env bash
set -eEuo pipefail

TMPFILE=$(mktemp /tmp/combined.XXXXXX)
ROLLBACK_NEEDED=false

cleanup() {
    if [[ "$ROLLBACK_NEEDED" == true ]]; then
        echo "Rolling back partial changes..." >&2
    fi
    rm -f "$TMPFILE"
    echo "Cleanup done." >&2
}

on_error() {
    ROLLBACK_NEEDED=true
    echo "ERR at line $1: $BASH_COMMAND" >&2
}

on_interrupt() {
    echo "Interrupted by user." >&2
    exit 130
}

trap cleanup       EXIT
trap 'on_error $LINENO' ERR
trap on_interrupt  INT

echo "hello" > "$TMPFILE"
echo "Script finished successfully."

动态重置和禁用陷阱

有时您需要在脚本运行期间修改陷阱——例如,关键部分完成后执行一次性清理,使后续正常操作不会触发不必要的回滚。

  • trap - SIGNAL 会将信号重置为内建的默认行为。
  • trap '' SIGNAL 会完全忽略该信号(进程无法被该信号终止)。
  • 您可以随时重新注册新的处理程序;最后一次 trap 调用会生效。

一种常见模式是在高风险操作成功后升级 EXIT 陷阱,将其从回滚处理程序切换为简单的清理处理程序。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/staged.XXXXXX)

rollback() {
    echo "ROLLBACK: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

cleanup_only() {
    echo "CLEANUP: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

# Start with rollback in case something fails during the risky phase
trap rollback EXIT

echo "Performing risky operation..."
echo "critical data" > "$TMPFILE"
# ... imagine more risky steps here ...

# Risky phase succeeded — switch to simple cleanup
trap cleanup_only EXIT

echo "Risky phase done. Now doing safe finalization."
cp "$TMPFILE" /tmp/staged_result.txt
echo "All done."

通过陷阱传递退出代码

有一个容易忽略的问题:如果清理函数包含可能失败的命令,脚本的最终退出代码可能会被覆盖。请显式保留原始退出代码。

在陷阱处理程序中,$? 保存着触发该陷阱的退出代码。请在处理程序的第一行捕获它,之后再执行任何其他命令,以免其值发生变化。

#!/usr/bin/env bash
set -euo pipefail

TMPFILE=$(mktemp /tmp/exitcode.XXXXXX)

cleanup() {
    local original_exit=$?   # capture BEFORE any other command
    echo "Cleaning up (original exit: $original_exit)" >&2
    rm -f "$TMPFILE"
    # Exit with the original code so callers see the real result
    exit "$original_exit"
}
trap cleanup EXIT

echo "Doing work..."
echo "data" > "$TMPFILE"

# Simulate failure
if [[ ! -f /tmp/required_marker ]]; then
    echo "Required marker missing!" >&2
    exit 2
fi

echo "Done."

函数和子 shell 中的陷阱

理解陷阱的继承对于编写正确的脚本至关重要:

  • 函数在同一 shell 中运行并继承陷阱,但只有启用 set -E(errtrace)后,ERR 才会传递到函数中。
  • 子 shell(( ... ))会以父 shell 陷阱的副本启动,但子 shell 内部的修改不会影响父 shell。
  • 后台作业(cmd &)属于子 shell,不会继承父 shell 的 INT/TERM 陷阱(它们会采用默认的信号处理方式)。

实用规则:将陷阱注册放在主脚本主体中;需要 ERR 传递到函数时使用 set -E;绝不要依赖陷阱跨越 & 的继承。

#!/usr/bin/env bash
set -eEuo pipefail   # -E = errtrace: ERR propagates into functions

on_error() {
    echo "ERR in function or main at line $1" >&2
}
trap 'on_error $LINENO' ERR

risky_function() {
    echo "Inside risky_function"
    ls /nonexistent_path   # triggers ERR — works because of set -E
}

# Subshell: has its own copy; changes don't affect parent
(
    trap '' ERR   # disable ERR only inside subshell
    ls /nonexistent_path 2>/dev/null || true
    echo "Subshell completed without triggering parent ERR"
)

risky_function

实际模式:原子替换文件

一种经典的防御性 Bash 模式是原子替换文件:先写入临时文件,然后将其重命名到目标位置。如果在重命名之前发生任何错误,原文件都不会受到影响。使用陷阱可以让这一过程更加可靠。

步骤:

  1. 在与目标文件相同的文件系统中创建临时文件(这样 mv 才是原子的)。
  2. 注册 EXIT 陷阱,以便提前退出时删除临时文件。
  3. 写入并验证新内容。
  4. 执行原子重命名(mv)——到这时才替换原文件。
  5. EXIT 陷阱仅在临时文件仍存在时删除它(mv 成功后,临时文件已经不存在)。
#!/usr/bin/env bash
set -euo pipefail

DEST="/tmp/important_config.conf"
TMPFILE=$(mktemp "$(dirname "$DEST")/.tmp.XXXXXX")

cleanup() {
    rm -f "$TMPFILE"   # no-op if mv already moved it
}
trap cleanup EXIT

# Write new content to temp file
cat > "$TMPFILE" <<'EOF'
[settings]
version=2
mode=production
EOF

# Validate before replacing
if ! grep -q 'version=' "$TMPFILE"; then
    echo "Validation failed — aborting replacement" >&2
    exit 1
fi

# Atomic rename — DEST is replaced only here
mv "$TMPFILE" "$DEST"
echo "Config updated atomically: $DEST"

知识检查:ERR 陷阱的继承

测试您对 Bash 中 ERR 陷阱如何处理函数的理解。

回顾:用于清理和处理信号的陷阱处理程序

在本课中,您学习了如何通过陷阱处理程序增强 Bash 脚本的健壮性:

  • EXIT 是您的通用安全网——创建任何临时资源后立即注册它,并始终先捕获 $?,以保留原始退出代码。
  • ERR 让您可以使用 $LINENO 和 $BASH_COMMAND 记录失败;使用 set -E 让函数继承它。
  • INT 可以优雅地处理 Ctrl+C——输出消息并以代码 130 退出,让 EXIT 负责清理。
  • 将临时资源收集到数组中,并在一个清理函数中统一删除。
  • 使用 trap - SIGNAL 进行重置,使用 trap '' SIGNAL 动态忽略信号。
  • 原子重命名模式(mktemp → 写入 → 验证 → mv)结合 EXIT 陷阱,可以确保不完整的写入永远不会损坏目标文件。

将 set -eEuo pipefail 与恰当设置的陷阱结合起来,可以为任何生产 Bash 脚本提供坚实的防御性基础。

免费开始

用 AI 导师学习 DevOps Bootcamp — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
142
课程
568

常见问题解答

「用于清理和处理信号的陷阱处理器」课时是免费的吗?

是的 — 「用于清理和处理信号的陷阱处理器」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。

「用于清理和处理信号的陷阱处理器」这节课中我会学到什么?

注册 EXIT、ERR 和 INT 陷阱,可靠地删除临时文件并回滚未完成的工作。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 DevOps Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「用于清理和处理信号的陷阱处理器」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 DevOps Bootcamp 课中编写并运行代码吗?

能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 set -euo pipefail 启用严格模式
  2. 用于清理和处理信号的陷阱处理器
  3. 安全的临时文件与锁目录
  4. 幂等脚本与带退避的重试逻辑
← 返回 DevOps Bootcamp