将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 是Bash 、Zsh 或Ksh93 ,我们可以使用进程替换。
接下来,让我们看一下该问题的一些更便携的解决方案。
4. 使用tee命令和命名管道
使用命名管道是解决该问题的一种可移植方法,因为所有 *nix 系统都支持它。
花几分钟时间了解命名管道是什么以及如何使用它是值得的。
4.1. 命名管道简而言之
我们已经熟悉无名管道,并且在 Linux 命令行中经常使用它。
无名管道是一种方便的技术,允许我们在不同进程之间传输数据,例如command1 | command2。但是,无名管道只存在于内核中,除command2以外的进程无法访问。
命名管道类似于未命名管道。此外,它存在于文件系统中,可以被多个进程打开进行读写。
设置和使用命名管道很方便。让我们看一下使用命名管道的标准操作:
- mkfifo myPipe——创建一个名为“ myPipe ”的命名管道。将创建一个特殊文件 myPipe
- command1 > myPipe – 将command1的输出重定向 到命名管道 myPipe
- command2 < myPipe – command2从myPipe读取数据作为输入
- 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/4 – tee命令将文件内容重定向到两个文件描述符 (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.sh。max.sh脚本会将其作为输入并找到最高分