失败的解决方案1

失败的解决方案1

在Linux中,是否可以运行管道:

cmd1 | cmd2

以这样的方式:

  1. cmd2cmd1直到完全完成才开始运行,并且

  2. 如果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

这似乎有效。但它有几个缺点:

  1. 它将数据写入 I/O 速度较慢的磁盘。 (当然你可以使用临时文件系统但这是一个额外的系统要求)。

  2. 您必须谨慎选择临时文件名。否则它可能会意外覆盖现有文件。临时表可能会有所帮助,但是未命名管道可以完全节省您的命名工作。

  3. 临时文件所在的文件系统可能不够大,无法容纳整个数据。

  4. 临时文件不会自动清理。

答案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
    
  • 不,上述解决方案不适合二进制数据,因为:

    1. 命令替换$(...)会吃掉所有尾随换行符。
    2. \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无论如何都会丢弃退出值。


这种方法(至少)还存在两个缺陷:

  1. 如前所述,它丢失了 的实际退出代码cmd1,将其减少为布尔值 true/false。如果退出代码有意义,那么它就丢失了。可以将代码保存到命令中的文件中,并在必要时sh稍后(或其他)重新加载它。ifne -n sh -c 'read code <FILENAME ; rm -f FILENAME; exit $code'
  2. 如果cmd1能够成功却没有任何输出,无论如何一切都会崩溃。

另外,当然,它是多个相当罕见的命令连接在一起,仔细引用,具有不明显的含义。

答案4

cmd1 | cmd2以这样的方式运行管道:

cmd2cmd1直到完全完成才开始运行

这一般来说是不可能的。读管道(7)这提醒你管道容量有限(通常为 4Kbytes 或 64Kbytes)并且他们使用一些核心他们的缓冲区的内存。

所以 的输出cmd1进入管道。当它变满时,任何写(2)cmd1由to完成STDOUT_FILENO会阻塞(除非cmd1专门编码来处理标准输出的非阻塞 I/O,这是非常不寻常的),cmd2直到阅读(2)从那根管子的另一端。如果cmd2不开始,那永远不会发生。

我强烈推荐阅读这样的书高级Linux编程其中详细解释了这一点(需要一整本书来解释这一切)。

相关内容