11.4 测试与分支

case 和 select 结构并不属于循环结构,因为它们并没有反复执行代码块。但是和循环结构相似的是,它们会根据代码块顶部或尾部的条件控制程序流。

下面介绍两种在代码块中控制程序流的方法:

case (in) / esac

在 shell 脚本中,case 模拟了 C/C++ 语言中的 switch,可以根据条件跳转到其中一个分支。其相当于简写版的 if/then/else 语句。很适合用来创建菜单选项哟!

1. case "$variable" in
2. "$condition1" )
3. command...
4. ;;
5. "$condition2" )
6. command...
7. ;;
8. esac

note

  • 对变量进行引用不是必须的,因为在这里不会进行字符分割。
  • 条件测试语句必须以右括号 ) 结束。[^1]
  • 每一段代码块都必须以双分号 ;; 结束。
  • 如果测试条件为真,其对应的代码块将被执行,而后整个 case 代码段结束执行。
  • case 代码段必须以 esac 结束(倒着拼写case)。

样例 11-25. 如何使用 case

1. #!/bin/bash
2. # 测试字符的种类。

4. echo; echo "Hit a key, then hit return."
5. read Keypress

7. case "$Keypress" in
8. [[:lower:]]   ) echo "Lowercase letter";;
9. [[:upper:]]   ) echo "Uppercase letter";;
10. [0-9]         ) echo "Digit";;
11. *             ) echo "Punctuation, whitespace, or other";;
12. esac      #  字符范围可以用[方括号]表示,也可以用 POSIX 形式的[[双方括号]]表示。

14. # 在这个例子的第一个版本中,用来测试是小写还是大写字符使用的是 [a-z] 和 [A-Z]。
15. # 这在一些特定的语言环境和 Linux 发行版中不起效。
16. # POSIX 形式具有更好的兼容性。
17. # 感谢 Frank Wang 指出这一点。

19. # 练习:
20. # -----
21. # 这个脚本接受一个单字符然后结束。
22. # 修改脚本,使得其可以循环接受输入,并且检测键入的每一个字符,直到键入 "X" 为止。
23. # 提示:将所有东西包在 "while" 中。

25. exit 0

样例 11-26. 使用 case 创建菜单

1. #!/bin/bash

3. # 简易的通讯录数据库

5. clear # 清屏。

7. echo "          Contact List"
8. echo "          ------- ----"
9. echo "Choose one of the following persons:"
10. echo
11. echo "[E]vans, Roland"
12. echo "[J]ones, Mildred"
13. echo "[S]mith, Julie"
14. echo "[Z]ane, Morris"
15. echo

17. read person

19. case "$person" in
20. # 注意变量是被引用的。

22. "E" | "e" )
23. # 同时接受大小写的输入。
24. echo
25. echo "Roland Evans"
26. echo "4321 Flash Dr."
27. echo "Hardscrabble, CO 80753"
28. echo "(303) 734-9874"
29. echo "(303) 734-9892 fax"
30. echo "revans@zzy.net"
31. echo "Business partner & old friend"
32. ;;
33. # 注意用双分号结束这一个选项。

35. "J" | "j" )
36. echo
37. echo "Mildred Jones"
38. echo "249 E. 7th St., Apt. 19"
39. echo "New York, NY 10009"
40. echo "(212) 533-2814"
41. echo "(212) 533-9972 fax"
42. echo "milliej@loisaida.com"
43. echo "Ex-girlfriend"
44. echo "Birthday: Feb. 11"
45. ;;

47. # Smith 和 Zane 的信息稍后添加。

49. *         )
50. # 缺省设置。
51. # 空输入(直接键入回车)也是执行这一部分。
52. echo
53. echo "Not yet in database."
54. ;;

56. esac

58. echo

60. # 练习:
61. # -----
62. # 修改脚本,使得其可以循环接受多次输入而不是只显示一个地址后终止脚本。

64. exit 0

你可以用 case 来检测命令行参数。

1. #!/bin/bash

