用于清理和处理信号的陷阱处理器
注册 EXIT、ERR 和 INT 陷阱,可靠地删除临时文件并回滚未完成的工作。
用于清理和处理信号的陷阱处理器 是 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 - PIPEEXIT 陷阱——您的安全网
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 模式是原子替换文件:先写入临时文件,然后将其重命名到目标位置。如果在重命名之前发生任何错误,原文件都不会受到影响。使用陷阱可以让这一过程更加可靠。
步骤:
- 在与目标文件相同的文件系统中创建临时文件(这样
mv才是原子的)。 - 注册 EXIT 陷阱,以便提前退出时删除临时文件。
- 写入并验证新内容。
- 执行原子重命名(
mv)——到这时才替换原文件。 - 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 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 set -euo pipefail 启用严格模式
- 用于清理和处理信号的陷阱处理器
- 安全的临时文件与锁目录
- 幂等脚本与带退避的重试逻辑