Bash Trap 命令

作者:Mitch Frazier

 

如果您编写过任何数量的 bash 代码,您可能遇到过trap命令。Trap 允许您捕获信号并在信号发生时执行代码。信号是异步通知,当某些事件发生时会发送到您的脚本。这些通知大多数是针对您希望永远不会发生的事件,例如无效的内存访问或错误的系统调用。但是,可能有一两个事件您可能想要合理地处理。还有可用的“用户”事件,这些事件永远不会由系统生成,您可以生成这些事件来向您的脚本发出信号。Bash 还提供了一个名为“EXIT”的伪信号,它会在您的脚本退出时执行;这可以用来确保您的脚本在退出时执行一些清理操作。

 

signal(7) 的手册页描述了所有可用的信号。signal (IPC) 的 Wikipedia 页面有更多细节。正如我提到的,它们中的大多数在脚本中都很少受到关注。“SIGINT”信号可能是脚本中唯一可能引起关注的信号。当您在键盘上键入 Ctrl-C 以中断正在运行的脚本时,会生成 SIGINT。如果您不希望您的脚本像这样被停止,您可以捕获该信号并提醒自己应该避免中断脚本。虽然,正如您将看到的,这不如人们希望的那么有用。“SIGUSR1”信号是一个“用户”定义的信号,您可以随意使用它。它永远不会由系统生成。

trap 命令最常见的用途是捕获 bash 生成的名为 EXIT 的伪信号。例如,假设您有一个脚本创建了一个临时文件。与其在脚本退出的每个位置删除它,不如在脚本的开头放置一个 trap 命令,该命令在退出时删除该文件

tempfile=/tmp/tmpdata
trap "rm -f $tempfile" EXIT

现在,每当您的脚本退出时,它都会删除您的临时文件。trap 命令的语法是“trap COMMAND SIGNALS...”,因此,除非您要执行的命令是单个单词,“command”部分应该用引号引起来。

如果您的清理需求很复杂,您不必尝试将所有内容都塞进带有分号的字符串中,只需编写一个函数即可

function cleanup()
{
    # ...
}

trap cleanup EXIT

请注意,如果您发送kill -9到您的脚本,它将不会在退出之前执行 EXIT trap。

您可能希望使用 trap 命令的另一个可能的事情是捕获 Ctrl-C,以便您的脚本不会被中断,或者您可以询问用户是否真的想要中断该进程。作为一个例子,我将使用以下处理函数,它在前两次 Ctrl-C 时警告用户,然后在第三次时退出

ctrlc_count=0

function no_ctrlc()
{
    let ctrlc_count++
    echo
    if [[ $ctrlc_count == 1 ]]; then
        echo "Stop that."
    elif [[ $ctrlc_count == 2 ]]; then
        echo "Once more and I quit."
    else
        echo "That's it.  I quit."
        exit
    fi
}

使用以下命令来测试处理程序

trap no_ctrlc SIGINT

while true
do
    echo Sleeping
    sleep 10
done

如果您运行它并键入 Ctrl-C 三次,您将得到以下输出

$ bash noctrlc.sh
Sleeping
^C
Stop that.
Sleeping
^C
Once more and I quit.
Sleeping
^C
That's it.  I quit.
$

我对测试脚本的第一次尝试使用了sleep 10作为 while 条件

trap no_ctrlc SIGINT

while sleep 10
do
    echo Sleeping
done

但这不起作用。经过一番思考,我意识到这是因为当 trap 命令返回时,它不会在中断的点恢复“sleep”命令,也不会重新启动“sleep”命令,而是返回到中断命令之后的下一个命令,在本例中,它是 while 循环之后的任何内容。所以循环结束,脚本正常退出。

这是一个重点:中断的命令不会重新启动。因此,如果您的脚本需要执行一些不应该被中断的重要操作,那么您不能,例如,使用 trap 命令来捕获信号,打印警告,然后像什么都没发生一样恢复操作。相反,如果您不能让某些东西被中断,您需要做的是在命令执行时禁用 Ctrl-C 处理。您也可以使用 trap 命令来执行此操作,方法是指定一个空命令来 trap。您也可以使用 trap 命令通过指定“-”作为命令来将信号处理重置为默认值。所以您可能会这样做

