意外退出bash时如何删除已创建的临时文件

127

我正在用bash脚本创建临时文件。在处理结束后,我会删除这些文件,但由于脚本运行时间较长,如果我在运行期间终止它或仅使用CTRL-C,那么临时文件就不会被删除。
有没有办法在执行结束前捕获这些事件并清除文件?

此外,是否有一些关于命名和位置的最佳实践来处理这些临时文件?
我目前不确定是使用:

TMP1=`mktemp -p /tmp`
TMP2=`mktemp -p /tmp`
...

TMP1=/tmp/`basename $0`1.$$
TMP2=/tmp/`basename $0`2.$$
...

或者也许有更好的解决方案吗?


交叉引用:https://unix.stackexchange.com/a/181938/9340 - hakre
8个回答

160

我通常创建一个目录来存放所有的临时文件,然后立即创建一个EXIT处理程序,在脚本退出时清理这个目录。

MYTMPDIR="$(mktemp -d)"
trap 'rm -rf -- "$MYTMPDIR"' EXIT

如果您将所有临时文件放在$MYTMPDIR下,那么在大多数情况下,当您的脚本退出时它们都会被删除。但是,使用SIGKILL(即kill -9)立即终止进程,这种情况下您的EXIT处理程序将不会运行。


32
一定要在程序退出时使用trap,而不是使用无用的TERM/INT/HUP/或其他任何信号。不过,记得要使用引号来引用参数扩展,并且我也建议你在trap中使用单引号: trap 'rm -rf "$TMPDIR"' EXIT - lhunath
9
使用单引号,因为如果在脚本的后续部分决定更改TMPDIR时,您的陷阱仍将起作用。 - lhunath
3
@AaronDigulla 为什么$()和反引号很重要? - Ogre Psalm33
4
命令和命令(加括号的“命令”)在Unix shell编程中有着不同的含义。命令是指可以从shell提示符下输入的可执行程序、脚本或shell内置的命令,它们会运行并将输出发送到标准输出。而命令(加括号的“命令”)是指将一系列命令放在一个子shell中运行,该子shell具有自己的环境,因此任何更改(例如变量赋值)都不会影响父shell。 - Aaron Digulla
1
@lhunath:单引号陷阱语法是什么?对于单引号内的双引号并不熟悉。认为在这种语法中,双引号没有特殊含义。你提出的语法是否将字符串未经评估地发送到trap,然后在EXIT上进行评估? - Alexander Torstling
4
代码应该始终使用单引号以防止注入导致任意代码执行。如果您将数据扩展到bash代码字符串中,那么该数据现在可以执行代码所能执行的任何操作,这可能会导致与空格相关的错误和破坏性错误(例如因奇怪原因清除您的主目录或引入安全漏洞)。请注意,trap接受一个bash代码字符串,稍后将按原样评估该字符串。因此,当陷阱触发时,单引号将消失,只剩下语法上的双引号。 - lhunath

117

你可以设置一个 "trap" 以在退出或按下控制-C时执行清理操作。

trap '{ rm -f -- "$LOCKFILE"; }' EXIT

另外,我最喜欢的Unix命令之一是打开文件,然后在文件仍然处于打开状态时将其删除。文件仍然存在于文件系统中,您可以读取和写入它,但是一旦程序退出,该文件就会消失。不过我不确定如何在bash中实现这一点。

顺便说一句:支持使用mktemp而不是使用自己的解决方案的一个理由是:如果用户预计您的程序将创建巨大的临时文件,他可能希望将 TMPDIR 设置为更大的位置,例如 /var/tmp。 mktemp 可以识别到这一点,而手动编写的解决方案(第二个选项)则不能。例如,我经常使用 TMPDIR=/var/tmp gvim -d foo bar


8
使用Bash,exec 5<>$TMPFILE 将文件描述符5绑定到 $TMPFILE,使其可读可写,并可以在之后使用 <&5>&5/proc/$$/fd/5 (Linux)。唯一的问题是Bash缺乏 seek 函数… - ephemient
6
关于“trap”的一些注释:由于设计上会立即终止执行程序,因此无法捕获“SIGKILL”(请注意,这是故意的)。因此,如果可能会发生这种情况,请备有应急计划(例如“tmpreaper”)。其次,陷阱不是累积性的——如果您有多个操作要执行,则它们必须全部包含在“trap”命令中。解决多个清理操作的方法之一是定义一个函数(如有必要,您可以在程序运行时重新定义它),并引用该函数:“trap cleanup_function EXIT”。 - Toby Speight
4
Shellcheck建议使用单引号,因为双引号会在现在被扩展,而不是在触发陷阱时才被扩���。 - LaFayette
1
你为什么在这里使用大括号? - CMCDragonkai
1
@CMCDragonkai 从链接的手册页中复制。可能是为了在陷阱中使用多个命令。 - Paul Tomblin
显示剩余3条评论

30

你想要使用trap命令来处理退出脚本或者类似CTRL-C的信号。详细信息请参考Greg's Wiki

对于你的临时文件,使用basename $0是个好主意,同时提供一个可以容纳足够多临时文件的模板:

