Chapter 14. 命令替换

命令替换 将会重新分配一个命令[1]甚至是多个命令的输出; 它会将命令的输出如实地添加到另一个上下文中. [2]

使用命令替换的典型形式是使用后置引用(`...`). 后置引用形式的命令(就是被反引号括起来)将会产生命令行文本.
   1 script_name=`basename $0`
   2 echo "The name of this script is $script_name."

这样的话, 命令的输出可以被当成传递到另一个命令的参数, 或者保存到变量中, 甚至可以用来产生for循环的参数列表.

   1 rm `cat filename`   # "filename" 包含了需要被删除的文件列表.
   2 #
   3 # S. C. 指出使用这种形式, 可能会产生"参数列表太长"的错误.
   4 # 更好的方法是              xargs rm -- < filename 
   5 # ( -- 同时覆盖了那些以"-"开头的文件所产生的特殊情况 )
   6 
   7 textfile_listing=`ls *.txt`
   8 # 变量中包含了当前工作目录下所有的*.txt文件.
   9 echo $textfile_listing
  10 
  11 textfile_listing2=$(ls *.txt)   # 这是命令替换的另一种形式.
  12 echo $textfile_listing2
  13 # 同样的结果.
  14 
  15 # 将文件列表放入到一个字符串中的一个可能的问题就是
  16 # 可能会混进一个新行.
  17 #
  18 # 一个安全的将文件列表传递到参数中的方法就是使用数组.
  19 #      shopt -s nullglob    # 如果不匹配, 那就不进行文件名扩展.
  20 #      textfile_listing=( *.txt )
  21 #
  22 # Thanks, S.C.

命令替换将会调用一个subshell.

命令替换可能会引起word splitting.
   1 COMMAND `echo a b`     # 2个参数: a and b
   2 
   3 COMMAND "`echo a b`"   # 1个参数: "a b"
   4 
   5 COMMAND `echo`         # 无参数
   6 
   7 COMMAND "`echo`"       # 一个空的参数
   8 
   9 
  10 # Thanks, S.C.

即使没有引起word splitting, 命令替换也会去掉多余的新行.
   1 # cd "`pwd`"  # 这句总会正常的工作.
   2 # 然而...
   3 
   4 mkdir 'dir with trailing newline
   5 '
   6 
   7 cd 'dir with trailing newline
   8 '
   9 
  10 cd "`pwd`"  # 错误消息:
  11 # bash: cd: /tmp/file with trailing newline: No such file or directory
  12 
  13 cd "$PWD"   # 运行良好.
  14 
  15 
  16 
  17 
  18 
  19 old_tty_setting=$(stty -g)   # 保存老的终端设置.
  20 echo "Hit a key "
  21 stty -icanon -echo           # 对终端禁用"canonical"模式.
  22                              # 这样的话, 也会禁用了*本地*的echo.
  23 key=$(dd bs=1 count=1 2> /dev/null)   # 使用'dd'命令来取得一个按键.
  24 stty "$old_tty_setting"      # 保存老的设置.
  25 echo "You hit ${#key} key."  # ${#variable} = number of characters in $variable
  26 #
  27 # 按键任何键除了回车, 那么输出就是"You hit 1 key."
  28 # 按下回车, 那么输出就是"You hit 0 key."
  29 # 新行已经被命令替换吃掉了.
  30 
  31 Thanks, S.C.

当一个变量是使用命令替换的结果做为值的时候, 然后使用echo命令来输出这个变量(并且不引用这个变量, 就是不用引号括起来), 那么命令替换将会从最终的输出中删掉换行符. 这可能会引起一些异常情况.
   1 dir_listing=`ls -l`
   2 echo $dir_listing     # 未引用, 就是没用引号括起来
   3 
   4 # 想打出来一个有序的目录列表.Expecting a nicely ordered directory listing.
   5 
   6 # 可惜, 下边将是我们所获得的:
   7 # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
   8 # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
   9 
  10 # 新行消失了.
  11 
  12 
  13 echo "$dir_listing"   # 用引号括起来
  14 # -rw-rw-r--    1 bozo       30 May 13 17:15 1.txt
  15 # -rw-rw-r--    1 bozo       51 May 15 20:57 t2.sh
  16 # -rwxr-xr-x    1 bozo      217 Mar  5 21:13 wi.sh

命令替换甚至允许将整个文件的内容放到变量中, 可以使用重定向或者cat命令.

   1 variable1=`<file1`      #  将"file1"的内容放到"variable1"中.
   2 variable2=`cat file2`   #  将"file2"的内容放到"variable2"中.
   3                         #  但是这行将会fork一个新进程, This, however, forks a new process, 
   4                         #+ 所以这行代码将会比第一行代码执行得慢.
   5 
   6 #  注意:
   7 #  变量中是可以包含空白的,
   8 #+ 甚至是 (厌恶至极的), 控制字符.

   1 #  摘录自系统文件, /etc/rc.d/rc.sysinit
   2 #+ (这是红帽安装中使用的)
   3 
   4 
   5 if [ -f /fsckoptions ]; then
   6         fsckoptions=`cat /fsckoptions`
   7 ...
   8 fi
   9 #
  10 #
  11 if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
  12              hdmedia=`cat /proc/ide/${disk[$device]}/media`
  13 ...
  14 fi
  15 #
  16 #
  17 if [ ! -n "`uname -r | grep -- "-"`" ]; then
  18        ktag="`cat /proc/version`"
  19 ...
  20 fi
  21 #
  22 #
  23 if [ $usb = "1" ]; then
  24     sleep 5
  25     mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"`
  26     kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"`
  27 ...
  28 fi

