第二十三章. 进程替换

管道 将一个命令的 标准输出 输送到另一个命令的 标准输入 是个强大的技术。但是如果你需要用管道输送多个命令的 标准输出 怎么办?这时候 进程替换 就派上用场了。

进程替换 把一个(或多个)进程 的输出送到另一个进程的 标准输入

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

  1. >(command_list)
  2. <(command_list)

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

23. 进程替换 - 图1“<”或”>”与括号之间没有空格,加上空格或报错。

  1. bash$ echo >(true)
  2. /dev/fd/63
  3. bash$ echo <(true)
  4. /dev/fd/63
  5. bash$ echo >(true) <(true)
  6. /dev/fd/63 /dev/fd/62
  7. bash$ wc <(cat /usr/share/dict/linux.words)
  8. 483523 483523 4992010 /dev/fd/63
  9. bash$ grep script /usr/share/dict/linux.words | wc
  10. 262 262 3601
  11. bash$ wc <(grep script /usr/share/dict/linux.words)
  12. 262 262 3601 /dev/fd/63

23. 进程替换 - 图2Bash用两个文件描述符创建管道,--fIn 和 fOut--true标准输入连接 fOut(dup2(fOut, 0)),然后Bash 传递一个 /dev/fd/fIn 参数给 echo 。在不使用 /dev/fd/<n> 的系统里,Bash可以用临时文件(感谢 S.C. 指出这点)。

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

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

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

  1. diff <(ls $first_directory) <(ls $second_directory)

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

  1. read -a list < <( od -Ad -w24 -t u2 /dev/urandom )
  2. # 从 /dev/urandom 读取一个随机数列表
  3. #+ 用 "od" 处理
  4. #+ 输送到 "read" 的标准输入. . .
  5. # 来自 "insertion-sort.bash" 示例脚本。
  6. # 致谢:JuanJo Ciarlante。
  1. PORT=6881 # bittorrent(BT端口)
  2. # 扫描端口,确保没有恶意行为
  3. netcat -l $PORT | tee>(md5sum ->mydata-orig.md5) |
  4. gzip | tee>(md5sum - | sed 's/-$/mydata.lz2/'>mydata-gz.md5)>mydata.gz
  5. # 检查解压缩结果:
  6. gzip -d<mydata.gz | md5sum -c mydata-orig.md5)
  7. # 对原件的MD5校验用来检查标准输入,并且探测压缩当中出现的问题。
  8. # Bill Davidsen 贡献了这个例子
  9. #+ (ABS指南作者做了轻微修改)。
  1. cat <(ls -l)
  2. # 等价于 ls -l | cat
  3. sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
  4. # 列出 3 个主要 'bin' 目录的文件,按照文件名排序。
  5. # 注意,有三个(数一下)单独的命令输送给了 'sort'。
  6. diff <(command1) <(command2) # 比较命令输出结果的不同之处。
  7. tar cf >(bzip2 -c > file.tar.bz2) $directory_name
  8. # 调用 "tar cf /dev/fd/?? $directory_name",然后 "bzip2 -c > file.tar.bz2"。
  9. #
  10. # 因为 /dev/fd/<n> 系统特性
  11. # 不需要在两个命令之间使用管道符
  12. #
  13. # 这个可以模拟
  14. #
  15. bzip2 -c < pipe > file.tar.bz2&
  16. tar cf pipe $directory_name
  17. rm pipe
  18. # 或者
  19. exec 3>&1
  20. tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-
  21. exec 3>&-
  22. # 致谢:Stéphane Chazelas