# Run something important, no Ctrl-C allowed.
trap "" SIGINT
important_command

# Less important stuff from here on out, Ctrl-C allowed.
trap - SIGINT
not_so_important_command

因此,除非您的脚本有长时间的等待,否则捕获信号并实际执行某些操作可能无法提供您期望的体验。

我想看的最后一件事是捕获发送到脚本的用户定义信号。假设我想监视系统日志并计算sudo运行的次数,并且我想在后台运行该脚本,然后在我想让它显示计数时向它发送一个信号

nopens=0
function show_opens()
{
    echo "Seen $nopens sudo session opens"
}
trap show_opens USR1

sudo journalctl -f | while read line
do
    if [[ $line =~ sudo.*session.*opened ]]; then
        let nopens++
    fi
done

这样做是将journalctl(即系统日志)的输出管道传输到循环中的read命令。在循环内部,if 语句检查该行是否是 sudo 命令。如果是,则递增计数器。循环之前的代码为 SIGUSR1 信号设置了一个 trap,当收到信号时,“show_opens”函数会打印出自脚本启动以来看到的 sudo 命令的数量。您可以使用 kill 命令向脚本发送 SIGUSR1 信号

$ bash bkgnd.sh &
[1] 1000
$ kill -SIGUSR1 1000

不幸的是,再一次,这未能奏效。我发现的第一个问题,我最近在我的关于作业控制的帖子中提到过,是如果 sudo 命令需要提示输入密码,则脚本会在启动后立即挂起。

提示:重置 sudo 的密码时间戳

如果您需要使用 sudo 测试某些内容,并想确保在 sudo 提示输入密码和不提示输入密码时一切正常工作,请执行命令sudo -k以重置 sudo 的密码时间戳。在使用 -k 选项执行 sudo 后,sudo 将在下次运行时再次要求输入密码,无论您最近输入密码的时间有多近。

在弄清楚挂起的后台问题后,我认为一切系统都“准备就绪”,但事实并非如此,仍然没有任何结果。现在的问题是因为循环正在从管道获取输入。原始 bash 进程现在为“journalctl”执行了一个子进程,为“while read line ...”执行了另一个子进程。当 bash 执行命令时,根据手册页

shell 捕获的 traps 会重置为从 shell 的父进程继承的值

因此,当这些子进程启动时,SIGUSR1 trap 会被重置,并且不再对该进程产生影响。要使其工作,我们需要在循环内设置 trap,使其成为子进程的一部分

nopens=0
function show_opens()
{
    echo "Seen $nopens sudo session opens"
}

sudo journalctl -f | while read line
do
    if [[ -z "$trap_set" ]]; then
        trap_set=1
        echo "Trap set in $BASHPID"
        trap show_opens USR1
    fi
    if [[ $line =~ sudo.*session.*opened ]]; then
        let nopens++
    fi
done

请注意,我使用$BASHPID来获取子进程的进程 ID($$总是返回原始 shell 的进程 ID)。

现在它可以工作了

$ bash bkgnd.sh &
[1] 1000
Trap set in 1002
$ kill -SIGUSR1 1002
Seen 1 sudo session opens
$ sudo ls
...
$ kill -SIGUSR1 1002
Seen 2 sudo session opens

最后,我不能说我可能会经常捕获 SIGINT 或任何其他信号(EXIT 除外),但我可以说我在使这些示例工作的过程中发现了一些关于 bash 的有趣的细微之处。

# Copyright 2019 Mitch Frazier <mitch -at- linuxjournal.com>
#
# This software may be used and distributed according to the terms of the
# MIT License or the GNU General Public License version 2 (or any later version).

Mitch Frazier 是 Emerson Electric Co. 的嵌入式系统程序员。自 2000 年代初以来,Mitch 一直是 Linux Journal 的贡献者和朋友。

加载 Disqus 评论