第34章 陷阱

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。以下划线开头的变量名就没问题.

6. # 然而 . . .   只用一个下划线作为变量名就不行。
7. _=25
8. echo $_           # $_是一个特殊变量, 代表最后一个命令的最后一个参数。
9. # 但是,_是一个有效的函数名!

11. xyz((!*=value2    # 引起严重的错误。
12. # Bash3.0之后,标点不能出现在变量名中。
  • 使用连字符或其他保留字符来做变量名(或函数名)。
1. var-1=23
2. # 用 'var_1 代替。

4. function-whatever ()   # 错误
5. # 用 ‘function_whatever ()’ 代替。

8. # Bash3.0之后,标点不能出现在函数名中。
9. function.whatever ()   # 错误
10. # 用 ‘functionWhatever ()’ 代替。
  • 让变量名与函数名相同。 这会使得脚本的可读性变得很差。
1. do_something ()
2. {
3. echo "This function does something with \"$1\"."
4. }

6. do_something=do_something

8. do_something do_something

10. # 这么做是合法的,但会让人混淆。
  • 不合时宜的使用空白符。与其他编程语言相比,Bash非常讲究空白符的使用。
1. var1 = 23   # ‘var1=23’才是正确的。
2. # 对于上边这一行来说,Bash会把“var1”当作命令来执行,
3. # “=”和“23”会被看作“命令”“var1”的参数。

5. let c = $a - $b   # ‘let c=$a-$b’或‘let "c = $a - $b"’才是正确的。

7. if [ $a -le 5]    # if [ $a -le 5 ]   是正确的。
8. #           ^^      if [ "$a" -le 5 ]   这么写更好。
9. # [[ $a -le 5 ]] 也行。
1. { ls -l; df; echo "Done." }
2. # bash: syntax error: unexpected end of file

4. { ls -l; df; echo "Done."; }
5. #                        ^     ### 最后的这条命令必须以分号结尾。
  • 假定未被初始化的变量(赋值前的变量)被“清0”。事实上,未初始化的变量值为“null”,而不是0。
1. #!/bin/bash

3. echo "uninitialized_var = $uninitialized_var"
4. # uninitialized_var =

6. # 但是 . . .
7. # if $BASH_VERSION ≥ 4.2; then

9. if [[ ! -v uninitialized_var ]]
10. then
11. uninitialized_var=0   # Initialize it to zero!
12. fi
  • 混淆测试符号=和-ep。请记住,=用于比较字符变量,而-ep用来比较整数。
1. if [ "$a" = 273 ]      # $a是整数还是字符串?
2. if [ "$a" -eq 273 ]    # $a为整数。

4. # 有些情况下,即使你混用-ep和=,也不会产生错误的结果。
5. # 然而 . . .

8. a=273.0   # 不是一个整数。

10. if [ "$a" = 273 ]
11. then
12. echo "Comparison works."
13. else
14. echo "Comparison does not work."
15. fi    # Comparison does not work.

17. # 与a=" 273"和a="0273"相同。

20. # 类似的, 如果对非整数值使用“-ep”的话,就会产生问题。

22. if [ "$a" -eq 273.0 ]
23. then
24. echo "a = $a"
25. fi  # 产生了错误消息而退出。
26. # test.sh: [: 273.0: integer expression expected

样例 34-1. 数字比较与字符串比较并不相同

1. #!/bin/bash
2. # bad-op.sh: 尝试一下对整数使用字符串比较。

4. echo
5. number=1

7. #  下面的"while循环"有两个过错误:
8. #+ 一个比较明显,而另一个比较隐蔽。

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. #+ 即便如此,比较两个整数仍是错误的。

20. echo "---------------------"

22. while [ "$number" \< 5 ]    #  1 2 3 4
23. do                          #
24. echo -n "$number "        #  看起来好像可以工作,但是 . . .
25. let "number += 1"         #+ 事实上是比较ASCII码,
26. done                      #+ 而不是整数比较。

28. echo; echo "---------------------"

30. # 这么做会产生问题。比如:

32. lesser=5
33. greater=105

35. if [ "$greater" \< "$lesser" ]
36. then
37. echo "$greater is less than $lesser"
38. fi                          # 105 is less than 5
39. #  事实上,在字符串比较中(按照ASCII码的顺序)
40. #+ "105"小于"5"。

42. echo

44. exit 0
  • 试图用let来设置字符串变量。
1. let "a = hello, you"
2. echo "$a"   # 0
  • 有时候在“test”中括号([ ])结构里的变量需要被引用起来(双引号)。如果不这么做的话,可能会引起不可预料的结果。请参考例子 7-6,例子 16-5,例子 9-6。
  • 为防分隔,用双引号引用一个包含空白符的变量。 有些情况下,这会产生意想不到的后果。
  • 脚本中的命令可能会因为脚本宿主不具备相应的运行权限而导致运行失败。如果用户在命令行中不能调用这个命令的话,那么即使把它放到脚本中来运行,也还是会失败。这时可以通过修改命令的属性来解决这个问题,有时候甚至要给它设置suid位(当然, 要以root身份来设置)。
  • 试图使用-作为作为重定向操作符(事实上它不是),通常都会导致令人不快的结果。
1. command1 2> - | command2
2. # 试图将command1的错误输出重定向到一个管道中 . . .
3. # . . . 不会工作。

5. command1 2>& - | command2  # 也没效果。

7. 感谢,S.C。
  • 使用Bash 2.0或更高版本的功能,可以在产生错误信息的时候,引发修复动作。但是比较老的Linux机器默认安装的可能是Bash 1.XX。
1. #!/bin/bash

3. minimum_version=2
4. # 因为Chet Ramey经常给Bash添加一些新的特征,
5. # 所以你最好将$minimum_version设置为2.XX,3.XX,或是其他你认为比较合适的值。
6. E_BAD_VERSION=80

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

15. ...
1. var=1 && ((--var)) && echo $var
2. #        ^^^^^^^^^ 在这里,这个与列表返回错误代码1而终止。
3. #                     不会打印$var的值!
4. echo $?   # 1
  • 一个带有DOS风格换行符(\r\n)的脚本将会运行失败,因为#!/bin/bash\r\n是不合法的,与我们所期望的#!/bin/bash\n不同,解决办法就是将这个脚本转换为UNIX风格的换行符。
1. #!/bin/bash

3. echo "Here"

5. unix2dos $0    # 脚本先将自己改为DOS格式。
6. chmod 755 $0   # 更改可执行权限。
7. # 'unix2dos'会删除可执行权限

9. ./$0           # 脚本尝试再次运行自己。
10. # 但它作为一个DOS文件,已经不能运行了。

12. echo "There"

14. exit 0
  • 以#!/bin/sh开头的Bash脚本,不能在完整的Bash兼容模式下运行。某些Bash特定的功能可能会被禁用。如果脚本需要完整的访问所有Bash专有扩展,那么它需要使用#!/bin/bash作为开头。
  • 如果在here document中,结尾的limit string之前加上空白字符的话,将会导致脚本的异常行为。
  • 在一个输出被捕获的函数中放置了不止一个echo语句。
1. add2 ()
2. {
3. echo "Whatever ... "   # 删掉zhehan
4. let "retval = $1 + $2"
5. echo $retval
6. }

8. num1=12
9. num2=43
10. echo "Sum of $num1 and $num2 = $(add2 $num1 $num2)"

12. #   Sum of 12 and 43 = Whatever ...
13. #   55

15. #        这些echo连在一起了。

这是行不通的。

  • 脚本不能将变量export到它的父进程(即调用这个脚本的shell),或父进程的环境中。就好比我们在生物学中所学到的那样,子进程只会继承父进程, 反过来则不行。
1. WHATEVER=/home/bozo
2. export WHATEVER
3. exit 0
1. bash$ echo $WHATEVER
2. bash$
  • 可以确定的是,即使回到命令行提示符,变量$WHATEVER仍然没有被设置。
  • 在子shell中设置和操作变量之后,如果尝试在子shell作用域之外使用同名变量的话, 将会产生令人不快的结果。

样例 34-2. 子shell缺陷

1. #!/bin/bash
2. # 子shell中的变量缺陷。

4. outer_variable=outer
5. echo
6. echo "outer_variable = $outer_variable"
7. echo

9. (
10. # 开始子shell

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  # 会修改全局变量吗?
16. echo "outer_variable inside subshell = $outer_variable"

18. # 如果将变量‘导出’会产生不同的结果么?
19. #    export inner_variable
20. #    export outer_variable
21. # 试试看。

23. # 结束子shell
24. )

26. echo
27. echo "inner_variable outside subshell = $inner_variable"  # Unset.
28. echo "outer_variable outside subshell = $outer_variable"  # Unchanged.
29. echo

31. exit 0

33. # 如果你去掉第19和第20行的注释会怎样?
34. # 会产生不同的结果吗?
  • 将echo的输出通过管道传递给read命令可能会产生不可预料的结果。在这种情况下,read命令的行为就好像它在子shell中运行一样。可以使用set命令来代替(就好像例子15-18一样)。

样例 34-3. 将echo的输出通过管道传递给read命令

1. #!/bin/bash
2. #  badread.sh:
3. #  尝试使用'echo'和'read'命令
4. #+ 非交互的给变量赋值。

6. #   shopt -s lastpipe

8. a=aaa
9. b=bbb
10. c=ccc

12. echo "one two three" | read a b c
13. # 尝试重新给变量a,b,和c赋值。

15. echo
16. echo "a = $a"  # a = aaa
17. echo "b = $b"  # b = bbb
18. echo "c = $c"  # c = ccc
19. # 重新赋值失败。

21. ### 但如果 . . .
22. ##  去掉第6行的注释:
23. #   shopt -s lastpipe
24. ##+ 就能解决这个问题!
25. ### 这是Bash 4.2版本的新特性。

27. # ------------------------------

29. # 试试下边这种方法。

31. var=`echo "one two three"`
32. set -- $var
33. a=$1; b=$2; c=$3

35. echo "-------"
36. echo "a = $a"  # a = one
37. echo "b = $b"  # b = two
38. echo "c = $c"  # c = three
39. # 重新赋值成功。

41. # ------------------------------

43. #  也请注意,echo到'read'的值只会在子shell中起作用。
44. #  所以,变量的值*只*会在子shell中被修改。

46. a=aaa          # 重新开始。
47. b=bbb
48. c=ccc

50. echo; echo
51. echo "one two three" | ( read a b c;
52. echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
53. # a = one
54. # b = two
55. # c = three
56. echo "-----------------"
57. echo "Outside subshell: "
58. echo "a = $a"  # a = aaa
59. echo "b = $b"  # b = bbb
60. echo "c = $c"  # c = ccc
61. echo

63. exit 0

事实上,也正如Anthony Richardson指出的那样,通过管道将输出传递到任何循环中, 都会引起类似的问题。

1. # 循环的管道问题。
2. #  这个例子由Anthony Richardson编写,
3. #+ 由Wilbert Berendsen补遗。

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

21. #  变量foundone在这里肯定是false,
22. #+ 因为它是在子shell中被设置为true的。
23. if [ $foundone = false ]
24. then
25. echo "No files need archiving."
26. fi

28. # =====================现在,下边是正确的方法:=================

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

38. if [ $foundone = false ]
39. then
40. echo "No files need archiving."
41. fi

43. # ==================这里是另一种方法==================

45. #  将脚本中读取变量的部分放到一个代码块中,
46. #+ 这样一来,它们就能在相同的子shell中共享了。
47. #  感谢,W.B。

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

58. if ! $foundone
59. then
60. echo "No files need archiving."
61. fi
62. }
  • 一个相关的问题:当你尝试将tail -f的stdout通过管道传递给grep时,会产生问题。
1. tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
2. #  “error.log”文件将不会写入任何东西。
3. #  正如Samuli Kaipiainen指出的那样,
4. #+ 这一结果是从grep的缓冲区输出的。
5. #  解决的办法就是把“--line-buffered”参数添加到grep中。
  • 在脚本中使用“suid”命令是非常危险的,因为这会危及系统安全。^suid
  • 使用shell脚本来编写CGI程序是值得商榷的。因为Shell脚本的变量不是“类型安全”的,当CGI被关联的时候,可能会产生令人不快的行为。此外,它还很难抵挡住“破解的考验”。
  • Bash不能正确地处理双斜线(//)字符串。
  • 在Linux或BSD上编写的Bash脚本,可能需要修改一下,才能使它们运行在商业的UNIX机器上。这些脚本通常都使用GNU命令和过滤工具,GNU工具通常都比一般的UNIX上的同类工具更加强大。这方面的一个非常明显的例子就是,文本处理工具tr。
  • 遗憾的是,更新Bash本身就会破坏过去工作完全正常的脚本。让我们回顾一下使用无正式文件的Bash功能有多危险。

危险正在接近你 — 小心,小心,小心,小心。 许多勇敢的心都在沉睡。 所以一定要小心 — 小心。

——A.J. Lamb and H.W. Petrie

注意事项