如何使用 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 功能:bash
、ksh
、zsh
。
- 这个答案意外地通过管道发送了输出进程替换的输出too:echo 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
这种方法的缺陷是:
不可预测的异步输出行为:来自输出进程内命令的输出流替换 >(...)
以不可预测的方式交错。
在bash
和ksh
(与zsh
相对,但请参阅下面的例外):
bash
和 ksh
不会等待输出进程至少在默认情况下完成替换产生的进程。
jmb 在对 dF. 的回答的评论中说得很好:
请注意,在
>(...)
中启动的命令与原始 shell 是分离的,您无法轻易确定它们何时结束;tee
将在写完所有内容后完成,但被替代的进程仍将消耗来自内核和文件 I/O 中各种缓冲区的数据,以及它们内部处理数据所花费的任何时间。如果你的外壳继续依赖子进程产生的任何东西,你可能会遇到竞争条件。
zsh
是唯一默认等待输出进程替换中运行的进程完成的shell , except 如果是 stderr 被重定向到一个 (2> >(...)
)。
ksh
(至少从版本 93u+
开始)允许使用无参数的 wait
来等待输出进程替换生成的进程完成。
请注意,在交互式会话中,也可能导致等待任何未决的后台作业。
bash v4.4+
可以等待 最近 用wait $!
启动的输出进程替换,但没有参数的wait
不会 工作,使得这不适用于具有多个 输出进程替换的命令。
不过,bash
和 ksh
可以通过将命令传送到 | cat
来强制等待,但请注意这使得命令在 subshell 中运行。 注意事项:
ksh
(截至ksh 93u+
)不支持将stderr发送到输出进程替换(2> >(...)
);这样的尝试被默默地忽略。
虽然zsh
是(值得称赞的)同步默认与(更常见的)stdout 输出进程替换,即使| cat
技术也不能使它们与 stderr 输出进程替换 (2> >(...)
) 同步。
然而,即使你确保同步执行,不可预知的交错输出的问题依然存在。
李>以下命令在bash
或ksh
中运行时,说明了有问题的行为(您可能需要运行多次才能看到两种症状):AFTER
通常会print before 输出替换的输出,而后者的输出可以不可预测地交错。
printf 'line %s\n' 1..30 | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER
简而言之:
保证每个命令的特定输出顺序:
bash
和 ksh
和 zsh
都不支持。
同步执行:
可行,除了 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 <<<"$line"; tr 1 b <<<"$line"; done < <(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 >(proc2) >(proc3) >/dev/null
方法时遇到了困难。
【讨论】:
【参考方案6】:另一种方法是,
eval `echo '&& echo 123 |''tr 1 a','tr 1 b' | sed -n 's/^&&//gp'`
输出:
a23
b23
这里不需要创建子shell
【讨论】:
这在什么外壳上工作?它所做的一切都是 evalecho 123 |tr 1 a,tr 1 b
抱怨 tr
不存在,如果你输入额外的空格,它会因为逗号而等待额外的输入,如果你将逗号更改为分号或 & 符号,你只会得到第一个一个打印 - 不是两个。
@JerryJeremiah:通过在 string 中创建命令行 echo '&& echo 123 |''tr 1 a','tr 1 b' | sed -n 's/^&&//gp'
和然后将该字符串传递给eval
。也就是说,它 (a) 在构造字符串的过程中创建了 3 个子外壳(1 个用于 `...`
,2 个用于嵌入式管道的段,以及 (b),更重要的是,它重复 i> 输入命令,以便为每个目标 tr
命令运行单独的副本。除了效率低下,相同的命令运行两次不一定会产生相同的输出两次。以上是关于如何使用 Unix(或 Windows)中的(最好是未命名的)管道将一个进程的标准输出发送到多个进程?的主要内容,如果未能解决你的问题,请参考以下文章
如何将 RPC XDR 从 UNIX 移植到 Windows? [关闭]
Windows 机器中的 UNIX/LINUX 本地主机服务器
什么相当于 Windows 10 PowerShell 中的 bash/shell/UNIX-terminal 命令“which”?
如何在Bash或UNIX shell中检查字符串中的第一个字符?
UNIX 上与 Oracle DB 的 Java getConnection 崩溃或花费的时间比 Windows 上长得多