如何使用 Unix(或 Windows)中的(最好是未命名的)管道将一个进程的标准输出发送到多个进程?

Posted

技术标签:

【中文标题】如何使用 Unix(或 Windows)中的(最好是未命名的)管道将一个进程的标准输出发送到多个进程?【英文标题】:How can I send the stdout of one process to multiple processes using (preferably unnamed) pipes in Unix (or Windows)? 【发布时间】:2010-09-08 19:46:52 【问题描述】:

我想将进程proc1的stdout重定向到两个进程proc2和proc3:

         proc2 -> stdout
       /
 proc1
       \ 
         proc3 -> stdout

我试过了

 proc1 | (proc2 & proc3)

但它似乎不起作用,即

 echo 123 | (tr 1 a & tr 1 b)

 b23

到标准输出而不是

 a23
 b23

【问题讨论】:

【参考方案1】:

编者注: - >(…) 是一个process substitution,它是一些 POSIX 兼容shell 的非标准shell 功能bashkshzsh。 - 这个答案意外地通过管道发送了输出进程替换的输出tooecho 123 | tee >(tr 1 a) | tr 1 b. - 进程替换的输出将不可预测地交错,并且除了zsh 之外,管道可能会在>(…) 中的命令执行之前终止。

在 unix(或 mac)中,使用 tee command:

$ echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
b23
a23

通常您会使用 tee 将输出重定向到多个文件,但使用 >(...) 您可以 重定向到另一个进程。所以,总的来说,

$ proc1 | tee >(proc2) ... >(procN-1) >(procN) >/dev/null

会做你想做的。

在 windows 下,我认为内置的 shell 没有等价物。微软的Windows PowerShell 有一个tee 命令。

【讨论】:

这不是 POSIX 结构,需要 bash 或 ksh。你对 tcsh 和 dash 等不走运。 @pixelbeat: ...但它可以分解为 POSIX 结构(请参阅我的答案:) 这并不完全符合@secr 的要求。 tee 将在通过管道发送之前将进程重定向的输出附加到stdout,这与将stdout 的相同实例通过管道传输到多个命令明显不同。 @dF,例如,echo 123 | tee >(tr 1 a) | tr 2 b 将导致 1b3 ab3,这在原始问题的上下文中没有意义。 虽然非常方便,但请注意,在 >(...) 内部开始的命令与原始 shell 分离,您无法轻松确定它们何时结束; tee 将在写完所有内容后完成,但被替代的进程仍将消耗来自内核和文件 I/O 中各种缓冲区的数据,以及它们内部处理数据所花费的任何时间。如果你的外壳继续依赖子进程产生的任何东西,你可能会遇到竞争条件。 @Dejay Clayton:您可以使用inproc | tee >(outproc1) >(outproc2) > /dev/null | outproc 丢弃原始输入。 outproc 将只看到由 outproc1 和 outproc2 生成的输出。原始输入已“消失”。【参考方案2】:

