在脚本中使用 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”中的“l”)查看它们中的大多数

$ 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本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.