Linux 基础体系 · 第 44/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Bash 函数与 Trap:退出码、错误传播、临时资源和信号清理
在 Bash 中,一个函数调用同时涉及三件容易混淆的事情:
- 函数内部每条命令都有自己的退出状态;
- 函数整体也必须向调用者返回一个退出状态;
trap可以在特定事件发生时插入处理逻辑,但它不会自动替你传播错误、恢复状态或保证资源清理成功。
如果把这三者混为一谈,就会出现“函数明明失败了却返回 0”“开启了 set -e 仍然继续执行”“临时目录没有清理”“收到 SIGTERM 后进程仍然残留”等问题。
本文使用 Bash 5.x 常见行为说明机制。trap、函数和退出状态属于 Bash 语言能力;mktemp、信号编号和 /tmp 的权限则依赖具体系统与实现。
1. 退出状态:命令、函数和脚本之间如何传递结果
1.1 $? 是上一条命令的退出状态
Bash 命令的退出状态是一个整数,通常取值范围为 0 到 255:
0表示成功;- 非
0表示失败或特殊状态; - 对于被信号终止的进程,Shell 通常使用
128 + 信号编号作为状态,例如SIGINT为 2,常见结果是130。
true
printf 'status=%s\n' "$?"
false
printf 'status=%s\n' "$?"
预期输出:
status=0
status=1
$? 不是一个可以长期保存的日志变量。任何命令都会改变它,包括 printf、local、[[ ... ]] 以及 echo。因此,必须在需要时立即保存:
do_work() {
some_command
local rc=$?
printf 'some_command returned %s\n' "$rc" >&2
return "$rc"
}
下面的写法存在风险:
do_work() {
some_command
printf 'command failed\n' >&2
return "$?"
}
printf 通常返回 0,所以这里的 return "$?" 很可能返回的是 printf 的状态,而不是 some_command 的状态。
1.2 函数默认返回最后一条命令的状态
函数没有显式执行 return 时,其退出状态等于函数体中最后执行的命令的退出状态:
f() {
false
printf 'failure was ignored\n'
}
f
printf 'function status=%s\n' "$?"
输出:
failure was ignored
function status=0
false 的失败状态被后面的 printf 覆盖了。函数并不是“自动传播最早发生的错误”,而是只向调用者暴露最终状态。
正确的显式传播方式是:
f() {
false || return
printf 'this line is reached only after success\n'
}
这里 return 不带参数时,会返回紧邻它之前的命令状态。在更复杂的代码中,显式保存更清楚:
f() {
false
local rc=$?
if (( rc != 0 )); then
printf 'f: operation failed, rc=%d\n' "$rc" >&2
return "$rc"
fi
}
return N 只应在函数或被函数调用的脚本环境中使用。它的状态会按 Shell 的退出状态规则转换到 8 位范围;应用协议如果需要表达更多错误信息,应使用输出、日志或结构化结果,而不要试图把任意大整数塞进退出码。
1.3 return、exit 和自然结束不是一回事
return结束当前函数,控制权回到调用者;exit结束当前 Shell 进程;- 函数自然执行到末尾,则返回最后一条命令的状态;
- 脚本自然执行到末尾,也以最后一条命令的状态作为脚本状态。
inner() {
return 7
printf 'unreachable\n'
}
outer() {
inner
printf 'inner status was overwritten\n'
}
outer
printf 'outer status=%s\n' "$?"
inner 返回 7,但 outer 最后的 printf 成功,因此 outer 返回 0。
如果函数是包装器,目标通常是“成功则继续,失败则原样返回”:
run_task() {
prepare_environment || return
execute_task || return
publish_result
}
这段代码中,每个 || return 都让失败沿函数调用链向上传播。
1.4 local 和命令替换可能遮蔽退出状态
这是 Bash 中很常见的错误:
bad() {
local output=$(false)
printf 'status=%s\n' "$?"
}
local 是一个 Bash 内建命令,它本身通常返回成功;因此,$(false) 的状态被 local 的状态遮蔽。
应分成两步:
good() {
local output
if ! output=$(false); then
printf 'command substitution failed\n' >&2
return 1
fi
}
更一般地说,凡是需要检查某条命令的状态,都不要把它嵌入另一个可能成功的命令中。
命令替换还会创建执行环境,并删除替换结果末尾的换行符:
value=$(printf 'a\n\n')
printf '<%s>\n' "$value"
输出通常是:
<a>
这属于 Bash 展开和引用问题:退出状态由命令替换中的最后一条命令决定,文本结果则经过命令替换规则处理,二者是不同的数据流。
2. 组合命令和管道的退出状态
2.1 if、while、&& 和 || 都会检查状态
这些结构并不是“忽略错误”,而是明确把命令状态作为条件使用:
if create_file; then
printf 'created\n'
else
printf 'create failed\n' >&2
fi
if 条件中的命令失败并不自动导致脚本退出,因为失败正是分支判断的一部分。
step_a && step_b
step_a || recover
对于 A && B:
- 如果
A返回非 0,B不执行,整体状态是A的状态; - 如果
A成功,整体状态是B的状态。
对于 A || B:
- 如果
A成功,B不执行,整体状态通常是 0; - 如果
A失败,执行B,整体状态是B的状态。
因此:
false || echo "recovered"
printf 'status=%s\n' "$?"
最后状态是 echo 的 0,而不是 false 的 1。如果恢复动作本身不应改变原始失败,需要单独保存状态。
2.2 管道默认只报告最后一段命令
false | true
printf 'status=%s\n' "$?"
默认输出:
status=0
管道状态等于最后一条命令 true 的状态,前面的 false 被隐藏。启用 pipefail 后,规则变为:
- 如果所有命令成功,管道状态为 0;
- 否则返回管道中最右侧失败命令的状态。
set -o pipefail
false | true
printf 'status=%s\n' "$?"
结果为非 0。
如果需要诊断每个阶段,必须立即读取 PIPESTATUS,因为下一条命令会覆盖它:
set -o pipefail
producer | transformer | consumer
pipeline_rc=$?
stage_statuses=("${PIPESTATUS[@]}")
printf 'pipeline=%s stages=%s\n' \
"$pipeline_rc" "${stage_statuses[*]}" >&2
PIPESTATUS 是当前 Shell 最近一次前台管道中各命令的状态数组。保存 $? 和保存 PIPESTATUS 都应紧接管道执行;中间插入其他命令会使诊断结果失真。
3. set -e:自动退出不是完整的错误传播机制
3.1 errexit 的基本目标
set -e
false
printf 'unreachable in this simple case\n'
在最简单的顶层场景中,false 会使 Shell 退出。
但 Bash 的 errexit 不是“任何非 0 都退出”。为了支持条件判断,以下位置通常属于例外:
if或while的条件命令;&&和||链中不处于最终决定位置的命令;!后面的命令;- 某些管道成员,除非启用
pipefail; - 被调用函数位于这些条件上下文中时,其内部命令也可能受到该上下文影响。
例如:
set -e
if false; then
printf 'not reached\n'
fi
false || printf 'failure was handled\n'
printf 'script continues\n'
输出:
failure was handled
script continues
这里的失败被 Shell 视为控制流的一部分。
3.2 函数调用处的条件上下文会影响函数体
这是 set -e 最容易产生意外的地方:
set -e
work() {
false
printf 'work continued\n'
}
if work; then
printf 'success\n'
fi
在常见 Bash 行为中,work 整体位于 if 条件中,因此函数体内的 false 也可能不触发 errexit,于是会打印:
work continued
success
这不是函数自动吞掉了错误,而是调用上下文告诉 Bash:这个函数的失败正在被条件结构检查。
所以不能仅凭“脚本开了 set -e”推断每次失败都立即终止。可靠的核心逻辑仍应使用显式检查:
work() {
false || {
printf 'work: failed\n' >&2
return 1
}
printf 'work continued\n'
}
3.3 命令替换中的 errexit
传统 Bash 行为中,命令替换 $(...) 内部通常会清除 -e,除非启用了 inherit_errexit 或处于 POSIX 模式:
set -e
value=$(false; printf 'still running inside substitution\n')
printf 'outer shell continues\n'
不同 Bash 版本、启动选项和 POSIX 模式可能改变此行为。即使开启:
shopt -s inherit_errexit
也不应把复杂错误处理建立在隐含继承规则上。更清楚的方式是让命令替换直接成为被检查的命令:
if ! value=$(generate_value); then
printf 'generate_value failed\n' >&2
exit 1
fi
3.4 推荐的错误传播模型
set -e 可以作为遗漏检查的辅助保护,但不应取代函数契约。一个明确的函数通常满足:
fetch() {
local destination=$1
curl --fail --silent --show-error \
--output "$destination" \
'https://example.invalid/data' ||
return
}
main() {
local file
file=$1
fetch "$file" || {
printf 'fetch failed: %s\n' "$file" >&2
return 1
}
process "$file" || return
}
main "$@"
这里每一层都定义了失败边界:
curl失败,fetch返回失败;main发现fetch失败后记录上下文;- 顶层调用者决定脚本最终退出码。
相比“所有地方都依赖 set -e”,这种方式更容易测试,也更容易区分可恢复错误和不可恢复错误。
4. ERR Trap:记录错误,不等于处理错误
4.1 ERR 何时触发
ERR trap 会在某些命令返回非 0 时执行,但它遵循与 errexit 类似的条件例外。也就是说,以下失败通常不会触发 ERR:
if false; then
:
fi
false || :
因此,ERR 不是“所有失败事件的全局审计钩子”。
一个基础诊断器如下:
#!/usr/bin/env bash
set -E
trap 'printf "ERR: status=%d command=%q function=%s line=%s\n" \
"$?" "$BASH_COMMAND" "${FUNCNAME[1]:-main}" "${BASH_LINENO[0]:-?}" >&2' ERR
fail() {
false
}
fail
set -E 等价于 set -o errtrace,使 ERR trap 继承到函数、命令替换和子 Shell 等执行环境。没有它时,ERR trap 默认不会以同样方式继承到函数调用层次中。
处理器中必须先保存 $?。例如:
trap 'rc=$?; printf "failed rc=%d\n" "$rc" >&2; return "$rc"' ERR
这段代码并不适合作为通用顶层 trap:在不同执行环境中使用 return 可能没有合法的函数上下文。更稳妥的诊断 trap 通常只记录信息,不主动改变控制流:
on_err() {
local rc=$?
printf 'error: rc=%d command=%q\n' \
"$rc" "$BASH_COMMAND" >&2
}
trap on_err ERR
如果同时使用 set -e,失败后是否退出由 errexit 决定,而不是由 ERR trap 的存在决定。
4.2 ERR、errexit 和 pipefail 的关系
set -E
set -o pipefail
on_err() {
local rc=$?
printf 'ERR: rc=%d command=%q\n' "$rc" "$BASH_COMMAND" >&2
}
trap on_err ERR
false | true
没有 pipefail 时,管道整体为 0,通常不会触发 ERR;启用 pipefail 后,管道整体失败,才有失败事件可供 ERR 观察。
这说明错误信息沿着状态计算传播:
各管道成员状态
↓
pipefail 计算管道状态
↓
ERR / errexit 根据管道状态决定是否动作
ERR 记录的是 Shell 认为失败的命令或组合命令,不一定等价于底层每个进程的独立状态。因此,复杂管道仍应结合 PIPESTATUS 诊断。
5. trap 的基本模型:在事件发生时执行 Shell 代码
5.1 安装和取消 Trap
trap 'printf "shell is exiting\n" >&2' EXIT
trap - EXIT
trap action signal... 为一个或多个事件安装处理动作,trap - signal... 恢复默认行为。
建议使用单引号安装包含变量的动作:
trap 'printf "file=%s\n" "$file"' EXIT
此时 $file 在 trap 真正执行时展开。若使用双引号:
trap "printf 'file=%s\n' '$file'" EXIT
变量可能在安装 trap 的瞬间就被展开,路径中的特殊字符也可能改变最终 Shell 代码。更安全、可维护的方式是让 trap 调用函数:
cleanup() {
printf 'cleaning up\n' >&2
}
trap cleanup EXIT
函数名作为 trap 动作不会提前展开复杂数据,也便于单独测试。
5.2 EXIT Trap 不是信号处理器
EXIT 在当前 Bash Shell 即将退出时执行,常用于释放临时资源:
trap cleanup EXIT
它覆盖的是正常结束、显式 exit,以及许多由 Shell 自己触发的退出路径。但它不能拦截所有终止方式:
SIGKILL(9)不能被捕获或忽略;SIGSTOP(19,在 Linux 上)不能被捕获或忽略;- 机器掉电、内核崩溃、强制容器销毁等情况不会给 Bash 执行清理代码的机会;
- 进程被
kill -9终止时,EXIT不会运行。
因此,清理逻辑只能提供“尽力而为”的资源回收,不能替代幂等设计、启动时过期资源回收和外部生命周期管理。
6. 临时资源清理:创建、登记、释放和保留原始状态
6.1 使用 mktemp 创建私有临时目录
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXXXX") || {
printf 'cannot create temporary directory\n' >&2
exit 1
}
mktemp -d 会请求创建一个新的临时目录,模板末尾的 X 由实现替换为随机字符。主流 Linux 发行版通常提供 util-linux 的 mktemp;它不是所有历史 Shell 环境都保证存在的 POSIX Shell 内建命令。
使用它而不是手工拼接:
tmpdir="/tmp/myapp.$$"
mkdir "$tmpdir"
原因是 PID 可预测,目录可能被提前创建或被符号链接替换。mktemp 的原子创建能力可以降低这类竞态风险,但仍需注意:
- 不要把用户可控字符串直接拼入
rm -rf; - 必须始终引用路径;
- 应验证临时路径确实由当前进程创建;
- 临时目录中可能包含敏感数据,权限和挂载点需要结合部署环境检查;
/tmp可能被定期清理,也可能是内存文件系统,不能假设永久存在。
6.2 EXIT 清理函数应保持幂等
幂等意味着清理执行一次或多次,最终结果相同。典型实现:
#!/usr/bin/env bash
set -u
tmpdir=
cleanup() {
local rc=$?
if [[ -n ${tmpdir:-} && -d $tmpdir ]]; then
rm -rf -- "$tmpdir" || {
printf 'warning: cannot remove %q\n' "$tmpdir" >&2
}
fi
return "$rc"
}
trap cleanup EXIT
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/myapp.XXXXXXXX") || {
printf 'cannot create temporary directory\n' >&2
exit 1
}
printf 'temporary directory: %s\n' "$tmpdir"
printf 'result\n' >"$tmpdir/result.txt"
这里的关键步骤是:
- 先将
tmpdir初始化为空; - 安装 trap,使后续失败也能触发清理;
- 成功创建后再写入目录变量;
- 清理时检查变量非空且路径是目录;
- 使用
--防止以-开头的路径被解释为选项; - 保存进入
cleanup时的退出状态; - 清理失败只记录警告,不覆盖原始业务错误。
如果 rm 失败后直接让 cleanup 返回 rm 的状态,脚本原本的业务失败可能被清理失败覆盖,或者原本成功的脚本因为清理异常变成失败。是否让清理失败改变最终状态,应由业务要求明确决定,而不能偶然由最后一条命令决定。
6.3 只在资源确实属于当前进程时删除
下面这种写法风险极高:
rm -rf "$tmpdir"
如果变量为空、被覆盖,或者包含错误路径,后果可能是灾难性的。最少应做到:
if [[ ${tmpdir:-} == /tmp/myapp.* && -d ${tmpdir:-} ]]; then
rm -rf -- "$tmpdir"
fi
不过字符串前缀检查不是完整的安全边界。生产代码还应避免让路径变量被外部输入直接控制,并在创建后保存明确的资源所有权信息。高权限脚本尤其不能把 TMPDIR、命令行参数或配置文件中的目录未经校验地交给递归删除命令。
7. 信号清理:从收到信号到退出的完整路径
7.1 常见信号与处理限制
常见信号包括:
| 信号 | 常见编号 | 含义 | 能否捕获 |
|---|---|---|---|
SIGHUP |
1 | 终端挂断或会话结束 | 能 |
SIGINT |
2 | 交互中断,通常来自 Ctrl-C |
能 |
SIGTERM |
15 | 请求进程优雅终止 | 能 |
SIGKILL |
9 | 强制终止 | 不能 |
SIGSTOP |
19 | 强制暂停 | 不能 |
信号编号在 Linux 上通常如表所示,但程序中应优先使用信号名,而不是假设数字在所有 Unix 系统上都相同。
清理的通常流程如下:
flowchart TD
A[进程运行] --> B{收到事件}
B -->|正常结束或 exit| C[EXIT trap]
B -->|SIGINT/SIGTERM/SIGHUP| D[信号 trap]
D --> E[设置业务退出码]
E --> C
C --> F[删除临时资源]
F --> G[进程结束]
B -->|SIGKILL/掉电| H[无法执行 Shell 清理]
信号 handler 通常不直接承担全部清理工作,而是设置退出路径,让统一的 EXIT trap 执行一次清理。
7.2 一个可运行的信号清理示例
#!/usr/bin/env bash
set -u
tmpdir=
cleanup() {
local rc=$?
if [[ -n ${tmpdir:-} && -d $tmpdir ]]; then
rm -rf -- "$tmpdir" || {
printf 'warning: cleanup failed for %q\n' "$tmpdir" >&2
}
fi
return "$rc"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/signal-demo.XXXXXXXX") || {
printf 'cannot create temporary directory\n' >&2
exit 1
}
printf 'pid=%d dir=%s\n' "$$" "$tmpdir"
printf 'press Ctrl-C or run: kill -TERM %d\n' "$$"
while :; do
sleep 10
done
运行后,从另一个终端发送:
kill -TERM <pid>
预期行为是:
- Bash 执行
TERMtrap; exit 143设置退出状态;- Bash 执行
EXITtrap; cleanup删除临时目录;- 进程以状态 143 结束。
之所以不能只写:
trap cleanup TERM
是因为清理函数返回后,进程可能继续执行原来的业务逻辑。信号处理器必须明确决定“继续”“重新发送信号”还是“以某个状态退出”。
7.3 sleep、wait 与信号竞态
如果脚本正在等待子进程:
wait "$child_pid"
收到被捕获的信号后,wait 可能因信号中断并返回大于 128 的状态;具体表现还与 Bash 版本、信号到达时机和子进程状态有关。不要把任何大于 128 的状态都简单视为子进程业务失败,必须区分:
- 子进程自身退出;
- 当前 Shell 被信号打断等待;
- 当前 Shell 即将退出。
生产脚本常见的策略是:
- 信号 handler 设置停止标志或直接退出;
- 终止仍在运行的子进程;
wait等待它们退出;- 最后由
EXITtrap 清理临时资源。
如果子进程还会继续创建孙进程,单独杀掉一个 PID 可能不够,需要进程组、服务管理器或容器运行时提供的生命周期控制。
8. Trap 的作用域、子 Shell 和并发边界
8.1 函数共享当前 Shell 的 Trap
函数通常在当前 Shell 中执行。函数内安装的 trap 会影响当前 Shell 后续行为:
set_trap() {
trap 'printf "changed\n"' EXIT
}
set_trap
exit
因此,库式脚本或被其他脚本 source 的文件不应随意修改调用者的全局 trap,除非明确记录并恢复原状态。
8.2 子 Shell 不是独立的资源所有者
以下结构会创建子 Shell 或类似隔离执行环境:
(
command
)
value=$(command)
变量修改通常不会回写父 Shell:
x=parent
(
x=child
printf '%s\n' "$x"
)
printf '%s\n' "$x"
输出为:
child
parent
但外部资源不是变量副本。子 Shell 和父 Shell 仍可能访问同一个临时目录、文件描述符或锁文件。如果父 Shell和子 Shell都安装删除同一目录的清理逻辑,就可能出现:
- 一个 Shell 先删除目录,另一个清理时收到“文件不存在”;
- 子 Shell 删除了父 Shell仍在使用的文件;
- 并发清理与业务写入发生竞态。
因此,临时资源应明确由一个生命周期主体拥有。并发 worker 最好只处理目录中的文件,不要让每个 worker 都执行顶层目录删除。
Bash 对不同 trap 类型在函数、子 Shell、命令替换中的继承规则并不完全相同;ERR 是否继承明确受 errtrace 影响,信号 trap 也会受子 Shell 执行环境影响。涉及复杂并发时,应使用最小实验验证当前 Bash 版本,而不是凭“trap 是全局的”或“子 Shell 完全隔离”推断。
9. 常见错误写法与诊断方式
9.1 错误:在函数末尾无意中返回成功
check() {
test -f "$1"
echo "checked"
}
test 失败后,echo 成功,函数返回 0。
修正:
check() {
test -f "$1" || return
echo "checked"
}
9.2 错误:把 set -e 当作事务机制
set -e
begin_operation
may_fail
write_marker
set -e 不会回滚 begin_operation 已产生的副作用,也无法保证所有错误路径都退出。若需要回滚,必须显式记录状态:
changed=0
if make_change; then
changed=1
else
exit 1
fi
rollback() {
local rc=$?
if (( changed )); then
undo_change || printf 'rollback failed\n' >&2
fi
return "$rc"
}
trap rollback EXIT
这里仍需考虑回滚本身失败、信号中断和重复执行。Shell 没有自动事务语义。
9.3 错误:Trap 中覆盖了原始状态
cleanup() {
rm -rf -- "$tmpdir"
printf 'cleanup done\n'
}
trap cleanup EXIT
如果脚本原本以状态 7 退出,清理函数中的命令可能让最终观察到的状态变得不明确,尤其当 trap 中显式执行 exit 或发生新的失败时。
应先保存状态,并让清理动作尽量不改变它:
cleanup() {
local rc=$?
rm -rf -- "$tmpdir" || :
printf 'cleanup done\n' >&2
return "$rc"
}
9.4 错误:信号 handler 只清理、不退出
trap 'rm -rf -- "$tmpdir"' TERM
收到 SIGTERM 后,Shell 可能继续执行原逻辑。更完整的路径应当是:
trap 'exit 143' TERM
trap cleanup EXIT
若清理需要先通知子进程、等待其退出,再退出,则应在专门的信号函数中实现,并防止重复进入。
9.5 用 ShellCheck 和故障注入验证
ShellCheck 能发现许多与本文直接相关的问题,例如:
- 未引用变量;
local x=$(cmd)造成状态遮蔽;- trap 字符串中的变量提前展开;
rm参数和数组展开存在风险;- 未检查命令替换结果。
可以把脚本保存为 demo.sh 后运行:
shellcheck demo.sh
bash -n demo.sh
bash -n 只检查语法,不执行命令;ShellCheck 也不能证明信号竞态和资源生命周期一定正确。
实际测试还应故意制造故障:
bash demo.sh
printf 'exit=%s\n' "$?"
# 另一个终端中发送:
kill -TERM <pid>
验证项目包括:
- 业务失败是否保留原始退出码;
ERR日志是否包含正确的BASH_COMMAND;SIGTERM后是否以 143 结束;- 临时目录是否被删除;
- 清理失败时是否仍保留业务错误;
- 重复执行 cleanup 是否安全;
SIGKILL时是否留下资源,以及下一次启动能否识别并回收过期资源。
对于更复杂的函数行为,可以使用 Bats 测试函数的标准输出、标准错误和退出状态;对于临时目录、伪造命令和权限故障,则应使用独立 fixture,避免测试直接破坏真实系统目录。
10. 一个完整的函数、错误传播和清理骨架
下面示例把这些机制组合起来:
#!/usr/bin/env bash
set -u
set -o pipefail
tmpdir=
cleanup() {
local rc=$?
if [[ -n ${tmpdir:-} && -d $tmpdir ]]; then
rm -rf -- "$tmpdir" || {
printf 'warning: failed to remove %q\n' "$tmpdir" >&2
}
fi
return "$rc"
}
on_err() {
local rc=$?
printf 'error: rc=%d command=%q line=%s\n' \
"$rc" "$BASH_COMMAND" "${BASH_LINENO[0]:-?}" >&2
}
trap cleanup EXIT
trap on_err ERR
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
make_input() {
local path=$1
printf 'input\n' >"$path" || {
printf 'cannot write input: %s\n' "$path" >&2
return 1
}
}
process_input() {
local input=$1
local output=$2
if ! tr '[:lower:]' '[:upper:]' <"$input" >"$output"; then
printf 'processing failed\n' >&2
return 1
fi
}
main() {
local input output
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/pipeline-demo.XXXXXXXX") || {
printf 'cannot create work directory\n' >&2
return 1
}
input="$tmpdir/input"
output="$tmpdir/output"
make_input "$input" || return
process_input "$input" "$output" || return
cat "$output"
}
main "$@"
其状态流为:
make_input
└─失败 → return 非 0
process_input
└─失败 → return 非 0
main
└─向顶层返回失败
EXIT trap
└─保存失败状态并清理临时目录
这个骨架没有依赖 set -e 来推断函数内部控制流,而是显式定义每个函数的失败出口;ERR 只负责诊断;EXIT 负责资源生命周期;信号 trap 负责把终止请求转换为明确退出路径。四者职责分离后,脚本的行为才容易推导、测试和恢复。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Bash 展开与引用深入:变量、命令替换、通配、数组和 IFS
- 下一篇:Shell 脚本测试与质量:ShellCheck、Bats、Fixture 和故障注入
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论