11.4 测试与分支

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

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

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. # 测试字符的种类。
  3. echo; echo "Hit a key, then hit return."
  4. read Keypress
  5. case "$Keypress" in
  6. [[:lower:]] ) echo "Lowercase letter";;
  7. [[:upper:]] ) echo "Uppercase letter";;
  8. [0-9] ) echo "Digit";;
  9. * ) echo "Punctuation, whitespace, or other";;
  10. esac # 字符范围可以用[方括号]表示,也可以用 POSIX 形式的[[双方括号]]表示。
  11. # 在这个例子的第一个版本中,用来测试是小写还是大写字符使用的是 [a-z] 和 [A-Z]。
  12. # 这在一些特定的语言环境和 Linux 发行版中不起效。
  13. # POSIX 形式具有更好的兼容性。
  14. # 感谢 Frank Wang 指出这一点。
  15. # 练习:
  16. # -----
  17. # 这个脚本接受一个单字符然后结束。
  18. # 修改脚本,使得其可以循环接受输入,并且检测键入的每一个字符,直到键入 "X" 为止。
  19. # 提示:将所有东西包在 "while" 中。
  20. exit 0

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

  1. #!/bin/bash
  2. # 简易的通讯录数据库
  3. clear # 清屏。
  4. echo " Contact List"
  5. echo " ------- ----"
  6. echo "Choose one of the following persons:"
  7. echo
  8. echo "[E]vans, Roland"
  9. echo "[J]ones, Mildred"
  10. echo "[S]mith, Julie"
  11. echo "[Z]ane, Morris"
  12. echo
  13. read person
  14. case "$person" in
  15. # 注意变量是被引用的。
  16. "E" | "e" )
  17. # 同时接受大小写的输入。
  18. echo
  19. echo "Roland Evans"
  20. echo "4321 Flash Dr."
  21. echo "Hardscrabble, CO 80753"
  22. echo "(303) 734-9874"
  23. echo "(303) 734-9892 fax"
  24. echo "revans@zzy.net"
  25. echo "Business partner & old friend"
  26. ;;
  27. # 注意用双分号结束这一个选项。
  28. "J" | "j" )
  29. echo
  30. echo "Mildred Jones"
  31. echo "249 E. 7th St., Apt. 19"
  32. echo "New York, NY 10009"
  33. echo "(212) 533-2814"
  34. echo "(212) 533-9972 fax"
  35. echo "milliej@loisaida.com"
  36. echo "Ex-girlfriend"
  37. echo "Birthday: Feb. 11"
  38. ;;
  39. # Smith 和 Zane 的信息稍后添加。
  40. * )
  41. # 缺省设置。
  42. # 空输入(直接键入回车)也是执行这一部分。
  43. echo
  44. echo "Not yet in database."
  45. ;;
  46. esac
  47. echo
  48. # 练习:
  49. # -----
  50. # 修改脚本,使得其可以循环接受多次输入而不是只显示一个地址后终止脚本。
  51. exit 0

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

  1. #!/bin/bash
  2. case "$1" in
  3. "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
  4. # 没有命令行参数,或者第一个参数为空。
  5. # 注意 ${0##*/} 是参数替换 ${var##pattern} 的一种形式。
  6. # 最后的结果是 $0.
  7. -*) FILENAME=./$1;; # 如果传入的参数以短横线开头,那么将其替换为 ./$1
  8. #+ 以避免后续的命令将其解释为一个选项。
  9. * ) FILENAME=$1;; # 否则赋值为 $1。
  10. esac

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

  1. #!/bin/bash
  2. while [ $# -gt 0 ]; do # 遍历完所有参数
  3. case "$1" in
  4. -d|--debug)
  5. # 检测是否是 "-d" 或者 "--debug"。
  6. DEBUG=1
  7. ;;
  8. -c|--conf)
  9. CONFFILE="$2"
  10. shift
  11. if [ ! -f $CONFFILE ]; then
  12. echo "Error: Supplied file doesn't exist!"
  13. exit $E_CONFFILE # 找不到文件。
  14. fi
  15. ;;
  16. esac
  17. shift # 检测下一个参数
  18. done
  19. # 摘自 Stefano Falsetto 的 "Log2Rot" 脚本中 "rottlog" 包的一部分。
  20. # 已授权使用。

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

  1. #!/bin/bash
  2. # case-cmd.sh: 使用命令替换生成 "case" 变量。
  3. case $( arch ) in # $( arch ) 返回设备架构。
  4. # 等价于 'uname -m"。
  5. i386 ) echo "80386-based machine";;
  6. i486 ) echo "80486-based machine";;
  7. i586 ) echo "Pentium-based machine";;
  8. i686 ) echo "Pentium2+-based machine";;
  9. * ) echo "Other type of machine";;
  10. esac
  11. exit 0

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

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

  1. #!/bin/bash
  2. # match-string.sh: 使用 'case' 结构进行简单的字符串匹配。
  3. match_string ()
  4. { # 字符串精确匹配。
  5. MATCH=0
  6. E_NOMATCH=90
  7. PARAMS=2 # 需要2个参数。
  8. E_BAD_PARAMS=91
  9. [ $# -eq $PARAMS ] || return $E_BAD_PARAMS
  10. case "$1" in
  11. "$2") return $MATCH;;
  12. * ) return $E_NOMATCH;;
  13. esac
  14. }
  15. a=one
  16. b=two
  17. c=three
  18. d=two
  19. match_string $a # 参数个数不够
  20. echo $? # 91
  21. match_string $a $b # 匹配不到
  22. echo $? # 90
  23. match_string $a $d # 匹配成功
  24. echo $? # 0
  25. exit 0