3. case "$1" in
4. "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
5. # 没有命令行参数,或者第一个参数为空。
6. # 注意 ${0##*/} 是参数替换 ${var##pattern} 的一种形式。
7. # 最后的结果是 $0.

9. -*) FILENAME=./$1;; #  如果传入的参数以短横线开头,那么将其替换为 ./$1
10. #+ 以避免后续的命令将其解释为一个选项。

12. * ) FILENAME=$1;;   # 否则赋值为 $1。
13. esac

下面是一个更加直观的处理命令行参数的例子:

1. #!/bin/bash

3. while [ $# -gt 0 ]; do    # 遍历完所有参数
4. case "$1" in
5. -d|--debug)
6. # 检测是否是 "-d" 或者 "--debug"。
7. DEBUG=1
8. ;;
9. -c|--conf)
10. CONFFILE="$2"
11. shift
12. if [ ! -f $CONFFILE ]; then
13. echo "Error: Supplied file doesn't exist!"
14. exit $E_CONFFILE     # 找不到文件。
15. fi
16. ;;
17. esac
18. shift       # 检测下一个参数
19. done

21. # 摘自 Stefano Falsetto 的 "Log2Rot" 脚本中 "rottlog" 包的一部分。
22. # 已授权使用。

样例 11-27. 使用命令替换生成 case 变量

1. #!/bin/bash
2. # case-cmd.sh: 使用命令替换生成 "case" 变量。

4. case $( arch ) in   # $( arch ) 返回设备架构。
5. # 等价于 'uname -m"。
6. i386 ) echo "80386-based machine";;
7. i486 ) echo "80486-based machine";;
8. i586 ) echo "Pentium-based machine";;
9. i686 ) echo "Pentium2+-based machine";;
10. *    ) echo "Other type of machine";;
11. esac

13. exit 0

case 还可以用来做字符串模式匹配。

样例 11-28. 简单的字符串匹配

1. #!/bin/bash
2. # match-string.sh: 使用 'case' 结构进行简单的字符串匹配。

4. match_string ()
5. { # 字符串精确匹配。
6. MATCH=0
7. E_NOMATCH=90
8. PARAMS=2     # 需要2个参数。
9. E_BAD_PARAMS=91

11. [ $# -eq $PARAMS ] || return $E_BAD_PARAMS

13. case "$1" in
14. "$2") return $MATCH;;
15. *   ) return $E_NOMATCH;;
16. esac

18. }

21. a=one
22. b=two
23. c=three
24. d=two

26. match_string $a     # 参数个数不够
27. echo $?             # 91

29. match_string $a $b  # 匹配不到
30. echo $?             # 90

32. match_string $a $d  # 匹配成功
33. echo $?             # 0

36. exit 0

样例 11-29. 检查输入

1. #!/bin/bash
2. # isaplpha.sh: 使用 "case" 结构检查输入。

4. SUCCESS=0
5. FAILURE=1   #  以前是FAILURE=-1,
6. #+ 但现在 Bash 不允许返回负值。

8. isalpha ()  # 测试字符串的第一个字符是否是字母。
9. {
10. if [ -z "$1" ]                # 检测是否传入参数。
11. then
12. return $FAILURE
13. fi

15. case "$1" in
16. [a-zA-Z]*) return $SUCCESS;;  # 是否以字母形式开始?
17. *        ) return $FAILURE;;
18. esac
19. }             # 可以与 C 语言中的函数 "isalpha ()" 作比较。

22. isalpha2 ()   # 测试整个字符串是否都是字母。
23. {
24. [ $# -eq 1 ] || return $FAILURE

26. case $1 in
27. *[!a-zA-Z]*|"") return $FAILURE;;
28. *) return $SUCCESS;;
29. esac
30. }

32. isdigit ()    # 测试整个字符串是否都是数字。
33. {             # 换句话说,也就是测试是否是一个整型变量。
34. [ $# -eq 1 ] || return $FAILURE

36. case $1 in
37. *[!0-9]*|"") return $FAILURE;;
38. *) return $SUCCESS;;
39. esac
40. }

