在脚本中使用 Bash 陷阱

陷阱可以帮助你的脚本干净地结束,无论它们是否成功运行。
126 位读者喜欢这篇文章。
hands programming

WOCinTech Chat. 由 Opensource.com 修改. CC BY-SA 4.0

检测 shell 脚本何时启动很容易,但要知道它何时停止并不总是那么容易。脚本可能会正常结束,就像作者希望它结束的那样,但它也可能由于意外的致命错误而失败。有时,保留脚本失败时正在进行的工作的残余是有益的,而有时这很不方便。无论哪种方式,检测脚本的结束并以某种预先计算好的方式对其做出反应,这就是 Bash trap 指令存在的原因。

响应失败

这是一个脚本中的一个失败如何导致未来失败的例子。假设你编写了一个程序,在 /tmp 中创建一个临时目录,以便它可以解压缩和处理文件,然后再以不同的格式将它们捆绑在一起

#!/usr/bin/env bash
CWD=`pwd`
TMP=${TMP:-/tmp/tmpdir}

## create tmp dir
mkdir "${TMP}"

## extract files to tmp
tar xf "${1}" --directory "${TMP}"

## move to tmpdir and run commands
pushd "${TMP}"
for IMG in *.jpg; do
  mogrify -verbose -flip -flop "${IMG}"
done
tar --create --file "${1%.*}".tar *.jpg

## move back to origin
popd

## bundle with bzip2
bzip2 --compress "${TMP}"/"${1%.*}".tar \
      --stdout > "${1%.*}".tbz

## clean up
/usr/bin/rm -r /tmp/tmpdir

大多数时候,脚本都能按预期工作。但是,如果你不小心在包含 PNG 文件而不是预期的 JPEG 文件的存档上运行它,它会在中途失败。一个失败导致另一个失败,最终,脚本退出而没有到达删除临时目录的最终指令。只要你手动删除该目录,你就可以快速恢复,但如果你不在场进行此操作,那么下次运行脚本时,它必须处理一个包含不可预测的剩余文件的现有临时目录。

解决这个问题的一种方法是通过在脚本的开头添加一个预防性的删除操作来反转和加倍逻辑。虽然有效,但它依赖于蛮力而不是结构。更优雅的解决方案是 trap

使用 trap 捕获信号

trap 关键字捕获执行期间可能发生的信号。如果你曾经使用过 killkillall 命令,你就使用过其中一个信号,这些命令默认调用 SIGTERM。shell 响应许多其他信号,你可以使用 trap -l (如“list”)查看它们中的大多数

$ trap --list
 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 识别

  • EXIT:当 shell 进程本身退出时发生
  • ERR:当命令(例如 tarmkdir)或内置命令(例如 pushdcd)以非零状态完成时发生
  • DEBUG:代表调试模式的布尔值

要在 Bash 中设置陷阱,请使用 trap,后跟你要执行的命令列表,后跟触发它的信号列表。

例如,此陷阱检测 SIGINT,即用户在进程运行时按下 Ctrl+C 时发送的信号

trap "{ echo 'Terminated with Ctrl+C'; }" SIGINT

可以使用检测 SIGINT、错误和成功退出的陷阱来修复具有临时目录问题的示例脚本

#!/usr/bin/env bash
CWD=`pwd`
TMP=${TMP:-/tmp/tmpdir}

trap \
 "{ /usr/bin/rm -r "${TMP}" ; exit 255; }" \
 SIGINT SIGTERM ERR EXIT

## create tmp dir
mkdir "${TMP}"
tar xf "${1}" --directory "${TMP}"

## move to tmp and run commands
pushd "${TMP}"
for IMG in *.jpg; do
  mogrify -verbose -flip -flop "${IMG}"
done
tar --create --file "${1%.*}".tar *.jpg

## move back to origin
popd

## zip tar
bzip2 --compress $TMP/"${1%.*}".tar \
      --stdout > "${1%.*}".tbz

对于复杂的操作,你可以使用 Bash 函数简化 trap 语句。

Bash 中的陷阱

陷阱对于确保你的脚本干净地结束非常有用,无论它们是否成功运行。完全依赖自动垃圾回收从来都不是安全的,因此这是一个总体上应该养成的良好习惯。尝试在你的脚本中使用它们,看看它们能做什么!

接下来要阅读的内容
标签
Seth Kenlon
Seth Kenlon 是一名 UNIX 极客、自由文化倡导者、独立多媒体艺术家和 D&D 爱好者。他曾在电影和计算行业工作,通常同时进行。

13 条评论

我尝试了 EXIT 陷阱,它也在脚本被 HUP、TERM 和 INT 信号杀死时执行。我认为它在脚本退出时都会执行,无论退出状态是否为 0。

我认为较新版本的 trap 命令使用 `-l` 而不是 `--list` - 我正在使用 Debian Testing

我在 RHEL 8 上也看到了。已更新,谢谢!

回复 作者:Erik Lundmark (未验证)

我发现这篇文章非常有用且解释得很好。非常感谢!

好东西,但吹毛求疵

将 `pwd` 更改为 $PWD,无需子 shell 即可工作

引用 "$TMP" 的用法 -- 有人可能会在那里有 $IFS 字符

同样适用于 $IMG

(请注意,在 var=$TMP 的情况下,不需要引号)

trap "/usr/bin/rm -r "$TMP"; exit 255" SIGINT SIGTERM ERR EXIT

应该可以工作。你是否检查过陷阱没有执行两次(例如,对于 ERR 和 EXIT)?

(最后(但肯定不是最不重要的),我个人建议对所有 shell 脚本使用 set -euf)

我同意许多这些建议,并相应地更改了文章的某些部分。其他要点也很好,但我会将它们作为评论留给用户自己决定。

回复 作者:Tomi-1234 (未验证)

当捕获多个信号(例如,SIGINT SIGTERM ERR EXIT)时,是否有一种方法可以以与陷阱对应的代码退出?谢谢

我所知道的唯一方法是为每个信号创建一个陷阱语句。

将所有陷阱放在脚本的开头,以确保 Bash “知道”所有不同的条件。

我很想知道是否有人有更复杂的方法来做到这一点(虽然我也希望在承诺使用它之前对其进行严格的测试)。

回复 作者:eli (未验证)

大家好,我想提一下,括号优于反引号进行命令替换,因为反引号已被弃用。请参阅“https://mywiki.wooledge.org/BashFAQ/082”。

在 ERR 和 EXIT 的描述中,澄清 ERR 在命令(外部命令如“mkdir”、“tar”,或内部命令如“pushd”)以错误状态完成时被触发可能很有帮助。

EXIT 在 shell 进程(脚本)本身退出时被触发。

很好的澄清。我已经将其添加到文章文本中以供后人参考。

感谢您的反馈!

回复 的描述中 作者:Steven Bakker

Creative Commons License本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
© . All rights reserved.