Bash:用 trap 统一清理临时目录并保留退出状态

110 次浏览3 条回复

脚本创建临时目录后,如果中途命令失败或收到终止信号,散落在各分支里的清理代码很容易漏执行。可以把清理集中到 EXIT trap,并让信号处理先转换成约定的退出码。

环境:Bash 4.4+;Linux,或已安装新版 Bash 的 macOS;需要 mktemp 和 rm。

创建 cleanup-demo.sh:

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

workdir="$(mktemp -d)"

cleanup() {
  local status=$?
  trap - EXIT
  rm -rf -- "$workdir"
  printf 'cleaned %s (exit=%d)\n' "$workdir" "$status" >&2
  exit "$status"
}

trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

printf 'temporary data\n' > "$workdir/result.txt"
printf 'working in %s\n' "$workdir"

# 用 false 模拟中途失败;改成 true 可观察成功退出。
false

运行并检查退出码:

chmod +x cleanup-demo.sh
./cleanup-demo.sh
printf 'status=%d\n' "$?"

false 使脚本以 1 退出,cleanup 中进入函数时的 $? 就是该状态;先保存它,再删除临时目录,最后原样退出。trap - EXIT 用于避免函数内的 exit 再次触发同一个 trap。

将 false 攻成 true 后,清理仍会执行,但退出码为 0。收到 SIGINT 或 SIGTERM 时则分别转换为常见的 130 和 143,随后仍由 EXIT trap 完成同一套清理。实际脚本如果还要释放挂载点、停止子进程或删除多个资源,可以继续放进 cleanup,但应先保存状态码,并避免让某个清理失败覆盖原始结果。

这个模式很实用,不过示例中还有一个与“保留退出状态”直接相关的边界:脚本启用了 set -e,cleanup 里的 rm 或后续 printf 一旦失败,函数可能在执行 exit "$status" 前终止,最终状态就不再是最初保存的值。若目标是无条件保留业务退出码,可以在保存后关闭 errexit,并单独报告清理错误:

cleanup() {
  local status=$?
  local cleanup_status

  trap - EXIT
  set +e

  rm -rf -- "$workdir"
  cleanup_status=$?
  if (( cleanup_status != 0 )); then
    printf 'cleanup failed for %s (status=%d)\n' \
      "$workdir" "$cleanup_status" >&2
  fi

  exit "$status"
}

环境仍是正文的 Bash 4.4+。另一种策略是只在原状态为 0 时把清理失败作为最终失败:(( status == 0 && cleanup_status != 0 )) && status=$cleanup_status。这样业务已经失败时保留业务状态,业务成功但资源未清理时也不会误报成功。

还有一个初始化时序上的小窗口:正文先执行 mktemp -d,之后才注册 EXIT trap。如果脚本恰好在两步之间终止,临时目录不会进入清理流程。可以先用空值初始化变量、安装带保护条件的 trap,再分配资源。

环境:Bash 4.4+;Linux,或已安装新版 Bash 的 macOS;需要 mktemp 和 rm。

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

workdir=''

cleanup() {
  local status=$?
  trap - EXIT
  set +e

  if [[ -n $workdir ]]; then
    rm -rf -- "$workdir" ||
      printf 'cleanup failed for %s\n' "$workdir" >&2
  fi

  exit "$status"
}

trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

workdir=$(mktemp -d)
printf 'temporary data\n' >"$workdir/result.txt"
``

空字符串检查很重要:若 `mktemp` 本身失败,`set -e` 会触发 `EXIT` trap,但清理函数不会把未初始化路径交给 `rm`。同时,trap 已经在目录成功创建前生效,初始化失败与后续中断都走同一条收尾路径。
阿在呢Lv1#1

这个模式很实用,不过示例中还有一个与“保留退出状态”直接相关的边界:脚本启用了 set -e,cleanup 里的 rm 或后续 printf 一旦失败,函数可能在执行 exit "$status" 前终止,最终状态就不再是最初保存的值。若目标是无条件保留业务退出码,可以在保存后关闭 errexit,并单独报告清理错误:

cleanup() {
  local status=$?
  local cleanup_status

  trap - EXIT
  set +e

  rm -rf -- "$workdir"
  cleanup_status=$?
  if (( cleanup_status != 0 )); then
    printf 'cleanup failed for %s (status=%d)\n' \
      "$workdir" "$cleanup_status" >&2
  fi

  exit "$status"
}

环境仍是正文的 Bash 4.4+。另一种策略是只在原状态为 0 时把清理失败作为最终失败:(( status == 0 && cleanup_status != 0 )) && status=$cleanup_status。这样业务已经失败时保留业务状态,业务成功但资源未清理时也不会误报成功。

这里确实还留着一个与标题相反的边界:set -Eeuo pipefail 在 cleanup 里仍然生效。rm 或日志 printf 一旦失败,函数可能在最后的 exit "$status" 前被 errexit 打断,原始退出状态就保不住。清理函数最好显式接管清理错误,例如:

cleanup() {
  local status=$?
  local cleanup_status=0
  trap - EXIT
  set +e

  rm -rf -- "$workdir" || cleanup_status=$?
  printf 'cleaned %s (exit=%d)\n' "$workdir" "$status" >&2 || cleanup_status=$?

  if (( cleanup_status != 0 )); then
    printf 'cleanup failed (status=%d)\n' "$cleanup_status" >&2 || :
  fi
  exit "$status"
}

另外,若要覆盖 mktemp 成功后、安装 EXIT trap 前的极小窗口,可以先把 workdir 设为空并安装带空值判断的 trap,再创建临时目录。