44. check_var ()  # 包装后的 isalpha ()。
45. {
46. if isalpha "$@"
47. then
48. echo "\"$*\" begins with an alpha character."
49. if isalpha2 "$@"
50. then        # 其实没必要检查第一个字符是不是字母。
51. echo "\"$*\" contains only alpha characters."
52. else
53. echo "\"$*\" contains at least one non-alpha character."
54. fi
55. else
56. echo "\"$*\" begins with a non-alpha character."
57. # 如果没有传入参数同样同样返回“存在非字母”。
58. fi

60. echo

62. }

64. digit_check ()  # 包装后的 isdigit ()。
65. {
66. if isdigit "$@"
67. then
68. echo "\"$*\" contains only digits [0 - 9]."
69. else
70. echo "\"$*\" has at least one non-digit character."
71. fi

73. echo

75. }

78. a=23skidoo
79. b=H3llo
80. c=-What?
81. d=What?
82. e=$(echo $b)   # 命令替换。
83. f=AbcDef
84. g=27234
85. h=27a34
86. i=27.34

88. check_var $a
89. check_var $b
90. check_var $c
91. check_var $d
92. check_var $e
93. check_var $f
94. check_var     # 如果不传入参数会发送什么?
95. #
96. digit_check $g
97. digit_check $h
98. digit_check $i

101. exit 0        # S.C. 改进了本脚本。

103. # 练习:
104. # -----
105. # 写一个函数 'isfloat ()' 来检测输入值是否是浮点数。
106. # 提示:可以参考函数 'isdigit ()',在其中加入检测合法的小数点即可。

select

select 结构是学习自 Korn Shell。其同样可以用来构建菜单。

1. select variable [in list]
2. do
3. command...
4. break
5. done

而效果则是终端会提示用户输入列表中的一个选项。注意,select 默认使用提示字串3(Prompt String 3,$PS3, 即#?),但同样可以被修改。

样例 11-30. 使用 select 创建菜单

1. #!/bin/bash

3. PS3='Choose your favorite vegetable: ' # 设置提示字串。
4. # 否则默认为 #?。

6. echo

8. select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
9. do
10. echo
11. echo "Your favorite veggie is $vegetable."
12. echo "Yuck!"
13. echo
14. break  # 如果没有 'break' 会发生什么?
15. done

17. exit

19. # 练习:
20. # -----
21. # 修改脚本,使得其可以接受其他输入而不是 "select" 语句中所指定的。
22. # 例如,如果用户输入 "peas,",那么脚本会通知用户 "Sorry. That is not on the menu."

如果 in list 被省略,那么 select 将会使用传入脚本的命令行参数($@)或者传入函数的参数作为 list。

可以与 for variable [in list] 中 in list 被省略的情况做比较。

样例 11-31. 在函数中使用 select 创建菜单

1. #!/bin/bash

3. PS3='Choose your favorite vegetable: '

5. echo

7. choice_of()
8. {
9. select vegetable
10. # [in list] 被省略,因此 'select' 将会使用传入函数的参数作为 list。
11. do
12. echo
13. echo "Your favorite veggie is $vegetable."
14. echo "Yuck!"
15. echo
16. break
17. done
18. }

20. choice_of beans rice carrorts radishes rutabaga spinach
21. #         $1    $2   $3      $4       $5       $6
22. #         传入了函数 choice_of()

24. exit 0

还可以参照 样例37-3。

[^1]: 在写匹配行的时候,可以在左边加上左括号 (,使整个结构看起来更加优雅。

1. case $( arch ) in   # $( arch ) 返回设备架构。
     ( i386 ) echo “80386-based machine”;;
   # ^      ^
     ( i486 ) echo “80486-based machine”;;
     ( i586 ) echo “Pentium-based machine”;;
     ( i686 ) echo “Pentium2+-based machine”;;
     (    * ) echo “Other type of machine”;;
   esac