我正在寻找一种方法来清理混乱时,我的顶级脚本退出。

特别是如果我想使用set -e,我希望后台进程在脚本退出时结束。


当前回答

如果在子shell中调用Jobs -p,则不能在所有shell中工作,除非它的输出被重定向到文件而不是管道中。(我想它最初只是用于交互使用的。)

下面这些怎么样:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

Debian的dash shell需要调用“jobs”,如果缺少当前作业(“%%”),则该shell无法更新。

其他回答

另一种选择是让脚本将自己设置为进程组的领导者,并在退出时在进程组上捕获killpg。

如果在子shell中调用Jobs -p,则不能在所有shell中工作,除非它的输出被重定向到文件而不是管道中。(我想它最初只是用于交互使用的。)

下面这些怎么样:

trap 'while kill %% 2>/dev/null; do jobs > /dev/null; done' INT TERM EXIT [...]

Debian的dash shell需要调用“jobs”,如果缺少当前作业(“%%”),则该shell无法更新。

我把@tokland的回答与http://veithen.github.io/2014/11/16/sigterm-propagation.html的知识相结合,当我注意到如果我正在运行一个前台进程(不是后台的&),陷阱不会触发:

#!/bin/bash

# killable-shell.sh: Kills itself and all children (the whole process group) when killed.
# Adapted from http://stackoverflow.com/a/2173421 and http://veithen.github.io/2014/11/16/sigterm-propagation.html
# Note: Does not work (and cannot work) when the shell itself is killed with SIGKILL, for then the trap is not triggered.
trap "trap - SIGTERM && echo 'Caught SIGTERM, sending SIGTERM to process group' && kill -- -$$" SIGINT SIGTERM EXIT

echo $@
"$@" &
PID=$!
wait $PID
trap - SIGINT SIGTERM EXIT
wait $PID

它工作的例子:

$ bash killable-shell.sh sleep 100
sleep 100
^Z
[1]  + 31568 suspended  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31568  0.0  0.0  19640  1440 pts/18   T    01:30   0:00 bash killable-shell.sh sleep 100
niklas   31569  0.0  0.0  14404   616 pts/18   T    01:30   0:00 sleep 100
niklas   31605  0.0  0.0  18956   936 pts/18   S+   01:30   0:00 grep --color=auto sleep

$ bg
[1]  + 31568 continued  bash killable-shell.sh sleep 100

$ kill 31568
Caught SIGTERM, sending SIGTERM to process group
[1]  + 31568 terminated  bash killable-shell.sh sleep 100

$ ps aux | grep "sleep"
niklas   31717  0.0  0.0  18956   936 pts/18   S+   01:31   0:00 grep --color=auto sleep

在@tokland的回答中描述的陷阱“kill 0”SIGINT SIGTERM EXIT解决方案真的很好,但最新的Bash在使用它时崩溃了,分割错误。这是因为Bash,从v. 4.3开始,允许陷阱递归,在这种情况下,它变得无限:

shell进程接收到SIGINT或SIGTERM或EXIT; 信号被捕获,执行kill 0,将SIGTERM发送给组中的所有进程,包括shell本身; 转到第1节

这可以通过手动注销陷阱来解决:

trap 'trap - SIGTERM && kill 0' SIGINT SIGTERM EXIT

允许打印接收到的信号并避免“Terminated:”消息的更花哨的方式:

#!/usr/bin/env bash

trap_with_arg() { # from https://stackoverflow.com/a/2183063/804678
  local func="$1"; shift
  for sig in "$@"; do
    trap "$func $sig" "$sig"
  done
}

stop() {
  trap - SIGINT EXIT
  printf '\n%s\n' "received $1, killing child processes"
  kill -s SIGINT 0
}

trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP

{ i=0; while (( ++i )); do sleep 0.5 && echo "a: $i"; done } &
{ i=0; while (( ++i )); do sleep 0.6 && echo "b: $i"; done } &

while true; do read; done

UPD:增加了一个最小的例子;改进的停止功能,以避免去捕获不必要的信号,并从输出中隐藏“Terminated:”消息。感谢Trevor Boyd Smith的建议!

为了安全起见,我发现最好定义一个清理函数并从trap中调用它:

cleanup() {
        local pids=$(jobs -pr)
        [ -n "$pids" ] && kill $pids
}
trap "cleanup" INT QUIT TERM EXIT [...]

或完全避免该函数:

trap '[ -n "$(jobs -pr)" ] && kill $(jobs -pr)' INT QUIT TERM EXIT [...]

为什么?因为通过简单地使用陷阱'kill $(jobs -pr)'[…]当陷阱条件发出信号时,就会有后台作业在运行。当没有工作时,会看到以下(或类似的)消息:

kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]

因为jobs -pr是空的——我在那个“陷阱”中结束了(双关语)。