tempfile() {
    tempprefix=$(basename "$0")
    mktemp /tmp/${tempprefix}.XXXXXX
}

TMP1=$(tempfile)
TMP2=$(tempfile)

trap 'rm -f $TMP1 $TMP2' EXIT

1
不要在TERM/INT上陷阱。在EXIT上陷阱。基于接收到的信号来预测退出条件是愚蠢的,绝对不是一个万无一失的方法。 - lhunath
3
小细节:使用$()而不是单引号反引号。同时,在$0周围加上双引号,因为它可能包含空格。 - Aaron Digulla
嗯,在这个评论中反引号很好用,但是这是一个公正的观点,养成使用 $() 的习惯是很好的。我也添加了双引号。 - Brian Campbell
1
你可以用TMP1=$(tempfile -s "XXXXXX")来替换整个子程序。 - Ruslan Kabalin
4
并非所有系统都有“tempfile”命令,而我所知道的所有合理的现代系统都有“mktemp”命令。 - Brian Campbell

16

请记住所选答案是bashism,这意味着解决方案为

trap "{ rm -f $LOCKFILE }" EXIT

如果您只想在bash中运行(如果shell是dash或经典的sh,它将无法捕获Ctrl+c),但如果您需要兼容性,则仍然需要枚举要捕获的所有信号。

还要记住,当脚本退出时,对于信号“0”(又名EXIT),trap始终执行,导致trap命令被重复执行。

这就是如果存在EXIT信号,则不将所有信号堆叠在一行中的原因。

为了更好地理解,请查看以下脚本,它可以在不同的系统上工作而无需更改:

#!/bin/sh

on_exit() {
  echo 'Cleaning up...(remove tmp files, etc)'
}

on_preExit() {
  echo
  echo 'Exiting...' # Runs just before actual exit,
                    # shell will execute EXIT(0) after finishing this function
                    # that we hook also in on_exit function
  exit 2
}


trap on_exit EXIT                           # EXIT = 0
trap on_preExit HUP INT QUIT TERM STOP PWR  # 1 2 3 15 30


sleep 3 # some actual code...

exit 

使用此解决方案将更具控制性,因为您可以在实际信号发生之前运行一些代码(preExit函数),并且如果需要,在实际退出信号(退出的最后阶段)时运行一些代码。


8

良好的习惯是美丽的

  • 避免假设一个变量的值在很长时间内不会被更改,尤其是这种错误会引发错误的情况。

  • 如果适用于您的代码,请使用陷阱(trap)立即展开变量的值。任何以单引号传递给trap的变量名将延迟展开其值直到catch之后。

  • 避免假定文件名不包含任何空格。

  • 使用 Bash 的 ${VAR@Q}$(printf '%q' "$VAR") 来解决由于文件名中出现空格和其他特殊字符(如引号和回车符)而引起的问题。

    zTemp=$(mktemp --tmpdir "$(basename "$0")-XXX.ps")
    trap "rm -f ${zTemp@Q}" EXIT

2
需要注意的是${parameter@operator}扩展是在Bash 4.4中添加的(于2016年9月发布)。 - Robin A. Meade
2
Bash < v4.4 的等效写法:trap "rm -f $(printf %q "$zTemp")" EXIT - Robin A. Meade
这是最佳答案,因为无需担心 zTemp 的值在脚本后面被更改。此外,zTemp 可以声明为函数的 local 变量;它不必是全局脚本变量。 - Robin A. Meade

5
使用可预测的文件名与$$的替代方案存在严重的安全漏洞,您永远不应该考虑使用它。即使只是在您的单用户PC上运行一个简单的个人脚本,这也是一个非常糟糕的习惯,您不应该养成。 BugTraq 充满了“不安全的临时文件”事件。有关临时文件安全方面的更多信息,请参见此处此处此处
我最初考虑引用不安全的TMP1和TMP2分配,但经过再三考虑,这可能不是一个好主意

1
如果可以的话,我会点赞两次:一次是为了安全建议,另一次是为了不引用坏主意和参考资料。 - TMG

2

我更喜欢使用tempfile,它可以在安全的方式下创建一个文件在/tmp目录中,您不必担心文件命名的问题:

tmp=$(tempfile -s "your_sufix")
trap "rm -f '$tmp'" exit

tempfile很不可移植,尽管更安全,但最好避免使用它或至少模拟它。 - lericson

-5

你不必费心去删除使用 mktemp 创建的那些临时文件。它们稍后会被自动删除。

如果可以的话,尽量使用 mktemp,因为它生成的文件比 '$$' 前缀更加独特。而且它看起来是一种更跨平台的创建临时文件的方式,而不是明确地将它们放入 /tmp 中。


操作删除|文件系统本身在一段时间后删除 - Mykola Golubyev
4
魔法?定期作业?还是重新启动的Solaris机器? - innaM
可能其中之一。如果临时文件没有被某些中断删除(这种情况不会太频繁),总有一天临时文件将被删除 - 这就是为什么它们被称为临时文件的原因。 - Mykola Golubyev
25
不能、不应该、必须不要假设放在/tmp目录下的东西会永远留在那里;同时,也不要假设它会神奇地消失。 - innaM

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接