在Unix(或Windows)中,如何使用(最好是未命名的)pipe道将一个进程的stdout发送到多个进程?
我想将进程proc1的stdoutredirect到两个进程proc2和proc3:
proc2 -> stdout / proc1 \ proc3 -> stdout
我试过了
proc1 | (proc2 & proc3)
但它似乎并没有工作,即
echo 123 | (tr 1 a & tr 1 b)
写
b23
stdout而不是
a23 b23
编者按 :
– >(…)
是一个进程replace ,它是一些 POSIX兼容shell的非标准shell特性 : bash
, ksh
, zsh
。
– 正如所写,答案也意外地通过pipe道发送输出过程替代的输出。
– echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
会阻止这种情况,但是它有缺陷:从进程取代的输出将被不可预知地交错,除了zsh
之外,stream水线可能在命令之前终止里面>(…)
做。
在unix中(或在Mac上),使用tee
命令 :
$ echo 123 | tee >(tr 1 a) | tr 1 b b23 a23
通常你会使用tee
将输出redirect到多个文件,但使用>(…)可以redirect到另一个进程。 所以,一般来说,
$ proc1 | tee >(proc2) ... >(procN-1) | procN
会做你想要的。
在Windows下,我不认为内置的shell有相同的。 尽pipe微软的Windows PowerShell有一个tee
命令。
就像dF所说, bash
允许使用>(…)
构造运行一个命令来代替文件名。 (还有<(…)
构造来代替另一个命令的输出来代替文件名,但现在是不相关的,我只是为了完整性而提到它)。
如果你没有bash,或者运行在带有旧版本bash的系统上,你可以通过使用FIFO文件来手动执行bash的function。
达到你想要的通用方法是:
- 确定有多less进程应该接收命令的输出,并创build尽可能多的FIFO,最好在全局临时文件夹上:
子过程= “ABCD” mypid = $$ 因为我在$ subprocesses#这样我们就可以兼容所有的sh派生的shell 做 mknod /tmp/pipe.$mypid.$ip DONE
- 启动所有等待FIFOinput的subprocess:
为我在$ subprocesses 做 tr 1 $ i </tmp/pipe.$mypid.$i&#background! DONE
- 执行你的命令向FIFOs开球:
proc1 | tee $(我在$ subprocesses中;做echo /tmp/pipe.$mypid.$i; done)
- 最后,删除FIFO:
为我在$ subprocesses; 做rm /tmp/pipe.$mypid.$i; DONE
注:为了兼容性的原因,我会做$(…)
反引号,但我不能这样做写这个答案(反引号用于SO)。 通常情况下, $(…)
已经足够大,即使在旧版本的ksh中也可以工作,但是如果不行的话,请将…
部分包含在反引号中。
由于@dF:提到PowerShell有T恤,我想我会在PowerShell中展示一种方法。
PS > "123" | % { $_.Replace( "1", "a"), $_.Replace( "2", "b" ) } a23 1b3
请注意,在创build下一个对象之前处理从第一个命令出来的每个对象。 这可以允许缩放到非常大的input。
Unix( bash
, ksh
, zsh
)
dF。的答案包含基于tee
和输出 过程replace 的答案的种子
( >(...)
) 可能会或可能不会工作,这取决于您的要求:
请注意,进程replace是一个非标准function,主要是POSIX-features-only shell(例如,在Ubuntu上充当/bin/sh
) 不支持。 针对/bin/sh
Shell脚本不应该依赖它们。
echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
这种方法的缺陷是:
-
不可预知的asynchronous输出行为 :输出stream程中的命令输出stream将以不可预知的方式进行交替replace
>(...)
。 -
在
bash
和ksh
(而不是zsh
– 但请参阅下面的例外):- 输出可能在命令完成后到达。
- 后续命令可能会在进程replace中的命令完成之前开始执行 –
bash
和ksh
不会等待输出进程replace生成的进程完成,至less默认情况下是这样。 - jmb对dF。的回答很好:
注意在
>(...)
内部启动的命令与原始shell分离,并且不能轻易确定它们何时完成; 在写完所有事情之后,tee
将会完成,但是被replace的进程仍然会消耗来自内核和文件I / O中各种缓冲区的数据,以及内部处理数据所花费的时间。 如果您的shell继续依赖子stream程产生的任何东西,您可能会遇到竞争条件。
-
zsh
是默认情况下等待输出进程中的进程replace完成的唯一shell, 除非它是stderr被redirect到一个(2> >(...)
)。 -
ksh
(至less在93u+
版本中)允许使用无参数的wait
来等待输出进程replace – 产生的进程完成。
请注意,在交互式会话中,可能导致等待任何待处理的后台作业 。 -
bash v4.4+
可以等待最近启动的输出进程replacewait $!
,但无参数的wait
不起作用,使得这不适用于具有多个输出过程replace的命令。 -
但是, 可以通过将命令输送到
| cat
来迫使bash
和ksh
等待| cat
,但请注意,这使命令运行在一个子shell 。 注意事项 :-
ksh
(从ksh 93u+
)不支持将stderr发送到输出进程replace(2> >(...)
); 这样的尝试是无声无息的 。 -
尽pipe默认情况下
zsh
与(更常见的) stdout输出过程replace( 默认 )是同步的 ,但即使| cat
| cat
技术不能使它们与stderr输出过程replace同步(2> >(...)
)。
-
-
但是, 即使确保同步执行 ,仍然存在不可预测的交错输出问题。
下面的命令在bash
或ksh
运行时,说明了有问题的行为(您可能需要多次运行才能看到两个症状): AFTER
通常会在输出replace输出之前打印,后者的输出可以是交错的不可预测的。
printf 'line %s\n' {1..30} | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER
总之 :
-
保证一个特定的每个命令输出序列:
-
bash
,ksh
和zsh
都不支持。
-
-
同步执行:
- 可以,除了以stderr为基础的输出过程replace:
- 在
zsh
,它们总是asynchronous的。 - 在
ksh
,他们根本不工作 。
- 在
- 可以,除了以stderr为基础的输出过程replace:
如果你能忍受这些限制,使用输出过程replace是一个可行的select(例如,如果他们全部写入单独的输出文件)。
请注意, tzot更麻烦,但潜在POSIX兼容的解决scheme也performance出不可预知的输出行为 ; 但是,通过使用wait
,可以确保后续命令不会开始执行,直到所有后台进程完成。
请参阅底部以获得更健壮的同步串行输出实现 。
具有可预测的输出行为的唯一简单的 bash
解决scheme如下,但是, 对于大型input集来说 , 速度非常慢 ,因为shell循环本身就很慢。
另请注意,这将交替输出来自目标命令的行 。
while IFS= read -r line; do tr 1 a <<<"$line" tr 1 b <<<"$line" done < <(echo '123')
Unix(使用GNU并行)
安装GNU parallel
function可以使用序列化(per-command)输出的强大解决scheme ,并且可以并行执行 :
$ echo '123' | parallel --pipe --tee {} ::: 'tr 1 a' 'tr 1 b' a23 b23
parallel
默认情况下确保不同命令的输出不交织(这种行为可以修改 – 见man parallel
)。
注意:一些Linux发行版带有不同的 parallel
工具,这不适用于上面的命令; 使用parallel --version
来确定哪一个,如果有的话。
视窗
Jay Bazuzi的有用答案显示了如何在PowerShell中执行此操作。 这就是说:他的回答是上面的循环bash
答案的模拟,它将会以大input集合慢速过慢,并且也会从目标命令交替输出行 。
基于bash
的,但是否则是可移植的Unix解决scheme,具有同步执行和输出序列化
以下是tzot答案中提供的方法的一个简单但相当稳健的实现, 它还提供了:
- 同步执行
- 序列化(分组)输出
虽然不严格POSIX兼容,因为它是一个bash
脚本,它应该是任何具有bash
Unix平台的移植 。
注意:您可以在本Gist中find在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${#maxNdx}d" # 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[@]}"
另一种方法是,
eval `echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'`
输出:
a23 b23
不需要在这里创build一个子shell