在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 

编者按
>(…)是一个进程替换 ,它是一些 POSIX兼容shell的非标准shell特性bashkshzsh
– 正如所写,答案也意外地通过管道发送输出过程替代的输出。
echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null会阻止这种情况,但它有缺陷:从进程取代的输出将被不可预知地交错,除了zsh之外,流水线可能在命令之前终止里面>(…)做。

在unix中(或在Mac上),使用tee命令 :

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

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

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

会做你想要的。

在Windows下,我不认为内置的shell有相同的。 尽管微软的Windows PowerShell有一个tee命令。

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

如果你没有bash,或者运行在带有旧版本bash的系统上,你可以通过使用FIFO文件来手动执行bash的功能。

达到你想要的通用方法是:

  • 确定有多少进程应该接收命令的输出,并创建尽可能多的FIFO,最好在全局临时文件夹上:
    子过程= “ABCD”
     mypid = $$
    因为我在$ subprocesses#这样我们就可以兼容所有的sh派生的shell  
    
         mknod /tmp/pipe.$mypid.$ip
     DONE
  • 启动所有等待FIFO输入的子进程:
    为我在$ 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有三通,我想我会在PowerShell中展示一种方法。

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

请注意,在创建下一个对象之前处理从第一个命令出来的每个对象。 这可以允许缩放到非常大的输入。

Unix( bashkshzsh

dF。的答案包含基于tee输出 过程替换 的答案的种子
>(...)可能会或可能不会工作,这取决于您的要求:

请注意,进程替换是一个非标准功能,主要是POSIX-features-only shell(例如,在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, 除非它是stderr被重定向到一个( 2> >(...) )。

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

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

  • 但是, 可以通过将命令输送到| cat 迫使 bashksh等待 | cat ,但请注意,这使命令运行在一个子shell 注意事项

    • ksh (从ksh 93u+ )不支持将stderr发送到输出进程替换( 2> >(...) ); 这样的尝试是无声无息的

    • 尽管默认情况下 zsh与(更常见的) 标准输出输出过程替换( 默认 )是同步 ,但即使| cat | cat技术不能使它们与stderr输出过程替换同步( 2> >(...) )。

  • 但是, 即使确保同步执行 ,仍然存在不可预测的交错输出问题。

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

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

总之

  • 保证一个特定的每个命令输出序列:

    • bashkshzsh都不支持。
  • 同步执行:

    • 可以,除了以stderr为基础的输出过程替换:
      • zsh ,它们总是异步的。
      • ksh ,他们根本不工作

如果你能忍受这些限制,使用输出过程替换是一个可行的选择(例如,如果他们全部写入单独的输出文件)。


请注意, tzot更麻烦,但潜在POSIX兼容的解决方案也表现出不可预知的输出行为 ; 但是,通过使用wait ,可以确保后续命令不会开始执行,直到所有后台进程完成。
请参阅底部获得更健壮的同步串行输出实现


具有可预测的输出行为的唯一简单的 bash解决方案如下所示,然而, 对于大型输入集合来说 ,这是非常慢的 ,因为shell循环本身就很慢。
另请注意,这将交替输出来自目标命令的行

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

Unix(使用GNU并行)

安装GNU parallel功能可以使用序列化(per-command)输出强大解决方案 ,并且可以并行执行

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

parallel默认情况下确保不同命令的输出不交织(这种行为可以修改 – 见man parallel )。

注意:一些Linux发行版带有不同的 parallel工具,这不适用于上面的命令; 使用parallel --version来确定哪一个,如果有的话。


视窗

Jay Bazuzi的有用答案显示了如何在PowerShell中执行此操作。 这就是说:他的回答是上面循环的bash答案的模拟,它将在大输入集合的速度下变得非常缓慢,并且还会交替来自目标命令的输出行



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

以下是tzot答案中提供的方法的一个简单但相当健壮的实现, 它还提供了:

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

尽管不严格符合POSIX标准,但由于它是一个bash脚本,它应该可移植到任何具有bash Unix平台

注意:您可以在本Gist中找到在MIT许可证下发布的更为全面的实现。

如果将以下代码保存为脚本fanout ,请将其设置为可执行文件,并将int设置为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 

不需要在这里创建一个子shell