Shell管道:当一个命令失败时立即退出

27
我正在使用Bash中的一系列管道命令。有没有一种方法可以配置Bash,使所有命令在其中一个命令失败时立即终止整个管道?
在我的情况下,第一个命令(例如`command1 `)运行一段时间,直到产生一些输出。例如,您可以将`command1`替换为`(sleep 5 && echo "Hello")`。
现在,`command1 | false`会在5秒后失败,但不会立即退出。
这种行为似乎与命令产生的输出数量有关。例如,`find / | false`会立即返回。
总的来说,我想知道Bash为什么会有这种行为。是否有任何情况需要像`command1 | non-existing-command`这样的代码不立即退出?
PS:对于我来说,使用临时文件不是一个选择,因为我要传递的中间结果太大而无法存储。
PPS:`set -e`和`set -o pipefail`似乎都不影响这种现象。

1
这个问题更适合在http://unix.stackexchange.com上提问。你可能会在那里得到一个好的答案。 - dogbane
5个回答

19

bash文档中在有关管道的章节中提到:

管道中的每个命令都在自己的子shell中执行[...]

"在自己的子shell中执行"意味着会生成一个新的bash进程,然后这个新进程就可以执行实际的命令。即使其中一个命令无法执行,每个子shell也会成功启动。

这解释了为什么整个管道即使有一个命令是无意义的,也能被成功设置。Bash不检查每个命令是否可以运行,而是将其委托给子shell。这也解释了为什么例如命令nonexisting-command | touch hello会抛出“command not found”错误,但文件hello仍然会被创建。

在同一部分中,它还说:

Shell会等待管道中所有命令终止后再返回值。

sleep 5 | nonexisting-command中,正如A.H.指出的那样,sleep 5会在5秒后终止,而不是立即终止,因此shell也会等待5秒。

我不知道为什么要这样实现。在像你这样的情况下,行为肯定不是人们期望的。

无论如何,一个稍微有些丑陋的解决方法是使用FIFOs:

mkfifo myfifo
./long-running-script.sh > myfifo &
whoops-a-typo < myfifo

这里启动了long-running-script.sh脚本,然后在下一行立即失败。使用多个FIFO,可以将其扩展到具有两个以上命令的管道。


3

sleep 5 不会产生任何输出,直到它完成,而 find / 立即产生 bash 尝试将其管道传输到 false 的输出。


@Jaypal:我同意,这个例子可能会有误导性。我编辑了帖子,希望现在更清晰明了。 - Tobi
1
@Dan:是的,但这并没有真正回答我的问题。我想知道是否可以让bash在一个命令失败后终止管道中的所有命令。 - Tobi

3

第一个程序在尝试向管道中写入数据之前不知道第二个程序是否已经终止。如果第二个程序已经终止,第一个程序将收到SIGPIPE信号,通常会导致立即退出。

您可以通过以下方式强制立即将第一行输出传输到管道中:

(sleep 0.1; echo; command1) | command2

这个100毫秒的休眠是为了等待可能在启动后立即退出的command2。

当然,如果command2在2秒后退出,而command1将保持静默60秒,那么整个Shell命令只会在60.1秒后返回。


1
find / |false 命令会更快地失败,因为 find 的第一个 write(2) 系统调用会因错误 EPIPE(管道破裂)而失败。这是因为 false 已经终止,因此在这两个命令之间的管道已经在一侧关闭了。
如果 find 忽略这个错误(理论上它可以这样做),它将变成“慢速失败”。 (sleep 5 && echo "Hello") | false 是“慢速失败”,因为第一部分 sleep 不会通过向管道写入来“测试”管道。5 秒后,echo 也会收到 EPIPE 错误。这个错误是否会在这种情况下终止第一部分并不重要。

0
下面的代码似乎在 Dash 中工作正常,但是 Bash 中管道内的 EXIT trap 不起作用,可能是 Bash 的一个 bug。
#!/bin/sh

echo PID of the shell: $$

trap 'echo In INT trap >&2; trap - EXIT INT; kill -s INT $$' INT

(
    # now in subshell
    pidofsubshell=$(exec sh -c 'echo "$PPID"')
    # $BASHPID can be used as a value, when using Bash
    echo PID of subshell: $pidofsubshell

    fifo=$(mktemp -u); shells=$(mktemp) childs=$(mktemp)
    mkfifo $fifo
    trap 'echo In sub trap >&2; rm $fifo $shells $childs; trap - EXIT; exit' EXIT HUP TERM INT ALRM

    pipe_trap() {
        code=$?
        echo In sub sub trap $1 >&2
        echo $1 $code >> $fifo
    }
    { trap 'echo In pipe signal trap >&2; kill $(cat $childs $shells) 2>/dev/null' INT HUP TERM ALRM
        { trap 'pipe_trap 1' EXIT
            sleep 30; } \
        | { trap 'pipe_trap 2' EXIT
            sleep 50 & sleep 2; } \
        | { trap 'pipe_trap 3' EXIT
            sleep 40; } &
    }

    echo ps tail:
    ps xao pid,ppid,pgid,sid,command | head -n 1
    ps xao pid,ppid,pgid,sid,command | tail -n 15
        ps -o pid= --ppid $pidofsubshell | head -n -2 > $shells # strip pids of ps and head
    echo shells:
    cat $shells
        while read -r ppid; do ps -o pid= --ppid $ppid; done <$shells >$childs
    echo childs of above
    cat $childs

    { 
        IFS=' ' read -r id exitcode
        echo Pipe part nr. $id terminated first with code $exitcode\; killing the remaining processes.
        kill $(cat $childs $shells) 2>/dev/null
    } < $fifo
)

echo
echo After subshell:
ps xao pid,ppid,pgid,sid,command | head -n 1
ps xao pid,ppid,pgid,sid,command | tail -n 15

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接