在Linux中,是否可以运行管道:
cmd1 | cmd2
以这样的方式:
cmd2
cmd1
直到完全完成才开始运行,并且如果
cmd1
有错误,则cmd2
根本不运行,并且管道的退出状态是 的退出状态cmd1
。
举个例子,如何制作这个管道:
false | echo ok
什么都不输出并返回非零状态?
失败的解决方案1
set -o pipefail
该管道确实具有非零退出状态,但即使失败cmd2
仍然运行。cmd1
解决方案2失败
cmd1 && cmd2
这不是管道。无 I/O 重定向。
解决方案 3 失败
mkfifo /tmp/fifo
cmd1 > /tmp/fifo && cmd2 < /tmp/fifo
它会阻塞。
次优解
touch /tmp/file
cmd1 > /tmp/file && cmd2 < /tmp/file
这似乎有效。但它有几个缺点:
它将数据写入 I/O 速度较慢的磁盘。 (当然你可以使用临时文件系统但这是一个额外的系统要求)。
您必须谨慎选择临时文件名。否则它可能会意外覆盖现有文件。临时表可能会有所帮助,但是未命名管道可以完全节省您的命名工作。
临时文件所在的文件系统可能不够大,无法容纳整个数据。
临时文件不会自动清理。
答案1
我们不知道 的cmd1
输出大小,但知道管道缓冲区大小有限。一旦将一定量的数据写入管道,任何后续写入都将阻塞,直到有人读取管道(类似于失败的解决方案 3)。
您必须使用保证不阻塞的机制。对于非常大的数据,请使用临时文件。否则,如果您有能力将数据保留在内存中(这毕竟是管道的想法),请使用:
result=$(cmd1) && cmd2 < <(printf '%s' "$result")
unset result
这里 的结果cmd1
存储在变量 中result
。如果cmd1
成功,cmd2
则执行并提供 中的数据result
。最后,result
取消设置以释放相关内存。
注意:以前,我使用此处字符串 ( <<< "$result"
) 来提供cmd2
数据,但 Stéphane Chazelas 观察到这bash
会创建一个您不想要的临时文件。
回答评论中的问题:
是的,命令可以链接随意:
result=$(cmd1) \ && result=$(cmd2 < <(printf '%s' "$result")) \ && result=$(cmd3 < <(printf '%s' "$result")) \ ... && cmdN < <(printf '%s' "$result") unset result
不,上述解决方案不适合二进制数据,因为:
- 命令替换
$(...)
会吃掉所有尾随换行符。 \0
命令替换结果中的NUL 字符 ( ) 的行为未指定(例如,Bash 会丢弃它们)。
- 命令替换
是的,为了避免二进制数据的所有这些问题,您可以使用类似的编码器
base64
(或uuencode
,或自制的编码器,仅处理 NUL 字符和尾随换行符):result=$(cmd1 > >(base64)) && cmd2 < <(printf '%s' "$result" | base64 -d) unset result
在这里,我必须使用进程替换 (
>(...)
) 才能保持cmd1
退出值不变。
也就是说,仅仅为了确保数据不写入磁盘似乎就相当麻烦。中间临时文件是更好的解决方案。看史蒂芬的回答这解决了您对此的大部分担忧。
答案2
管道命令的全部要点是同时运行它们并读取另一个命令的输出。如果您想按顺序运行它们,并且如果我们保留管道隐喻,则需要将第一个命令的输出通过管道传输到一个存储桶(存储它),然后将该存储桶清空到另一个命令中。
但是使用管道执行此操作意味着第一个命令有两个进程(该命令和另一个进程从管道另一端读取其输出以存储在桶中),第二个命令有两个进程(一个将桶清空到一端)管道的命令从另一端读取它)。
对于存储桶,您需要内存或文件系统。内存不能很好地扩展,你需要管道。文件系统更有意义。这就是/tmp
目的。请注意,磁盘可能永远不会看到数据,因为数据可能要等到很久以后(在删除临时文件之后)才会被刷新到那里,即使是,它也可能仍保留在内存中(缓存)。如果不是这样,那么数据就会太大而无法首先放入内存中。
请注意,shell 中始终使用临时文件。在大多数 shell 中,此处的文档和此处的字符串是使用临时文件实现的。
在:
cat << EOF
foo
EOF
大多数 shell 都会创建一个临时文件,打开它进行写入和读取,删除它,用 填充它foo
,然后cat
使用从打开的 fd 复制的 stdin 来运行以进行读取。该文件甚至在填满之前就被删除(这给系统一个线索,即无论写入其中的内容都不需要在断电后继续存在)。
你可以在这里做同样的事情:
tmp=$(mktemp) && {
rm -f -- "$tmp" &&
cmd1 >&3 3>&- 4<&- &&
cmd2 <&4 4<&- 3>&-
} 3> "$tmp" 4< "$tmp"
然后,您不必担心清理问题,因为文件从一开始就被删除了。不需要额外的进程将数据放入和取出存储桶,cmd1
并且cmd2
可以自行完成。
如果您想将输出存储在内存中,那么使用 shell 并不是一个好主意。除了zsh
不能在其变量中存储任意数据之外,第一个 shell 除外。您需要使用某种形式的编码。然后,为了传递该数据,如果在使用此处文档或此处字符串时不将其写入磁盘,则最终会在内存中将其复制多次。
perl
例如,您可以使用:
perl -MPOSIX -e '
sub status() {return WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?) | 128}
$/ = undef;
open A, "-|", "cmd1" or die "open A: $!\n";
$out = <A>;
close A;
$status = status;
exit $status if $status != 0;
open B, "|-", "cmd2" or die "open B: $!\n";
print B $out;
close B;
exit status'
答案3
这是一个坦率地说糟糕的版本,将不同的工具拼接在一起moreutils
:
chronic sh -c '! { echo 123 ; false ; }' | mispipe 'ifne -n false' 'ifne echo ok'
它仍然不是您想要的:如果失败,它返回 1,否则返回 0。但是,除非第一个命令成功,否则它不会启动第二个命令,它根据第一个命令是否有效返回失败或成功代码,并且它不使用文件。
更通用的版本是:
chronic sh -c '! '"$CMD1" | mispipe 'ifne -n false' "ifne $CMD2"
这汇集了三个 moreutils 工具:
chronic
安静地运行命令,除非失败。在本例中,我们正在运行一个 shell 来运行您的第一个命令,以便我们可以反转成功/失败结果:它将安静地运行该命令如果失败,如果成功则在最后打印输出。mispipe
通过管道将两个命令连接在一起,返回第一个命令的退出状态。这和 的效果类似set -o pipefail
。这些命令以字符串形式提供,以便可以区分它们。ifne
如果标准输入非空,或者如果标准输入为空,则运行程序-n
。我们使用它两次:第一个是
ifne -n false
。这运行false
,并将其用作退出代码,当且仅当输入是空的(意味着chronic
吃了它,意味着cmd1
失败了)。当输入不为空时,它不会运行
false
,通过 like 传递输入cat
,并退出 0。输出将通过 管道输送到下一个命令mispipe
。第二个是
ifne cmd2
。这运行cmd2
当且仅当输入是非空的。该输入是 的输出ifne -n false
,当 的输出chronic
非空时(命令成功时会发生这种情况),该输出也将非空。当输入为空时,
cmd2
永远不会运行,并且ifne
退出为零。mispipe
无论如何都会丢弃退出值。
这种方法(至少)还存在两个缺陷:
- 如前所述,它丢失了 的实际退出代码
cmd1
,将其减少为布尔值 true/false。如果退出代码有意义,那么它就丢失了。可以将代码保存到命令中的文件中,并在必要时sh
稍后(或其他)重新加载它。ifne -n sh -c 'read code <FILENAME ; rm -f FILENAME; exit $code'
- 如果
cmd1
能够成功却没有任何输出,无论如何一切都会崩溃。
另外,当然,它是多个相当罕见的命令连接在一起,仔细引用,具有不明显的含义。