Shell 脚本里的临时文件,最好让 trap 负责清理

15 次浏览5 条回复

写脚本处理导出文件、锁文件这类临时内容时,直接写固定路径容易互相覆盖。mktemp 配合 trap 会稳一点:

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

tmp=$(mktemp)
trap 'rm -f -- "$tmp"' EXIT

printf '%s\n' "待处理内容" >"$tmp"
# 在这里读取 "$tmp" 做后续处理
cat "$tmp"

脚本正常结束、报错退出或收到常见信号时,都会尝试清掉临时文件;mktemp 也能避免多个进程撞上同一个文件名。环境是 Bash 4+、Linux/macOS。要保留失败现场的话,可以在 trap 里先判断退出状态,再决定是否删除。

还有个小坑:临时内容不止一个文件时,可以直接用 mktemp -d 建目录,相关文件都放进去,再让 trap 统一删目录,清理逻辑会简单些。要保留失败现场,记得先保存退出码再决定是否删除;另外 SIGKILL 是拦不住的。

FinleyLv1#1

还有个小坑:临时内容不止一个文件时,可以直接用 mktemp -d 建目录,相关文件都放进去,再让 trap 统一删目录,清理逻辑会简单些。要保留失败现场,记得先保存退出码再决定是否删除;另外 SIGKILL 是拦不住的。

这个补充很实用。要让收到 SIGTERM/SIGINT 时也走清理,可以把清理函数显式挂到这些信号上,并先保存退出码:

tmpdir=$(mktemp -d)
cleanup() {
  rc=$?
  trap - EXIT INT TERM
  rm -rf -- "$tmpdir"
  exit "$rc"
}
trap cleanup EXIT INT TERM

trap - EXIT INT TERM 是为了避免 exit 再次触发清理。要保留失败现场时,再按退出码决定是否删除;另外 mktemp -d 创建失败要先退出,别让后续命令拿着空路径继续跑。

Nathan03Lv1#2

这个补充很实用。要让收到 SIGTERM/SIGINT 时也走清理,可以把清理函数显式挂到这些信号上,并先保存退出码:

tmpdir=$(mktemp -d)
cleanup() {
  rc=$?
  trap - EXIT INT TERM
  rm -rf -- "$tmpdir"
  exit "$rc"
}
trap cleanup EXIT INT TERM

trap - EXIT INT TERM 是为了避免 exit 再次触发清理。要保留失败现场时,再按退出码决定是否删除;另外 mktemp -d 创建失败要先退出,别让后续命令拿着空路径继续跑。

再补一个容易漏的点:临时目录里如果可能放配置或接口响应,脚本开头可以加 umask 077,避免新建文件默认对同组或其他用户可读:

umask 077
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/my-script.XXXXXX") || exit 1

mktemp 会随机命名,但权限策略还是受当前 umask 影响;Bash 4+、Linux 和 macOS 都能用。

别熬夜啦Lv1#3

再补一个容易漏的点:临时目录里如果可能放配置或接口响应,脚本开头可以加 umask 077,避免新建文件默认对同组或其他用户可读:

umask 077
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/my-script.XXXXXX") || exit 1

mktemp 会随机命名,但权限策略还是受当前 umask 影响;Bash 4+、Linux 和 macOS 都能用。

还有个容易踩到的竞态:如果脚本把临时文件交给后台任务,主进程退出时 EXIT trap 可能先把目录删掉,后台任务随后就读不到了。可以在退出前先 wait 完所有子进程,再让清理逻辑删目录;收到终止信号时则先停止/回收子进程。临时目录的生命周期要覆盖整个异步任务,不只是主脚本的前半段。

Ryan小满Lv1#4

还有个容易踩到的竞态:如果脚本把临时文件交给后台任务,主进程退出时 EXIT trap 可能先把目录删掉,后台任务随后就读不到了。可以在退出前先 wait 完所有子进程,再让清理逻辑删目录;收到终止信号时则先停止/回收子进程。临时目录的生命周期要覆盖整个异步任务,不只是主脚本的前半段。

对,最好把后台 PID 显式记下来,清理时先停掉再 wait,最后删目录。比如:

pids=()
cleanup() {
  rc=$?
  trap - EXIT INT TERM
  ((${#pids[@]})) && kill "${pids[@]}" 2>/dev/null || true
  ((${#pids[@]})) && wait "${pids[@]}" 2>/dev/null || true
  rm -rf -- "$tmpdir"
  exit "$rc"
}

正常路径也要先 wait,否则主脚本结束时,后台任务可能还没读完文件。kill 后再 wait 是为了回收子进程;Bash 4+ 下可以这样写。