不要将一个非常长的文本文件的内容设置到一个变量中, 除非你有一个非常好的原因非要这么做不可. 不要将2进制文件的内容保存到变量中.


Example 14-1. 愚蠢的脚本策略

   1 #!/bin/bash
   2 # stupid-script-tricks.sh: 朋友, 别在家这么做.
   3 # 来自于"Stupid Script Tricks," 卷I.
   4 
   5 
   6 dangerous_variable=`cat /boot/vmlinuz`   # 这是压缩过的Linux内核本身.
   7 
   8 echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
   9 # 这个字符串变量的长度是 $dangerous_variable = 794151
  10 # (不要使用'wc -c /boot/vmlinuz'来计算长度.)
  11 
  12 # echo "$dangerous_variable"
  13 # 千万别尝试这么做! 这样将挂起这个脚本.
  14 
  15 
  16 #  文档作者已经意识到将二进制文件设置到
  17 #+ 变量中是一个没用的应用.
  18 
  19 exit 0

注意, 在这里是不会发生缓冲区溢出错误. 因为这是一个解释型语言的实例, Bash就是一种解释型语言, 解释型语言会比编译型语言提供更多的对程序错误的保护措施.

变量替换允许将一个循环的输出放入到一个变量中.这么做的关键就是将循环中echo命令的输出全部截取.


Example 14-2. 从循环的输出中产生一个变量

   1 #!/bin/bash
   2 # csubloop.sh: 从循环的输出中产生一个变量.
   3 
   4 variable1=`for i in 1 2 3 4 5
   5 do
   6   echo -n "$i"                 #  对于这里的命令替换来说
   7 done`                          #+ 这个'echo'命令是非常关键的.
   8 
   9 echo "variable1 = $variable1"  # variable1 = 12345
  10 
  11 
  12 i=0
  13 variable2=`while [ "$i" -lt 10 ]
  14 do
  15   echo -n "$i"                 # 再来一个, 'echo'是必须的.
  16   let "i += 1"                 # 递增.
  17 done`
  18 
  19 echo "variable2 = $variable2"  # variable2 = 0123456789
  20 
  21 #  这就证明了在一个变量声明中
  22 #+ 嵌入一个循环是可行的.
  23 
  24 exit 0

对于命令替换来说,$(COMMAND) 形式已经取代了反引号"`".

   1 output=$(sed -n /"$1"/p $file)   # 来自于 "grp.sh"例子.
   2 	      
   3 # 将一个文本的内容保存到变量中.
   4 File_contents1=$(cat $file1)      
   5 File_contents2=$(<$file2)        # Bash 也允许这么做.

$(...) 形式的命令替换在处理双反斜线(\\)时与`...`形式不同.

 bash$ echo `echo \\`
 
 
 bash$ echo $(echo \\)
 \
 	      

$(...) 形式的命令替换是允许嵌套的. [3]

   1 word_count=$( wc -w $(ls -l | awk '{print $9}') )

或者, 可以更加灵活. . .


Example 14-3. 找anagram(回文构词法, 可以将一个有意义的单词, 变换为1个或多个有意义的单词, 但是还是原来的子母集合)

   1 #!/bin/bash
   2 # agram2.sh
   3 # 关于命令替换嵌套的例子.
   4 
   5 #  使用"anagram"工具
   6 #+ 这是作者的"yawl"文字表包中的一部分.
   7 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
   8 #  http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
   9 
  10 E_NOARGS=66
  11 E_BADARG=67
  12 MINLEN=7
  13 
  14 if [ -z "$1" ]
  15 then
  16   echo "Usage $0 LETTERSET"
  17   exit $E_NOARGS         # 脚本需要一个命令行参数.
  18 elif [ ${#1} -lt $MINLEN ]
  19 then
  20   echo "Argument must have at least $MINLEN letters."
  21   exit $E_BADARG
  22 fi
  23 
  24 
  25 
  26 FILTER='.......'         # 必须至少有7个字符.
  27 #       1234567
  28 Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
  29 #           |     |    嵌套的命令替换        | |
  30 #        (              数组分配                 )
  31 
  32 echo
  33 echo "${#Anagrams[*]}  7+ letter anagrams found"
  34 echo
  35 echo ${Anagrams[0]}      # 第一个anagram.
  36 echo ${Anagrams[1]}      # 第二个anagram.
  37                          # 等等.
  38 
  39 # echo "${Anagrams[*]}"  # 在一行上列出所有的anagram . . .
  40 
  41 #  考虑到后边还有"数组"作为单独的一章进行讲解,
  42 #+ 这里就不深入了.
  43 
  44 # 可以参阅agram.sh脚本, 这也是一个找出anagram的例子.
  45 
  46 exit $?

命令替换在脚本中使用的例子:

  1. Example 10-7

  2. Example 10-26

  3. Example 9-28

  4. Example 12-3

  5. Example 12-19

  6. Example 12-15

  7. Example 12-49

  8. Example 10-13

  9. Example 10-10

  10. Example 12-29

  11. Example 16-8

  12. Example A-17

  13. Example 27-2

  14. Example 12-42

  15. Example 12-43

  16. Example 12-44

注意事项:

[1]

对于命令替换来说, 这个命令可以是外部的系统命令, 也可以是内部脚本的内建命令, 甚至是一个脚本函数.

[2]

从技术的角度来讲, 命令替换将会抽取出一个命令的输出, 然后使用=操作赋值到一个变量中.

[3]

事实上, 对于反引号的嵌套是可行的, 但是只能将内部的反引号转义才行, 就像John默认指出的那样.
   1 word_count=` wc -w \`ls -l | awk '{print $9}'\` `