样例 11-29. 检查输入

  1. #!/bin/bash
  2. # isaplpha.sh: 使用 "case" 结构检查输入。
  3. SUCCESS=0
  4. FAILURE=1 # 以前是FAILURE=-1,
  5. #+ 但现在 Bash 不允许返回负值。
  6. isalpha () # 测试字符串的第一个字符是否是字母。
  7. {
  8. if [ -z "$1" ] # 检测是否传入参数。
  9. then
  10. return $FAILURE
  11. fi
  12. case "$1" in
  13. [a-zA-Z]*) return $SUCCESS;; # 是否以字母形式开始?
  14. * ) return $FAILURE;;
  15. esac
  16. } # 可以与 C 语言中的函数 "isalpha ()" 作比较。
  17. isalpha2 () # 测试整个字符串是否都是字母。
  18. {
  19. [ $# -eq 1 ] || return $FAILURE
  20. case $1 in
  21. *[!a-zA-Z]*|"") return $FAILURE;;
  22. *) return $SUCCESS;;
  23. esac
  24. }
  25. isdigit () # 测试整个字符串是否都是数字。
  26. { # 换句话说,也就是测试是否是一个整型变量。
  27. [ $# -eq 1 ] || return $FAILURE
  28. case $1 in
  29. *[!0-9]*|"") return $FAILURE;;
  30. *) return $SUCCESS;;
  31. esac
  32. }
  33. check_var () # 包装后的 isalpha ()。
  34. {
  35. if isalpha "$@"
  36. then
  37. echo "\"$*\" begins with an alpha character."
  38. if isalpha2 "$@"
  39. then # 其实没必要检查第一个字符是不是字母。
  40. echo "\"$*\" contains only alpha characters."
  41. else
  42. echo "\"$*\" contains at least one non-alpha character."
  43. fi
  44. else
  45. echo "\"$*\" begins with a non-alpha character."
  46. # 如果没有传入参数同样同样返回“存在非字母”。
  47. fi
  48. echo
  49. }
  50. digit_check () # 包装后的 isdigit ()。
  51. {
  52. if isdigit "$@"
  53. then
  54. echo "\"$*\" contains only digits [0 - 9]."
  55. else
  56. echo "\"$*\" has at least one non-digit character."
  57. fi
  58. echo
  59. }
  60. a=23skidoo
  61. b=H3llo
  62. c=-What?
  63. d=What?
  64. e=$(echo $b) # 命令替换。
  65. f=AbcDef
  66. g=27234
  67. h=27a34
  68. i=27.34
  69. check_var $a
  70. check_var $b
  71. check_var $c
  72. check_var $d
  73. check_var $e
  74. check_var $f
  75. check_var # 如果不传入参数会发送什么?
  76. #
  77. digit_check $g
  78. digit_check $h
  79. digit_check $i
  80. exit 0 # S.C. 改进了本脚本。
  81. # 练习:
  82. # -----
  83. # 写一个函数 'isfloat ()' 来检测输入值是否是浮点数。
  84. # 提示:可以参考函数 '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
  2. PS3='Choose your favorite vegetable: ' # 设置提示字串。
  3. # 否则默认为 #?。
  4. echo
  5. select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
  6. do
  7. echo
  8. echo "Your favorite veggie is $vegetable."
  9. echo "Yuck!"
  10. echo
  11. break # 如果没有 'break' 会发生什么?
  12. done
  13. exit
  14. # 练习:
  15. # -----
  16. # 修改脚本,使得其可以接受其他输入而不是 "select" 语句中所指定的。
  17. # 例如,如果用户输入 "peas,",那么脚本会通知用户 "Sorry. That is not on the menu."

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

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

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

  1. #!/bin/bash
  2. PS3='Choose your favorite vegetable: '
  3. echo
  4. choice_of()
  5. {
  6. select vegetable
  7. # [in list] 被省略,因此 'select' 将会使用传入函数的参数作为 list。
  8. do
  9. echo
  10. echo "Your favorite veggie is $vegetable."
  11. echo "Yuck!"
  12. echo
  13. break
  14. done
  15. }
  16. choice_of beans rice carrorts radishes rutabaga spinach
  17. # $1 $2 $3 $4 $5 $6
  18. # 传入了函数 choice_of()
  19. 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