第31章. Gotchas

 

Turandot: Gli enigmi sono tre, la morte una!

Caleph: No, no! Gli enigmi sono tre, una la vita!

 Puccini

将保留字和字符声明为变量名.
   1 case=value0       # 引发错误.
   2 23skidoo=value1   # 也会有错误.
   3 # 以数字开头的变量名是由shell保留使用的.
   4 # 试试 _23skidoo=value1. 用下划线开头的变量名是允许的.
   5 
   6 # 但是 . . .   仅使用下划线来用做变量名也是不行的.
   7 _=25
   8 echo $_           # $_ 是一个特殊的变量,被设置为最后命令的最后一个参数.
   9 
  10 xyz((!*=value2    # 引起严重的错误.
  11 # 在第三版的Bash, 标点不能在变量名中出现.

用连字符或其他保留字符当做变量名(或函数名).
   1 var-1=23
   2 # 用 'var_1' 代替.
   3 
   4 function-whatever ()   # 错误
   5 # 用 'function_whatever ()' 代替.
   6 
   7  
   8 # 在第三版的 Bash, 标点不能在函数名中使用.
   9 function.whatever ()   # 错误
  10 # 用 'functionWhatever ()' 代替.

给变量和函数使用相同的名字. 这会使脚本不能分辨两者.
   1 do_something ()
   2 {
   3   echo "This function does something with \"$1\"."
   4 }
   5 
   6 do_something=do_something
   7 
   8 do_something do_something
   9 
  10 # 这些都是合法的,但让人混淆.

不适当地使用宽白符(whitespace). 和其它的编程语言相比,Bash非常讲究空白字符的使用.
   1 var1 = 23   # 'var1=23' 正确.
   2 # 上面一行,Bash试图执行命令"var1"
   3 # 并且它的参数是"="和"23".
   4 	
   5 let c = $a - $b   # 'let c=$a-$b' 或 'let "c = $a - $b"'是正确的.
   6 
   7 if [ $a -le 5]    # if [ $a -le 5 ]   是正确的.
   8 # if [ "$a" -le 5 ]   会更好.
   9 # [[ $a -le 5 ]] 也可以.

未初始化的变量(指赋值前的变量)被认为是NULL值的,而不是有零值.
   1 #!/bin/bash
   2 
   3 echo "uninitialized_var = $uninitialized_var"
   4 # uninitialized_var =

混淆测试里的=-eq 操作符. 请记住, = 是比较字符变量而 -eq 比较整数.
   1 if [ "$a" = 273 ]      # $a 是一个整数还是一个字符串?
   2 if [ "$a" -eq 273 ]    # 如果$a 是一个整数,用这个表达式.
   3 
   4 # 有时你能混用 -eq 和 = 而没有不利的结果.
   5 # 然而 . . .
   6 
   7 
   8 a=273.0   # 不是一个整数.
   9 	   
  10 if [ "$a" = 273 ]
  11 then
  12   echo "Comparison works."
  13 else  
  14   echo "Comparison does not work."
  15 fi    # Comparison does not work.
  16 
  17 # 与   a=" 273"  和 a="0273" 一样.
  18 
  19 
  20 # 同样, 问题仍然是试图对非整数值使用 "-eq" 测试.
  21 	   
  22 if [ "$a" -eq 273.0 ]
  23 then
  24   echo "a = $a"
  25 fi  # 因错误信息而中断.  
  26 # test.sh: [: 273.0: integer expression expected

误用字符串比较操作符.


例子 31-1. 数字和字符串比较是不相等同的

   1 #!/bin/bash
   2 # bad-op.sh: 在整数比较中使用字符串比较.
   3 
   4 echo
   5 number=1
   6 
   7 # 下面的 "while" 循环有两个错误:
   8 #+ 一个很明显,另一个比较隐蔽.
   9 
  10 while [ "$number" < 5 ]    # 错误! 应该是:  while [ "$number" -lt 5 ]
  11 do
  12   echo -n "$number "
  13   let "number += 1"
  14 done  
  15 #  尝试运行时会收到错误信息而退出:
  16 #+ bad-op.sh: line 10: 5: No such file or directory
  17 #  在单括号里, "<" 需要转义,
  18 #+ 而即使是如此, 对此整数比较它仍然是错的.
  19 
  20 
  21 echo "---------------------"
  22 
  23 
  24 while [ "$number" \< 5 ]    #  1 2 3 4
  25 do                          #
  26   echo -n "$number "        #  看起来好像是能工作的, 但 . . .
  27   let "number += 1"         #+ 它其实是在对 ASCII 码的比较,
  28 done                        #+ 而非是对数值的比较.
  29 
  30 echo; echo "---------------------"
  31 
  32 # 下面这样便会引起问题了. 例如:
  33 
  34 lesser=5
  35 greater=105
  36 
  37 if [ "$greater" \< "$lesser" ]
  38 then
  39   echo "$greater is less than $lesser"
  40 fi                          # 105 is less than 5
  41 #  事实上, "105" 小于 "5"
  42 #+ 是因为使用了字符串比较 (以ASCII码的排序顺序比较).
  43 
  44 echo
  45 
  46 exit 0

有时在测试时的方括号([ ])里的变量需要引用起来(双引号). 如果没有这么做可能会引起不可预料的结果. 参考例子 7-6, 例子 16-5, 和 例子 9-6.

在脚本里的命令可能会因为脚本没有运行权限而导致运行失败. 如果用户不能在命令行里调用一个命令,即使把这个命令加到一个脚本中也一样会失败. 这时可以尝试更改访命令的属性,甚至可能给它设置suid位(当然是以root来设置).

试图用 - 来做重定向操作(事实上它不是操作符)会导致令人讨厌的意外.
   1 command1 2> - | command2  # 试图把command1的错误重定向到一个管道里...
   2 #    ...不会工作.	
   3 
   4 command1 2>& - | command2  # 也没有效果.
   5 
   6 Thanks, S.C.

用 Bash 版本 2+ 的功能可以当有错误信息时引发修复动作. 老一些的 Linux机器可能默认的安装是 1.XX 版本的Bash.
   1 #!/bin/bash
   2 
   3 minimum_version=2
   4 # 因为 Chet Ramey 经常给Bash增加新的特性,
   5 # 你把 $minimum_version 设为 2.XX比较合适,或者是其他合适的值.
   6 E_BAD_VERSION=80
   7 
   8 if [ "$BASH_VERSION" \< "$minimum_version" ]
   9 then
  10   echo "This script works only with Bash, version $minimum or greater."
  11   echo "Upgrade strongly recommended."
  12   exit $E_BAD_VERSION
  13 fi
  14 
  15 ...

在非Linux的机器上使用Bourne shell脚本(#!/bin/sh)的Bash专有功能可能会引起不可预料的行为. Linux系统通常都把sh 取别名为 bash, 但在其他的常见的UNIX系统却不一定是这样.

使用Bash中没有文档化的属性是危险的尝试. 在这本书的前几版中有几个脚本依赖于exit或return的值没有限制不能用负整数(虽然限制了exitreturn 的最大值是255). 不幸地是, 在版本 2.05b 以上这种情况就消失了. 参考See 例子 23-9.

一个带有DOS风格新行符 (\r\n) 的脚本会执行失败, 因为#!/bin/bash\r\n 不是合法的,不同于合法的#!/bin/bash\n. 解决办法就是把脚本转换成UNIX风格的新行符.
   1 #!/bin/bash
   2 
   3 echo "Here"
   4 
   5 unix2dos $0    # 脚本先把自己改成DOS格式.
   6 chmod 755 $0   # 更改回执行权限.
   7                # 'unix2dos'命令会删除执行权限.
   8 
   9 ./$0           # 脚本尝试再次运行自己本身.
  10                # 但它是一个DOS文件而不会正常工作了.
  11 
  12 echo "There"
  13 
  14 exit 0

shell脚本以 #!/bin/sh 行开头将不会在Bash兼容的模式下运行. 一些Bash专有的功能可能会被禁用掉. 那些需要完全使用Bash专有扩展特性的脚本应该用#!/bin/bash开头.

脚本里在 here document终结输入的字符串前加入空白字符会引起不可预料的结果.

脚本不能export(导出)变量到它的父进程(parent process),或父进程的环境里. 就像我们学的生物一样,一个子进程可以从父进程里继承但不能去影响父进程.
   1 WHATEVER=/home/bozo
   2 export WHATEVER
   3 exit 0
 bash$ echo $WHATEVER
 
 bash$ 
可以确定, 回到命令提示符, $WHATEVER 变量仍然没有设置.

子SHELL(subshell)设置和操作变量 , 然后尝试在子SHELL的作用范围外使用相同名的变量将会导致非期望的结果.


例子 31-2. 子SHELL缺陷

   1 #!/bin/bash
   2 # 在子SHELL中的变量缺陷.
   3 
   4 outer_variable=outer
   5 echo
   6 echo "outer_variable = $outer_variable"
   7 echo
   8 
   9 (
  10 # 子SHELL开始
  11 
  12 echo "outer_variable inside subshell = $outer_variable"
  13 inner_variable=inner  # Set
  14 echo "inner_variable inside subshell = $inner_variable"
  15 outer_variable=inner  # Will value change globally?
  16 echo "outer_variable inside subshell = $outer_variable"
  17 
  18 # 导出变量会有什么不同吗?
  19 #    export inner_variable
  20 #    export outer_variable
  21 # 试试看.
  22 
  23 # 子SHELL结束
  24 )
  25 
  26 echo
  27 echo "inner_variable outside subshell = $inner_variable"  # Unset.
  28 echo "outer_variable outside subshell = $outer_variable"  # Unchanged.
  29 echo
  30 
  31 exit 0
  32 
  33 # 如果你没有注释第 19 和 20行会怎么样?
  34 # 会有什么不同吗?

echo 的输出用管道(Piping)输送给read命令可能会产生不可预料的结果. 在这个情况下, read 表现地好像它是在一个子SHELL里一样. 可用set 命令代替 (就像在例子 11-16里的一样).


例子 31-3. 把echo的输出用管道输送给read命令

   1 #!/bin/bash
   2 #  badread.sh:
   3 #  尝试用 'echo 和 'read'
   4 #+ 来达到不用交互地给变量赋值的目的.
   5 
   6 a=aaa
   7 b=bbb
   8 c=ccc
   9 
  10 echo "one two three" | read a b c
  11 # 试图重新给 a, b, 和 c赋值.
  12 
  13 echo
  14 echo "a = $a"  # a = aaa
  15 echo "b = $b"  # b = bbb
  16 echo "c = $c"  # c = ccc
  17 # 重新赋值失败.
  18 
  19 # ------------------------------
  20 
  21 # 用下面的另一种方法.
  22 
  23 var=`echo "one two three"`
  24 set -- $var
  25 a=$1; b=$2; c=$3
  26 
  27 echo "-------"
  28 echo "a = $a"  # a = one
  29 echo "b = $b"  # b = two
  30 echo "c = $c"  # c = three 
  31 # 重新赋值成功.
  32 
  33 # ------------------------------
  34 
  35 #  也请注意echo值到'read'命令里是在一个子SHELL里起作用的.
  36 #  所以,变量的值只在子SHELL里被改变了.
  37 
  38 a=aaa          # 从头开始.
  39 b=bbb
  40 c=ccc
  41 
  42 echo; echo
  43 echo "one two three" | ( read a b c;
  44 echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
  45 # a = one
  46 # b = two
  47 # c = three
  48 echo "-----------------"
  49 echo "Outside subshell: "
  50 echo "a = $a"  # a = aaa
  51 echo "b = $b"  # b = bbb
  52 echo "c = $c"  # c = ccc
  53 echo
  54 
  55 exit 0

事实上, 也正如 Anthony Richardson 指出的那样, 管道任何的数据到循环里都会引起相似的问题.

   1 # 循环管道问题.
   2 #  Anthony Richardson编写此例,
   3 #+ Wilbert Berendsen补遗此例.
   4 
   5 
   6 foundone=false
   7 find $HOME -type f -atime +30 -size 100k |
   8 while true
   9 do
  10    read f
  11    echo "$f is over 100KB and has not been accessed in over 30 days"
  12    echo "Consider moving the file to archives."
  13    foundone=true
  14    # ------------------------------------
  15    echo "Subshell level = $BASH_SUBSHELL"
  16    # Subshell level = 1
  17    # 没错, 现在是在子shell里头运行.
  18    # ------------------------------------
  19 done
  20    
  21 #  foundone 变量在此总是有false值
  22 #+ 因此它是在子SHELL里被设为true值的
  23 if [ $foundone = false ]
  24 then
  25    echo "No files need archiving."
  26 fi
  27 
  28 # =====================现在, 使用正确的方法:=================
  29 
  30 foundone=false
  31 for f in $(find $HOME -type f -atime +30 -size 100k)  # 没有使用管道.
  32 do
  33    echo "$f is over 100KB and has not been accessed in over 30 days"
  34    echo "Consider moving the file to archives."
  35    foundone=true
  36 done
  37    
  38 if [ $foundone = false ]
  39 then
  40    echo "No files need archiving."
  41 fi
  42 
  43 # ==================另一种方法==================
  44 
  45 #  脚本中读变量值的相应部分替换在代码块里头读变量,
  46 #+ 这使变量能在相同的子SHELL里共享了.
  47 #  Thank you, W.B.
  48 
  49 find $HOME -type f -atime +30 -size 100k | {
  50      foundone=false
  51      while read f
  52      do
  53        echo "$f is over 100KB and has not been accessed in over 30 days"
  54        echo "Consider moving the file to archives."
  55        foundone=true
  56      done
  57 
  58      if ! $foundone
  59      then
  60        echo "No files need archiving."
  61      fi
  62 }

相关的问题是:当尝试写 tail -f 的输出给管道并传递给grep时会发生问题.
   1 tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
   2 # "error.log"文件里将不会写入任何东西.

--

在脚本中使用"suid" 的命令是危险的, 因为这会危及系统安全. [1]

用shell编写CGI程序是值得商榷的. Shell脚本的变量不是"类型安全的", 这样它用于CGI连接使用时会引发不希望的结果. 其次, 它很难防范骇客的攻击.

Bash 不能正确处理双斜线 (//) 字符串.

在Linux 或 BSD上写的Bash脚本可能需要修正以使它们也能在商业的UNIX (或 Apple OSX)上运行. 这些脚本常使用比一般的UNIX系统上的同类工具更强大功能的GNU 命令和过滤工具. 这方面一个明显的例子是文本处理工具tr.

 

Danger is near thee --

Beware, beware, beware, beware.

Many brave hearts are asleep in the deep.

So beware --

Beware.

 A.J. Lamb and H.W. Petrie

Notes

[1]

给脚本设置suid 权限是没有用的.