如何拆分和重新连接来自多个进程的标准输出?

15

我正在开发一个流水线,其中有几个分支点,后来会合并-- 它们看起来像这样:

         command2
        /        \
command1          command4
        \        /
         command3

每个命令都会通过标准输出(STDOUT)进行写入,并通过标准输入(STDIN)接受输入。需要将command1的STDOUT传递给两个按顺序运行的命令command2和command3,它们的输出需要被有效地连接并传递给command4。我最初认为类似这样的命令可以工作:

$ command1 | (command2; command3) | command4

但是这并不起作用,因为只有来自command2的STDOUT被传递到command4,当我移除command4时,很明显command3没有从command1接收到适当的流——换句话说,就好像command2正在耗尽或消耗流。当我在中间使用{command2; command3;}时,也会得到相同的结果。所以我想我应该使用带有进程替换的'tee',于是我尝试了以下内容:

$ command1 | tee >(command2) | command3 | command4

但令人惊讶的是,那也不起作用——看起来command1的输出 command2的输出都被导入了command3,这就导致错误并且只有command3的输出被导入了command4。我发现以下内容可以正确地将输入和输出传递给command2和command3:

$ command1 | tee >(command2) >(command3) | command4

然而,这样会将command1的输出流传输到command4中,导致问题,因为command2和command3产生的规范与command1不同。我想到的解决方案似乎有些狡猾,但它确实有效:

$ command1 | tee >(command2) >(command3) > /dev/null | command4

这会阻止command1将其输出传递给command4,同时收集来自command2和command3的标准输出。虽然它能够工作,但我感觉可能有更明显的解决方案。我错了吗?我已经阅读了数十个线程,并没有找到在我的使用情况下有效的解决方法,也没有看到关于分割和重新连接流的确切问题的详细说明(尽管我不可能是第一个处理此问题的人)。我应该只使用命名管道吗?我尝试过,但难以使其正常工作,所以也许这是另一个帖子的另一个故事。我在RHEL5.8中使用bash。


看起来你的问题已经有了一个“可行”的解决方案——所以你是在寻求另一种解决方案吗?通常这种分裂在Shell脚本中不经常出现,但在像Hadoop-MapReduce这样的专业工具中经常出现——我认为你不可能找到比Bash管道更好的东西。 - Soren
@Soren -- 是的,我在想是否有更好的解决方案。虽然我的解决方案似乎可行,但我期望有一种不需要将stdout重定向到/dev/null的解决方案,并且我很好奇我犯了什么错误,因为这可能对我(或其他人)在继续开发时很有启发性。 - Peter
3个回答

9
您可以像这样玩转文件描述符;
((date | tee >( wc >&3) | wc) 3>&1) | wc

或者

((command1 | tee >( command2 >&3) | command3) 3>&1) | command4

为了解释,tee >( wc >&3)会在标准输出上输出原始数据,内部的wc会将结果输出到FD 3。然后外部的3>&1将FD3的输出合并回STDOUT,因此来自两个wc的输出都被发送到尾随命令。
但是,这个流水线(或您自己的解决方案中的流水线)中没有任何内容可以保证输出不会混乱。也就是说,如果担心命令2的不完整行与命令3的行混杂在一起,则需要执行以下两个操作之一:
1.编写自己的tee程序,其中内部使用popen,并在发送完整行到命令4之前读取每行返回stdout。 2.将命令2和命令3的输出写入文件,并使用cat将数据合并为输入传递给命令4。

谢谢 - 这正是我要找的。我也感谢关于输出流潜在插入的注释。我想知道是否有比重写 tee 或使用文件更优雅的解决方案。也许我可以使用 wait 命令来等待 command2 完成后再运行 command3? - Peter
这个解决方案似乎适用于bash/ksh/zsh。有人知道如何使它在/bin/static-sh(即busybox)上工作吗? - Tzunghsing David Wong

1
请参见https://unix.stackexchange.com/questions/28503/how-can-i-send-stdout-to-multiple-commands。在所有答案中,我发现this answer特别适合我的需求。
稍微扩展一下@Soren的答案,
$ ((date | tee >( wc >&3) | wc) 3>&1) | cat -n
     1         1       6      29
     2         1       6      29

你可以不使用 tee 命令,而是使用环境变量来实现。

$ (z=$(date); (echo "$z"| wc ); (echo "$z"| wc) ) | cat -n
     1         1       6      29
     2         1       6      29

在我的情况下,我应用了这种技术,并编写了一个在busybox下运行的更复杂的脚本。

0

我相信你的解决方案很好,它使用了文档中提到的tee命令。 如果你阅读tee的man手册,它会说:

Copy standard input to each FILE, and also to standard output

你的文件是处理替代。

而标准输出是需要删除的,因为你不需要它,这就是你重定向到 /dev/null 的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接