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

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

How can I send the stdout of one process to multiple processes using (preferably unnamed) pipes in Unix (or Windows)?

我想将进程proc1的标准输出重定向到两个进程proc2和proc3:

1
2
3
4
5
         proc2 -> stdout
       /
 proc1
       \
         proc3 -> stdout

我试过了

1
 proc1 | (proc2 & proc3)

但它似乎不起作用,即

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

1
 b23

而不是stdout

1
2
 a23
 b23

编者注:
->(…)是一种进程替换,它是某些POSIX兼容shell的非标准shell功能:bashkshzsh
-此答案也意外地通过管道发送了输出过程替换的输出:echo 123 | tee >(tr 1 a) | tr 1 b
-进程替换的输出将以不可预测的方式交错,并且,除了zsh中的内容外,流水线可能在>(…)中的命令执行之前终止。

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

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

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

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

会做你想要的。

在Windows下,我不认为内置外壳具有等效功能。微软的Windows PowerShell有一个tee命令。


就像dF所说的,bash允许使用运行命令的>(…)构造代替文件名。 (也有<(…)构造来代替另一个命令的输出来代替文件名,但这已经不相关了,我仅出于完整性而提及它)。

如果没有bash或在具有较旧版本bash的系统上运行,则可以通过使用FIFO文件来手动执行bash的操作。

实现所需目标的通用方法是:

  • 确定应接收命令输出的进程数,并创建尽可能多的FIFO,最好在全局临时文件夹中:
1
2
3
4
5
6
    subprocesses="a b c d"
    mypid=$$
    for i in $subprocesses # this way we are compatible with all sh-derived shells  
    do
        mkfifo /tmp/pipe.$mypid.$i
    done
  • 启动所有子进程,等待来自FIFO的输入:
1
2
3
4
    for i in $subprocesses
    do
        tr 1 $i </tmp/pipe.$mypid.$i & # background!
    done
  • 执行准备进入FIFO的命令:
1
    proc1 | tee $(for i in $subprocesses; do echo /tmp/pipe.$mypid.$i; done)
  • 最后,删除FIFO:
1
    for i in $subprocesses; do rm /tmp/pipe.$mypid.$i; done

注意:出于兼容性原因,我会在$(…)中加上反引号,但是我无法编写此答案(反引号在SO中使用)。通常,$(…)足够旧,即使在旧版本的ksh中也可以使用,但如果不行,则将部分用反引号引起来。


Unix(bashkshzsh)

dF。的答案包含基于tee的答案的种子和输出过程的替换
(>(...))可能有效或可能无效,具体取决于您的要求:

好。

请注意,流程替换是一项非标准功能,(通常)
仅限POSIX功能的外壳,例如dash(在Ubuntu上充当/bin/sh
例如),不支持。针对/bin/sh的Shell脚本不应依赖于它们。

好。

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

这种方法的陷阱是:

好。

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

    好。

  • bashksh中(与zsh相反-但请参见下面的异常):

    好。

  • 命令完成后,输出可能会到达。
  • 后续命令可以在进程替换中的命令完成之前开始执行-bashksh至少在默认情况下,不等待输出进程替换生成的进程完成。
  • jmb在dF的回答中很好地说明了这一点:
  • 好。

    好。

    be aware that the commands started inside >(...) are dissociated from the original shell, and you can't easily determine when they finish; the tee will finish after writing everything, but the substituted processes will still be consuming the data from various buffers in the kernel and file I/O, plus whatever time is taken by their internal handling of data. You can encounter race conditions if your outer shell then goes on to rely on anything produced by the sub-processes.

    Ok.

  • zsh是默认情况下唯一等待输出过程替换中运行的进程完成的外壳程序,除非将stderr重定向到一个(2> >(...))。

    好。

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

    好。

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

    好。

  • 但是,可以通过将命令传递给| cat来强制bashksh等待,但是请注意,这会使命令在子shell中运行。注意事项:

    好。

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

    好。

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

    好。

  • 好。

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

    好。

  • 好。

    bashksh中运行时,以下命令说明了有问题的行为(您可能必须多次运行才能看到两种症状):AFTER通常将在输出替换输出之前打印,并且后者的输出可能无法预料地交错。

    好。

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

    简而言之:

    好。

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

    好。

  • bashkshzsh都不支持。
  • 好。

  • 同步执行:

    好。

  • 可行,但使用stderr的输出过程替代除外:

  • zsh中,它们总是异步的。
  • ksh中,它们根本不起作用。
  • 好。

    好。

    好。

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

    好。

    请注意,tzot繁琐得多,但是可能符合POSIX的解决方案也表现出不可预测的输出行为。但是,通过使用wait,可以确保在所有后台进程完成之前,后续命令不会开始执行。
    有关更健壮,同步,序列化输出的实现,请参见底部。

    好。

    唯一具有可预测的输出行为的bash解决方案是以下解决方案,但是,对于大型输入集,该解决方案的速度过慢,这是因为Shell循环本来就很慢。
    还要注意,这会交替输出目标命令的输出行。

    好。

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

    Unix(使用GNU Parallel)

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

    好。

    1
    2
    3
    $ 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,使其可执行并放入PATH,则问题中的命令将按以下方式工作:

    好。

    1
    2
    3
    4
    5
    $ echo 123 | fanout 'tr 1 a' 'tr 1 b'
    # tr 1 a
    a23
    # tr 1 b
    b23

    fanout脚本源代码:

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #!/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
    '
    "$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[@]}"

    好。


    自@dF:提到PowerShell已经开球以来,我想我将展示一种在PowerShell中执行此操作的方法。

    1
    2
    3
    4
    5
    6
    7
    PS >"123" | % {
        $_.Replace("1","a"),
        $_.Replace("2","b" )
    }

    a23
    1b3

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


    另一种方法是

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

    输出:

    1
    2
    a23
    b23

    无需在此处创建子外壳


    推荐阅读