Contents

将Stdout发送到多个命令

1. 概述

当我们使用 Linux 命令行时,我们经常将一个命令的输出通过管道传递给另一个命令。

然而,有时,我们可能会面临将一个命令的输出发送给多个命令的问题。在本教程中,我们将讨论如何解决这个问题。

3. 问题介绍

首先,让我们看一个具体的例子,这样我们可以更容易地理解问题。

假设我们有一个输入文件scores.txt,其中包含学生的姓名及其考试成绩:

$ cat scores.txt 
Mark 98.3
Kent 99.7
Amanda 88.8
Eric 95
John 70
Timo 42
Jerry 93

此外,我们还创建了三个 shell 脚本文件:

  • avg.sh – 计算所有学生的平均分并保存到文件:avg.result
  • max.sh – 将最高分与学生姓名一起保存到文件中:  max.result
  • min.sh – 将最低分数和学生姓名保存到文件中:  min.result

我们的问题是将命令cat scores.txt的输出发送到上面的三个脚本,以获得平均分、最高分和最低分。

现在,让我们仔细看看脚本。每个脚本都包含一个简短的awk 命令来进行计算,然后将结果保存在一个文件中:

$ head *.sh
==> avg.sh <==
#!/bin/bash
if [ -p /dev/stdin ]; then
    awk '{ sum+=$2 } END{ printf "The average score: %.2f\n", sum/NR }' /dev/stdin > avg.result
else
    echo "Error Occured: No input was found on stdin!"
fi
==> max.sh <==
#!/bin/bash
if [ -p /dev/stdin ]; then
    awk 'max < $2{ max=$2; max_row=$0 } \
        END{ printf "The highest score: %s\n", max_row }' /dev/stdin > max.result
else
    echo "Error Occured: No input was found on stdin!"
fi
==> min.sh <==
#!/bin/bash
if [ -p /dev/stdin ]; then
    awk 'NR==1 || $2 < min{ min=$2; min_row=$0 } \
        END{ printf "The lowest score: %s\n", min_row }' /dev/stdin > min.result
else
    echo "Error Occured: No input was found on stdin!"
fi

上面的三个脚本非常简单。但是,有几点值得一提:

  • 所有脚本只会从*stdin (/dev/stdin)*读取输入
  • 我们需要*[ -p /dev/stdin ]检查每个脚本。否则,如果我们在 stdin*上运行没有输入的脚本,它将挂起直到输入内容

在本教程中,我们将介绍如何通过三种不同的方式解决问题:

3.使用 tee命令和进程替换

使用tee命令和进程替换,我们可以将stdin 直接提供给进程:

$ tee >(process1) >(process2) >(process3)....

因此,我们的问题可以这样解决:

$ cat scores.txt | tee >(./min.sh) >(./max.sh) | ./avg.sh
$ head *.result
==> avg.result <==
The average score: 82.30
==> max.result <==
The highest score: Kent 99.7
==> min.result <==
The lowest score: Timo 42

这个解决方案非常简单。但是,**并非所有 shell 都支持进程替换功能。**如果我们的 shell 是BashZshKsh93 ,我们可以使用进程替换。

接下来,让我们看一下该问题的一些更便携的解决方案。

4. 使用tee命令和命名管道

使用命名管道是解决该问题的一种可移植方法,因为所有 *nix 系统都支持它。

花几分钟时间了解命名管道是什么以及如何使用它是值得的。

4.1. 命名管道简而言之

我们已经熟悉无名管道,并且在 Linux 命令行中经常使用它。

无名管道是一种方便的技术,允许我们在不同进程之间传输数据,例如command1 | command2。但是,无名管道只存在于内核中,除command2以外的进程无法访问。

命名管道类似于未命名管道。此外,它存在于文件系统中,可以被多个进程打开进行读写。

设置和使用命名管道很方便。让我们看一下使用命名管道的标准操作:

  • mkfifo myPipe——创建一个名为“ myPipe ”的命名管道。将创建一个特殊文件 myPipe
  • command1 > myPipe – 将command1的输出重定向 到命名管道 myPipe
  • command2 < myPipe –  command2myPipe读取数据作为输入
  • rm myPipe – 关闭命名管道,就像删除普通文件一样