就像 dF 所说,bash 允许使用 >(…) 构造运行命令来代替文件名。 (还有<(…) 构造来替换另一个命令的输出 来代替文件名,但现在这无关紧要,我只是为了完整性而提到它。

如果您没有 bash,或者在使用旧版本 bash 的系统上运行,您可以通过使用 FIFO 文件手动完成 bash 的工作。

实现您想要的通用方法是:

决定应该有多少进程接收命令的输出,并创建尽可能多的 FIFO,最好在全局临时文件夹中: 子进程="a b c d" mypid=$$ for i in $subprocesses # 这样我们就可以兼容所有 sh 派生的 shell 做 mkfifo /tmp/pipe.$mypid.$i 完毕 启动所有等待来自 FIFO 的输入的子进程: 对于我在 $subprocesses 做 tr 1 $i /pipe.$mypid.$i & # 背景! 完毕 执行您的命令发球到 FIFO: proc1 | tee $(for i in $subprocesses; do echo /tmp/pipe.$mypid.$i; done) 最后,移除 FIFO: 对于 $subprocesses 中的 i;做 rm /tmp/pipe.$mypid.$i;完毕

注意:出于兼容性原因,我会在 $(…) 中使用反引号,但我无法编写此答案(在 SO 中使用了反引号)。通常,$(…) 已经足够老,即使在旧版本的 ksh 中也可以工作,但如果不是,请将 部分括在反引号中。

【讨论】:

++ 是一种很好的方法,但您应该使用mkfifo 而不是mknod,因为只有前者符合POSIX。此外,使用不带引号的命令替换很脆弱,并且有可能使用通配符来提高效率。在我的回答中,我冒昧地实施了一个更强大的解决方案——尽管基于bash。请注意,$(…) 已经成为 POSIX 的一部分很长时间了,所以我会远离不可预测的`…`(SO 绝对允许在代码块甚至内联代码跨度中使用`(至少现在:))。 如果读取端进程之一停止消耗(即无法启动、死亡等),写入端似乎会阻塞。在考虑解决方案的必要弹性时需要考虑的事项。【参考方案3】:

Unix (bash, ksh, zsh)

dF.'s answer 包含基于tee输出 process substitutions 的答案的种子 (>(...)) 可能会或可能不会工作,具体取决于您的要求:

请注意,进程替换是一个非标准功能,(大部分) 仅具有 POSIX 功能的 shell,例如 dash(在 Ubuntu 上充当 /bin/sh, 例如),支持。针对/bin/sh 的Shell 脚本不应依赖它们。

echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null

这种方法的缺陷是:

不可预测的异步输出行为:来自输出进程内命令的输出流替换 >(...) 以不可预测的方式交错。

bashksh(与zsh 相对,但请参阅下面的例外):

输出可能会在命令完成后到达。 后续命令可能开始执行进程替换中的命令完成之前 - bashksh 不会等待输出进程至少在默认情况下完成替换产生的进程。 jmb 在对 dF. 的回答的评论中说得很好:

请注意,在>(...) 中启动的命令与原始 shell 是分离的,您无法轻易确定它们何时结束; tee 将在写完所有内容后完成,但被替代的进程仍将消耗来自内核和文件 I/O 中各种缓冲区的数据,以及它们内部处理数据所花费的任何时间。如果你的外壳继续依赖子进程产生的任何东西,你可能会遇到竞争条件。

zsh 是唯一默认等待输出进程替换中运行的进程完成的shell , except 如果是 stderr 被重定向到一个 (2> >(...))。

ksh(至少从版本 93u+ 开始)允许使用无参数的 wait 来等待输出进程替换生成的进程完成。 请注意,在交互式会话中,也可能导致等待任何未决的后台作业

bash v4.4+ 可以等待 最近wait $! 启动的输出进程替换,但没有参数的wait 不会 工作,使得这不适用于具有多个 输出进程替换的命令。

不过,bashksh 可以通过将命令传送到 | cat强制等待,但请注意这使得命令在 subshel​​l 中运行。 注意事项

ksh(截至ksh 93u+)不支持将stderr发送到输出进程替换(2> >(...));这样的尝试被默默地忽略

虽然zsh 是(值得称赞的)同步默认与(更常见的)stdout 输出进程替换,即使| cat 技术也不能使它们与 stderr 输出进程替换 (2> >(...)) 同步。

然而,即使你确保同步执行不可预知的交错输出的问题依然存在。

李>

以下命令在bashksh 中运行时,说明了有问题的行为(您可能需要运行多次才能看到两种症状):AFTER 通常会print before 输出替换的输出,而后者的输出可以不可预测地交错。

printf 'line %s\n' 1..30 | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER

简而言之

保证每个命令的特定输出顺序:

bashkshzsh 都不支持。

同步执行:

可行,除了 stderr 来源的输出进程替换: 在zsh 中,它们总是是异步的。 在ksh 中,它们根本不起作用

如果您可以忍受这些限制,使用输出进程替换是一个可行的选择(例如,如果它们都写入单独的输出文件)。


请注意,tzot's much more cumbersome, but potentially POSIX-compliant solution 也表现出不可预测的输出行为;但是,通过使用wait,您可以确保在所有后台进程都完成之前不会开始执行后续命令。见底部了解更强大、同步、序列化输出的实现


唯一的直截了当bash 解决方案具有可预测的输出行为如下,但是,它速度太慢了大型输入集,因为 shell 循环本身就很慢。 另请注意,这交替目标命令的输出行

while IFS= read -r line; do 
  tr 1 a <<<"$line"
  tr 1 b <<<"$line"
done < <(echo '123')

Unix(使用 GNU Parallel)

安装GNU parallel 可实现强大的解决方案,该解决方案具有序列化(按命令)输出,还允许并行执行

$ echo '123' | parallel --pipe --tee  ::: 'tr 1 a' 'tr 1 b'
a23
b23

parallel 默认确保不同命令的输出不会交错(此行为可以修改 - 请参阅man parallel)。

注意:一些 Linux 发行版带有一个不同 parallel 实用程序,它不适用于上面的命令;使用parallel --version 确定您拥有哪一个(如果有)。


窗口

Jay Bazuzi's helpful answer 展示了如何在 PowerShell 中执行此操作。也就是说:他的答案类似于上面的循环 bash 答案,它会在大输入集的情况下非常慢并且交替输出线来自目标命令



基于bash,但具有同步执行和输出序列化的可移植 Unix 解决方案

以下是tzot's answer 中提出的方法的简单但相当稳健的实现,该方法还提供:

同步执行 序列化(分组)输出

虽然不完全符合 POSIX,因为它是一个 bash 脚本,它应该可移植到任何具有 bash 的 Unix 平台

注意:您可以在this Gist 中找到在 MIT 许可下发布的更成熟的实现。

如果您将下面的代码保存为脚本fanout,使其可执行并放入您的PATH,则问题中的命令将按如下方式工作:

$ echo 123 | fanout 'tr 1 a' 'tr 1 b'
# tr 1 a
a23
# tr 1 b
b23

fanout脚本源码

#!/usr/bin/env bash

# The commands to pipe to, passed as a single string each.
aCmds=( "$@" )

# Create a temp. directory to hold all FIFOs and captured output.
tmpDir="$TMPDIR:-/tmp/$kTHIS_NAME-$$-$(date +%s)-$RANDOM"
mkdir "$tmpDir" || exit
# Set up a trap that automatically removes the temp dir. when this script
# exits.
trap 'rm -rf "$tmpDir"' EXIT 

# Determine the number padding for the sequential FIFO / output-capture names, 
# so that *alphabetic* sorting, as done by *globbing* is equivalent to
# *numerical* sorting.
maxNdx=$(( $# - 1 ))
fmtString="%0$#maxNdxd"

# Create the FIFO and output-capture filename arrays
aFifos=() aOutFiles=()
for (( i = 0; i <= maxNdx; ++i )); do
  printf -v suffix "$fmtString" $i
  aFifos[i]="$tmpDir/fifo-$suffix"
  aOutFiles[i]="$tmpDir/out-$suffix"
done

# Create the FIFOs.
mkfifo "$aFifos[@]" || exit

# Start all commands in the background, each reading from a dedicated FIFO.
for (( i = 0; i <= maxNdx; ++i )); do
  fifo=$aFifos[i]
  outFile=$aOutFiles[i]
  cmd=$aCmds[i]
  printf '# %s\n' "$cmd" > "$outFile"
  eval "$cmd" < "$fifo" >> "$outFile" &
done

# Now tee stdin to all FIFOs.
tee "$aFifos[@]" >/dev/null || exit

# Wait for all background processes to finish.
wait

# Print all captured stdout output, grouped by target command, in sequences.
cat "$aOutFiles[@]"

【讨论】:

【参考方案4】:

由于@dF: 提到 PowerShell 有 tee,我想我会展示一种在 PowerShell 中执行此操作的方法。

PS > "123" | %  
    $_.Replace( "1", "a"), 
    $_.Replace( "2", "b" ) 


a23
1b3

请注意,第一个命令产生的每个对象都会在创建下一个对象之前进行处理。这可以允许扩展到非常大的输入。

【讨论】:

是的,但这相当于在 Bash 中执行 while IFS= read -r line; do tr 1 a &lt;&lt;&lt;"$line"; tr 1 b &lt;&lt;&lt;"$line"; done &lt; &lt;(echo '123'),它在 内存方面 可以很好地扩展,但在性能方面 则不行。 【参考方案5】:

您还可以将输出保存在变量中并将其用于其他进程:

out=$(proc1); echo "$out" | proc2; echo "$out" | proc3

但是,只有在

    proc1 在某个时候终止:-) proc1 不会产生太多输出(不知道有什么限制,但可能是你的 RAM)

但是它很容易记住,并且为您从那里产生的进程中获得的输出提供了更多选项,例如。 g.:

out=$(proc1); echo $(echo "$out" | proc2) / $(echo "$out" | proc3) | bc

我在使用 | tee &gt;(proc2) &gt;(proc3) &gt;/dev/null 方法时遇到了困难。

【讨论】:

【参考方案6】:

另一种方法是,

 eval `echo '&& echo 123 |''tr 1 a','tr 1 b' | sed -n 's/^&&//gp'`

输出:

a23
b23

这里不需要创建子shell

【讨论】:

这在什么外壳上工作?它所做的一切都是 eval echo 123 |tr 1 a,tr 1 b 抱怨 tr 不存在,如果你输入额外的空格,它会因为逗号而等待额外的输入,如果你将逗号更改为分号或 & 符号,你只会得到第一个一个打印 - 不是两个。 @JerryJeremiah:通过在 string 中创建命令行 echo '&amp;&amp; echo 123 |''tr 1 a','tr 1 b' | sed -n 's/^&amp;&amp;//gp' 和然后将该字符串传递给eval。也就是说,它 (a) 在构造字符串的过程中创建了 3 个子外壳(1 个用于 `...`,2 个用于嵌入式管道的段,以及 (b),更重要的是,它重复 i> 输入命令,以便为每个目标 tr 命令运行单独的副本。除了效率低下,相同的命令运行两次不一定会产生相同的输出两次。

以上是关于如何使用 Unix(或 Windows)中的(最好是未命名的)管道将一个进程的标准输出发送到多个进程?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 RPC XDR 从 UNIX 移植到 Windows? [关闭]

Windows 机器中的 UNIX/LINUX 本地主机服务器

windows批处理如何替换换行符?

什么相当于 Windows 10 PowerShell 中的 bash/shell/UNIX-terminal 命令“which”?

如何在Bash或UNIX shell中检查字符串中的第一个字符?

UNIX 上与 Oracle DB 的 Java getConnection 崩溃或花费的时间比 Windows 上长得多