在子shell中 echo 命令用管道输送给 while-read 循环时会出现问题,下面是避免的方法:

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

  1. #!/bin/bash
  2. # wr-ps.bash: 使用进程替换的 while-read 循环。
  3. # 示例由 Tomas Pospisek 贡献。
  4. # (ABS指南作者做了大量改动。)
  5. echo
  6. echo "random input" | while read i
  7. do
  8. global=3D": Not available outside the loop."
  9. # ... 因为在子 shell 中运行。
  10. done
  11. echo "\$global (从子进程之外) = $global"
  12. # $global (从子进程之外) =
  13. echo; echo "--"; echo
  14. while read i
  15. do
  16. echo $i
  17. global=3D": Available outside the loop."
  18. # ... 因为没有在子 shell 中运行。
  19. done < <( echo "random input" )
  20. # ^ ^
  21. echo "\$global (使用进程替换) = $global"
  22. # 随机输入
  23. # $global (使用进程替换)= 3D: Available outside the loop.
  24. echo; echo "##########"; echo
  25. # 同样道理 . . .
  26. declare -a inloop
  27. index=0
  28. cat $0 | while read line
  29. do
  30. inloop[$index]="$line"
  31. ((index++))
  32. # 在子 shell 中运行,所以 ...
  33. done
  34. echo "OUTPUT = "
  35. echo ${inloop[*]} # ... 什么也没有显示。
  36. echo; echo "--"; echo
  37. declare -a outloop
  38. index=0
  39. while read line
  40. do
  41. outloop[$index]="$line"
  42. ((index++))
  43. # 没有在子 shell 中运行,所以 ...
  44. done < <( cat $0 )
  45. echo "OUTPUT = "
  46. echo ${outloop[*]} # ... 整个脚本的结果显示出来。
  47. exit $?

下面是个类似的例子。

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

  1. #!/bin/bash
  2. # psub.bash
  3. # 受 Diego Molina 启发(感谢!)。
  4. declare -a array0
  5. while read
  6. do
  7. array0[${#array0[@]}]="$REPLY"
  8. done < <( sed -e 's/bash/CRASH-BANG!/' $0 | grep bin | awk '{print $1}' )
  9. # 由进程替换来设置'read'默认变量($REPLY)。
  10. #+ 然后将变量复制到一个数组。
  11. echo "${array0[@]}"
  12. exit $?
  13. # ====================================== #
  14. # 运行结果:
  15. bash psub.bash
  16. #!/bin/CRASH-BANG! done #!/bin/CRASH-BANG!

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

  1. # SuSE 发行版中提取的脚本片段:
  2. # --------------------------------------------------------------#
  3. while read des what mask iface; do
  4. # 一些命令 ...
  5. done < <(route -n)
  6. # ^ ^ 第一个 < 是重定向,第二个是进程替换。
  7. # 为了测试,我们让它来做点儿事情。
  8. while read des what mask iface; do
  9. echo $des $what $mask $iface
  10. done < <(route -n)
  11. # 输出内容:
  12. # Kernel IP routing table
  13. # Destination Gateway Genmask Flags Metric Ref Use Iface
  14. # 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
  15. # --------------------------------------------------------------#
  16. # 正如 Stéphane Chazelas 指出的,
  17. #+ 一个更容易理解的等价代码如下:
  18. route -n |
  19. while read des what mask iface; do # 通过管道输出设置的变量
  20. echo $des $what $mask $iface
  21. done # 这段代码的结果更上面的相同。
  22. # 但是,Ulrich Gayer 指出 . . .
  23. #+ 这段简化版等价代码在 while 循环里用了子 shell,
  24. #+ 因此当管道终止时变量都消失了。
  25. # --------------------------------------------------------------#
  26. # 然而,Filip Moritz 说上面的两个例子有一个微妙的区别,
  27. #+ 见下面的代码
  28. (
  29. route -n | while read x; do ((y++)); done
  30. echo $y # $y is still unset
  31. while read x; do ((y++)); done < <(route -n)
  32. echo $y # $y has the number of lines of output of route -n
  33. )
  34. # 更通俗地说(译者注:原文本行少了注释符)
  35. (
  36. : | x=x
  37. # 似乎启动了子 shell ,就像
  38. : | ( x=x )
  39. # 而
  40. x=x < <(:)
  41. # 并没有。
  42. )
  43. # 这个方法在解析 csv 和类似格式时很有用。
  44. # 也就是在效果上,原始 SuSE 系统的代码片段就是做这个用的。

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