mktemp 命令和 trap 命令

mktemp 命令和 trap 命令

Bash 脚本有时需要创建临时文件或临时目录。常见的做法是,在 /tmp 目录里面创建文件或目录,这样做有很多弊端,使用 mktemp 命令是最安全的做法。

临时文件的安全问题

直接创建临时文件,尤其在 /tmp 目录里面,往往会导致安全问题。

首先,/tmp 目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的。

# touch /tmp/info.txt && ls -l /tmp/info.txt
-rw-r--r-- 1 root root 0 Sep 10 15:27 /tmp/info.txt

上面命令在 /tmp 目录直接创建文件,该文件默认是所有人可读的。

其次,如果攻击者知道临时文件的文件名,他可以创建符号链接,链接到临时文件,可能导致系统运行异常。攻击者也可能向脚本提供一些恶意数据。因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。

最后,临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。

生成临时文件应该遵循下面的规则。

mktemp 命令的用法

mktemp 命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。

直接运行 mktemp 命令,就能生成一个临时文件。

# mktemp
/tmp/tmp.4GcsWSG4vj

# ll $(mktemp)
-rw------- 1 root root 0 Sep 10 15:29 /tmp/tmp.5TmEUUOL3X

上面命令中,mktemp 命令生成的临时文件名是随机的,而且权限是只有用户本人可读写。

Bash 脚本使用 mktemp 命令时,为了确保临时文件创建成功,mktemp 命令后面最好使用 OR 运算符(||),保证创建失败时退出脚本。

#!/bin/bash

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

为了保证脚本退出时临时文件被删除,可以使用 trap 命令指定退出时的清除操作。关于 trap 命令的使用,请看 trap 命令 小节

#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

mktemp 命令的参数

-d 参数可以创建一个临时目录。有时候脚本的每次运行产生的文件都需要隔离开,我们就会使用随机目录

$ mktemp -d
/tmp/tmp.t1OotJV4mE

-p 参数可以指定临时文件所在的目录。默认是使用 $TMPDIR 环境变量指定的目录,如果这个变量没设置,那么使用 /tmp 目录。

$ mktemp -p /opt/shell_script/
/opt/shell_script/tmp.qxcyvg1cRn

-t 参数可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的 X 字符,表示随机字符,建议至少使用六个 X。默认的文件名模板是 tmp. 后接十个随机字符。

$ mktemp -t xiashuo.log.XXXXXXX
/tmp/xiashuo.log.WYCYKuA

trap 命令

trap 命令用于在脚本中接收信号,kill 命令用于向进程发送信号

关于 kill 命令的解析,请看《终止进程 kill 和 killall》

trap 命令用来在 Bash 脚本中响应系统信号。

最常见的系统信号就是 SIGINT(中断),即按 Ctrl + C 所产生的信号。trap 命令的 -l 参数,可以列出所有的系统信号。

$ trap -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

trap 的命令格式如下。

$ trap [动作] [信号1] [信号2] ...

上面代码中,动作 是一个 Bash 命令,需要用引号引起来,否则会被视为信号参数,信号 常用的有以下几个。

trap 命令响应 EXIT 信号的写法如下。

$ trap 'rm -f "$TMPFILE"' EXIT

上面命令中,脚本遇到 EXIT 信号时,就会执行 rm -f "$TMPFILE"

trap 命令的常见使用场景,就是在 Bash 脚本中指定退出时执行的清理命令。

#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
  echo 'find'
fi

上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生 EXIT 信号,从而触发删除临时文件。

此外,trap 命令在错误处理中的用处很大,可以让我们在出现错误之后执行相关的操作。

请看《set 命令和 shopt 命令》的 set -E 小节

注意,trap 命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。

如果 trap 需要触发多条命令,可以封装一个 Bash 函数。

function egress {
  command1
  command2
  command3
}

trap egress EXIT

简单实践一下

创建脚本 shell_trap.sh

#!/usr/bin/env bash

trap "echo script has been interrupted" INT
trap "echo script has been terminated" TERM
trap "echo script has exit" EXIT

# 持续运行 60s
for((i=0;i<10;i++)); do
    # sleep 的这 5 秒无法被中断 
    sleep 5
done

赋权后执行,60s 后输出

$ ./shell_trap.sh 
script has exit

再次执行,运行到一半,按 Ctrl + c

$ ./shell_trap.sh 
^Cscript has exit

再次执行,运行到一半,通过 ps -aux |grep shell_trap.sh 找到 pid,然后 kill pidkill  <pid> 等价于 kill -15 <pid>

此时,并不能立即结束进程,虽然会立即输出 echo script has been terminated,但是要等到 60s 线程执行结束之后,才会输出 echo script has exit

$ ./shell_trap.sh 
script has been terminated
script has exit

其实原因很简单,除了直接 kill -9 pid,其他信号,kill 命令只负责发送信号,具体的行为需要脚本监听到信号之后自己去实现

修改 shell_trap.sh

#!/usr/bin/env bash

trap "echo script has been interrupted" INT
trap "echo script has been terminated;exit 1" TERM
trap "echo script has exit" EXIT

# 持续运行 60s
for((i=0;i<10;i++)); do
    # sleep 的这 5 秒无法被中断 
    sleep 5
done

再次执行,然后通过 kill pid 杀掉进程,立即输出

$ ./shell_trap.sh 
script has been terminated
script has exit

再次执行,然后 kill -9 pid,输出

$ ./shell_trap.sh 
Killed