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

时间:2012-01-23 17:46:02

标签: bash pipe

我在bash中使用了几个命令的管道。如果其中一个命令失败,有没有办法配置bash立即终止整个管道中的所有命令?

在我的例子中,第一个命令,比如command1,运行一段时间,直到它产生一些输出。例如,您可以将command1替换为(sleep 5 && echo "Hello")

现在,command1 | false在5秒后失败,但不会立即失败。

此行为似乎与命令生成的输出量有关。例如,find / | false会立即返回。

总的来说,我想知道为什么bash表现得像这样。任何人都可以想象像command1 | non-existing-command这样的代码不会立即退出的任何情况吗?

PS:使用临时文件对我来说不是一个选项,因为我管道的中间结果很大,无法存储。

PPS:set -eset -o pipefail似乎都不会影响这种现象。

5 个答案:

答案 0 :(得分:16)

bash文档在section about pipelines

中说明
  

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

中执行

“在它自己的子shell中”意味着产生了一个新的bash进程,然后执行实际的命令。每个子shell都会成功启动,即使它立即确定要求执行的命令不存在。

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

在同一部分,它还说:

  

shell在返回值之前等待管道中的所有命令终止。

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

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

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

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

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

答案 1 :(得分:4)

sleep 5在完成之前不会产生任何输出,而find /会立即产生bash尝试输出到false的输出。

答案 2 :(得分:3)

第一个程序在尝试将某个日期写入管道之前不知道第二个程序是否终止。如果第二个终止,第一个接收SIGPIPE,通常会立即退出。

您可以在启动后立即强制输出​​第一行输出,如下所示:

(sleep 0.1; echo; command1) | command2

此100ms睡眠旨在等到可能的命令2在启动后立即退出。 当然,如果command2在2秒后退出,并且command1将静默60秒,则整个shell命令将仅在60.1秒后返回。

答案 3 :(得分:2)

find / |false失败的速度更快,因为来自write(2)的第一个find系统调用失败,错误为EPIPE(断开管道)。这是因为false已经终止,因此这两个命令之间的管道已经在一侧关闭。

如果find会忽略该错误(理论上可以这样做),那么它也会“失败”。

(sleep 5 && echo "Hello") | false“失败慢”,因为第一部分sleep没有通过写入来“测试”管道。 5秒后,echo也会出现EPIPE错误。在这种情况下,此错误是否终止于第一部分对于该问题并不重要。

答案 4 :(得分:0)

以下代码似乎对Dash有效,但是管道中的EXIT陷阱在Bash中不起作用;也许这是Bash中的错误。

#!/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