# 23. 进程替换

用[管道](http://tldp.org/LDP/abs/html/special-chars.html#PIPEREF) 将一个命令的 `标准输出` 输送到另一个命令的 `标准输入` 是个强大的技术。但是如果你需要用管道输送*多个*命令的 `标准输出` 怎么办？这时候 *进程替换* 就派上用场了。

*进程替换* 把一个（或多个）[进程](http://tldp.org/LDP/abs/html/special-chars.html#PROCESSREF) 的输出送到另一个进程的 `标准输入`。

**样板** 命令列表要用括号括起来

```
>(command_list)
<(command_list)
```

进程替换使用 `/dev/fd/<n>` 文件发送括号内进程的结果到另一个进程。\[1]

![](http://tldp.org/LDP/abs/images/caution.gif)"<"或">"与括号之间没有空格，加上空格或报错。

```
bash$ echo >(true)
/dev/fd/63

bash$ echo <(true)
/dev/fd/63

bash$ echo >(true) <(true)
/dev/fd/63 /dev/fd/62

bash$ wc <(cat /usr/share/dict/linux.words)
 483523  483523 4992010 /dev/fd/63

bash$ grep script /usr/share/dict/linux.words | wc
    262     262    3601

bash$ wc <(grep script /usr/share/dict/linux.words)
    262     262    3601 /dev/fd/63
```

![](http://tldp.org/LDP/abs/images/note.gif)Bash用两个文件描述符创建管道，`--fIn 和 fOut--` 。[true](http://tldp.org/LDP/abs/html/internal.html#TRUEREF) 的`标准输入`连接 fOut(dup2(fOut, 0))，然后Bash 传递一个 `/dev/fd/fIn` 参数给 **echo** 。在不使用 `/dev/fd/<n>` 的系统里，Bash可以用临时文件（感谢 S.C. 指出这点）。

进程替换可以比较两个不同命令的输出，或者同一个命令使用不同选项的输出。

```
bash$ comm <(ls -l) <(ls -al)
total 12
-rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
-rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
-rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
        total 20
        drwxrwxrwx    2 bozo bozo     4096 Mar 10 18:10 .
        drwx------   72 bozo bozo     4096 Mar 10 17:58 ..
        -rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
        -rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
        -rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
```

进程替换可以比较两个目录的内容——来检查哪些文件在这个目录而不在那个目录。

```
diff <(ls $first_directory) <(ls $second_directory)
```

进程替换的一些其他用法：

```
read -a list < <( od -Ad -w24 -t u2 /dev/urandom )
#  从 /dev/urandom 读取一个随机数列表
#+ 用 "od" 处理
#+ 输送到 "read" 的标准输入. . .
#  来自 "insertion-sort.bash" 示例脚本。
#  致谢：JuanJo Ciarlante。
```

```
PORT=6881   # bittorrent（BT端口）

#  扫描端口，确保没有恶意行为
netcat -l $PORT | tee>(md5sum ->mydata-orig.md5) |
gzip | tee>(md5sum - | sed 's/-$/mydata.lz2/'>mydata-gz.md5)>mydata.gz

#  检查解压缩结果：
  gzip -d<mydata.gz | md5sum -c mydata-orig.md5)
#  对原件的MD5校验用来检查标准输入，并且探测压缩当中出现的问题。

#  Bill Davidsen 贡献了这个例子
#+ （ABS指南作者做了轻微修改）。
```

```
cat <(ls -l)
# 等价于    ls -l | cat

sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
#  列出 3 个主要 'bin' 目录的文件，按照文件名排序。
#  注意，有三个（数一下）单独的命令输送给了 'sort'。

diff <(command1) <(command2)    # 比较命令输出结果的不同之处。

tar cf >(bzip2 -c > file.tar.bz2) $directory_name

#  调用 "tar cf /dev/fd/?? $directory_name"，然后 "bzip2 -c > file.tar.bz2"。
#
#  因为 /dev/fd/<n> 系统特性
#  不需要在两个命令之间使用管道符
#
#  这个可以模拟
#
bzip2 -c < pipe > file.tar.bz2&
tar cf pipe $directory_name
rm pipe
#    或者
exec 3>&1
tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-
exec 3>&-

# 致谢：Stéphane Chazelas
```

在子shell中 [echo 命令用管道输送给 while-read 循环](http://tldp.org/LDP/abs/html/gotchas.html#BADREAD0)时会出现问题，下面是避免的方法：

**例23-1 不用 fork 的代码块重定向。**

```
#!/bin/bash

#  wr-ps.bash: 使用进程替换的 while-read 循环。

#  示例由 Tomas Pospisek 贡献。
# （ABS指南作者做了大量改动。）

echo

echo "random input" | while read i
do
  global=3D": Not available outside the loop."
  # ... 因为在子 shell 中运行。
done

echo "\$global (从子进程之外) = $global"
# $global (从子进程之外) =

echo; echo "--"; echo

while read i
do
  echo $i
  global=3D": Available outside the loop."
  # ... 因为没有在子 shell 中运行。
done < <( echo "random input" )
#    ^ ^

echo "\$global (使用进程替换) = $global"
#  随机输入
#  $global (使用进程替换)= 3D: Available outside the loop.


echo; echo "##########"; echo



# 同样道理 . . .

declare -a inloop
index=0
cat $0 | while read line
do
  inloop[$index]="$line"
  ((index++))
  # 在子 shell 中运行，所以 ...
done
echo "OUTPUT = "
echo ${inloop[*]}           # ... 什么也没有显示。


echo; echo "--"; echo


declare -a outloop
index=0
while read line
do
  outloop[$index]="$line"
  ((index++))
  # 没有在子 shell 中运行，所以 ...
done < <( cat $0 )
echo "OUTPUT = "
echo ${outloop[*]}          # ... 整个脚本的结果显示出来。

exit $?
```

下面是个类似的例子。

**例 23-2. 重定向进程替换的输出到一个循环内**

```
#!/bin/bash
# psub.bash
#  受 Diego Molina 启发（感谢！）。

declare -a array0
while read
do
  array0[${#array0[@]}]="$REPLY"
done < <( sed -e 's/bash/CRASH-BANG!/' $0 | grep bin | awk '{print $1}' )
#  由进程替换来设置'read'默认变量（$REPLY）。
#+ 然后将变量复制到一个数组。

echo "${array0[@]}"

exit $?

# ====================================== #
# 运行结果：
bash psub.bash

#!/bin/CRASH-BANG! done #!/bin/CRASH-BANG!
```

一个读者发来一个有趣的进程替换例子，如下：

```
# SuSE 发行版中提取的脚本片段：

# --------------------------------------------------------------#
while read  des what mask iface; do
# 一些命令 ...
done < <(route -n)  
#    ^ ^  第一个 < 是重定向，第二个是进程替换。

#  为了测试，我们让它来做点儿事情。
while read  des what mask iface; do
  echo $des $what $mask $iface
done < <(route -n)  

# 输出内容:
# Kernel IP routing table
# Destination Gateway Genmask Flags Metric Ref Use Iface
# 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
# --------------------------------------------------------------#

#  正如 Stéphane Chazelas 指出的,
#+ 一个更容易理解的等价代码如下：
route -n |
  while read des what mask iface; do   # 通过管道输出设置的变量
    echo $des $what $mask $iface
  done  #  这段代码的结果更上面的相同。
        #  但是，Ulrich Gayer 指出 . . .
        #+ 这段简化版等价代码在 while 循环里用了子 shell，
        #+ 因此当管道终止时变量都消失了。

# --------------------------------------------------------------#

#  然而，Filip Moritz 说上面的两个例子有一个微妙的区别，
#+ 见下面的代码

(
route -n | while read x; do ((y++)); done
echo $y # $y is still unset

while read x; do ((y++)); done < <(route -n)
echo $y # $y has the number of lines of output of route -n
)

#  更通俗地说（译者注：原文本行少了注释符）
(
: | x=x
# 似乎启动了子 shell ，就像
: | ( x=x )
# 而
x=x < <(:)
# 并没有。
)
#  这个方法在解析 csv 和类似格式时很有用。
#  也就是在效果上，原始 SuSE 系统的代码片段就是做这个用的。
```

注解 \[1] 这个与命名管道（使用临时文件）的效果相同，而且事实上，进程替换也曾经用过命名管道。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.clay-wangzhi.com/shell/part5/23_process_substitution.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