4.2. 使用命名管道的一个小例子

在理解了什么是命名管道之后,让我们通过一个例子来展示如何使用命名管道: 首先,让我们使用mkfifo命令创建命名管道:

$ mkfifo myPipe
$ ls -l myPipe 
prw-r--r-- 1 kent kent 0 Jul 12 16:38 myPipe

在上面的ls命令的输出中,最左边一列的p表示文件myPipe是一个命名管道。

之后,我们将告诉一个简短的awk命令从我们刚刚创建的命名管道中读取数据:

$ awk '$0 = NR ":" $0' > withLineNumber.txt < myPipe &
[1] 467439

awk命令 将从命名管道中读取数据。此外,它在每行上添加一个行号作为前缀,并将结果保存在名为 withLineNumber.txt 的文件中。

我们可能会注意到该命令以 *&*字符结尾 。这是因为我们仍然想在当前 shell 中键入其他命令,而*&*运算符 允许我们 在后台运行 awk命令。

现在,让我们使用我们的scores.txt文件来提供命名管道:

$ cat scores.txt > myPipe

数据写入命名管道后,后台作业完成:

$ jobs
[1]+  Done            awk '$0 = NR ":" $0' > withLineNumber.txt < myPipe

接下来,让我们检查withLineNumber.txt是否 已创建并填充了预期数据:

$ cat withLineNumber.txt 
1:Mark 98.3
2:Kent 99.7
3:Amanda 88.8
4:Eric 95
5:John 70
6:Timo 42
7:Jerry 93

最后,我们应该使用rm命令关闭命名管道:

$ rm myPipe

4.3. 使用tee和命名管道解决问题

现在,让我们看看如何使用tee命令和命名管道解决我们的问题:

$ mkfifo myPipe1 myPipe2
$ ./min.sh < myPipe1 &
[1] 482525
$ ./max.sh < myPipe2 &
[2] 482657
$ cat scores.txt | tee myPipe1 myPipe2 |./avg.sh 

此外,让我们检查结果文件是否已创建:

$ head *.result
==> avg.result <==
The average score: 82.30
==> max.result <==
The highest score: Kent 99.7
==> min.result <==
The lowest score: Timo 42

伟大的!生成结果文件。 最后,我们不应该忘记使用rm命令关闭管道:

$ rm myPipe1 myPipe2

在这个解决方案中,tee命令帮助我们将scores.txt文件的内容重定向 到两个命名管道。如果我们知道命名管道的工作原理,理解解决方案就不成问题。

5. 使用 tee命令和文件描述符

由于我们可以在任何 POSIX shell 中使用多个文件描述符,因此使用文件描述符的解决方案也是可移植的。 首先,让我们先看看如何使用文件描述符解决问题:

$ { { cat scores.txt| tee /dev/fd/3 /dev/fd/4 | ./avg.sh \
     } 3>&1 | ./min.sh  \
   } 4>&1 | ./max.sh    \

执行上面的命令后,让我们检查结果文件,看看它们是否包含预期的结果:

$ head *.result
==> avg.result <==
The average score: 82.30
==> max.result <==
The highest score: Kent 99.7
==> min.result <==
The lowest score: Timo 42

现在,让我们了解解决方案的工作原理:

  • 花括号*{…}* – 我们使用花括号来包装命令以在当前 shell 而不是子 shell 中运行这些命令
  • tee /dev/fd/3 /dev/fd/4tee命令将文件内容重定向到两个文件描述符 (FD) 3 和 4
  • tee ……| ./avg.sh; – 此外,  tee命令将文件内容通过管道传输到avg.sh脚本
  • {cat.. | tee..} 3>&1 | ./min.sh; – 我们将 FD 3 重定向到 FD 1,即标准输出。然后我们将标准输出通过管道传递给min.sh作为输入
  • {…} 4>&1 | ./max.sh; –同样,我们将 FD 4 重定向到标准输出,然后通过管道传输到max.shmax.sh脚本会将其作为输入并找到最高分