10.4. 测试与分支

caseselect结构在技术上说不是循环,因为它们并不对可执行的代码块进行迭代.但是和循环相似的是,它们也依靠在代码块的顶部或底部的条件判断来决定程序的分支.

在代码块中控制程序分支

case (in) / esac

在shell中的case同C/C++中的switch结构是相同的.它允许通过判断来选择代码块中多条路径中的一条.它的作用和多个if/then/else语句相同,是它们的简化结构,特别适用于创建目录.

case "$variable" in

?"$condition1" )
?command...
?;;

?"$condition2" )
?command...
?;;

esac

Note

  • 对变量使用""并不是强制的,因为不会发生单词分离.

  • 每句测试行,都以右小括号)结尾.

  • 每个条件块都以两个分号结尾;;.

  • case块的结束以esac(case的反向拼写)结尾.


例子 10-24. 使用case

   1 #!/bin/bash
   2 # 测试字符串范围
   3 
   4 echo; echo "Hit a key, then hit return."
   5 read Keypress
   6 
   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      #  允许字符串的范围出现在[]中,
  13           #+ 或者POSIX风格的[[中.
  14 
  15 #  在这个例子的第一个版本中,
  16 #+ 测试大写和小写字符串使用的是
  17 #+ [a-z] 和 [A-Z].
  18 #  这种用法将不会在某些特定的场合或Linux发行版中正常工作.
  19 #  POSIX 风格更具可移植性.
  20 #  感谢Frank Wang 指出这点.
  21 
  22 #  练习:
  23 #  --------
  24 #  就像这个脚本所表现的,它只允许单次的按键,然后就结束了.
  25 #  修改这个脚本,让它能够接受重复输入,
  26 #+ 报告每个按键,并且只有在"X"被键入时才结束.
  27 #  暗示: 将这些代码都用"while"循环圈起来.
  28 
  29 exit 0


例子 10-25. 使用case来创建菜单

   1 #!/bin/bash
   2 
   3 # 未经处理的地址资料
   4 
   5 clear # 清屏.
   6 
   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
  16 
  17 read person
  18 
  19 case "$person" in
  20 # 注意,变量是被引用的.
  21 
  22   "E" | "e" )
  23   # 接受大写或小写输入.
  24   echo
  25   echo "Roland Evans"
  26   echo "4321 Floppy Dr."
  27   echo "Hardscrabble, CO 80753"
  28   echo "(303) 734-9874"
  29   echo "(303) 734-9892 fax"
  30   echo "[email protected]"
  31   echo "Business partner & old friend"
  32   ;;
  33 # 注意,在每个选项后边都需要以;;结尾.
  34 
  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 "[email protected]"
  43   echo "Ex-girlfriend"
  44   echo "Birthday: Feb. 11"
  45   ;;
  46 
  47 # 后边的Smith和Zane的信息在这里就省略了.
  48 
  49           * )
  50    # 默认选项.
  51    # 空输入(敲RETURN).
  52    echo
  53    echo "Not yet in database."
  54   ;;
  55 
  56 esac
  57 
  58 echo
  59 
  60 #  练习:
  61 #  --------
  62 #  修改这个脚本,让它能够接受多输入,
  63 #+ 并且能够显示多个地址.
  64 
  65 exit 0

一个case的特殊用法,用来测试命令行参数.
   1 #! /bin/bash
   2 
   3 case "$1" in
   4 "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;  # 没有命令行参数,
   5                                                         # 或者第一个参数为空.
   6 # 注意:${0##*/} 是${var##pattern} 这种模式的替换. 得到的结果是$0.
   7 
   8 -*) FILENAME=./$1;;   #  如果传递进来的文件名参数($1)以一个破折号开头,
   9                       #+ 那么用./$1来代替
  10                       #+ 这样后边的命令将不会把它作为一个选项来解释.
  11 
  12 * ) FILENAME=$1;;     # 否则, $1.
  13 esac

这是一个更容易懂的命令行参数处理的一个例子:
   1 #! /bin/bash
   2 
   3 
   4 while [ $# -gt 0 ]; do    # 直到你用完所有的参数...
   5   case "$1" in
   6     -d|--debug)
   7               # 是"-d" 或 "--debug" 参数吗?
   8               DEBUG=1
   9               ;;
  10     -c|--conf)
  11               CONFFILE="$2"
  12               shift
  13               if [ ! -f $CONFFILE ]; then
  14                 echo "Error: Supplied file doesn't exist!"
  15                 exit $E_CONFFILE     # 文件没发现错误.
  16               fi
  17               ;;
  18   esac
  19   shift       # 检查剩下的参数.
  20 done
  21 
  22 #  来自Stefano Falsetto的 "Log2Rot" 脚本,
  23 #+ 他的"rottlog" 包的一部分.
  24 #  已得到使用许可


例子 10-26. 使用命令替换来产生case变量

   1 #!/bin/bash
   2 # case-cmd.sh: 使用命令替换来产生"case"变量
   3 
   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
  12 
  13 exit 0

case结构也可以过滤通配扩展(globbing)模式的字符串.


例子 10-27. 简单字符串匹配

   1 #!/bin/bash
   2 # match-string.sh: 简单字符串匹配
   3 
   4 match_string ()
   5 {
   6   MATCH=0
   7   NOMATCH=90
   8   PARAMS=2     # 函数需要2个参数.
   9   BAD_PARAMS=91
  10 
  11   [ $# -eq $PARAMS ] || return $BAD_PARAMS
  12 
  13   case "$1" in
  14   "$2") return $MATCH;;
  15   *   ) return $NOMATCH;;
  16   esac
  17 
  18 }  
  19 
  20 
  21 a=one
  22 b=two
  23 c=three
  24 d=two
  25 
  26 
  27 match_string $a     # 参数个数错误.
  28 echo $?             # 91
  29 
  30 match_string $a $b  # 不匹配
  31 echo $?             # 90
  32 
  33 match_string $b $d  # 匹配
  34 echo $?             # 0
  35 
  36 
  37 exit 0		    


例子 10-28. 检查是否是字母输入

   1 #!/bin/bash
   2 # isalpha.sh: 使用"case"结构来过滤字符串.
   3 
   4 SUCCESS=0
   5 FAILURE=-1
   6 
   7 isalpha ()  # 检查输入的*第一个字符*是不是字母表上的字符.
   8 {
   9 if [ -z "$1" ]                # 没有参数传进来?
  10 then
  11   return $FAILURE
  12 fi
  13 
  14 case "$1" in
  15 [a-zA-Z]*) return $SUCCESS;;  # 以一个字母开头?
  16 *        ) return $FAILURE;;
  17 esac
  18 }             # 同C语言的"isalpha()"函数相比较.
  19 
  20 
  21 isalpha2 ()   # 测试是否*整个字符串*为字母表字符.
  22 {
  23   [ $# -eq 1 ] || return $FAILURE
  24 
  25   case $1 in
  26   *[!a-zA-Z]*|"") return $FAILURE;;
  27                *) return $SUCCESS;;
  28   esac
  29 }
  30 
  31 isdigit ()    # 测试是否*整个字符串*都是数字.
  32 {             # 换句话说就是测试是否是整数变量.
  33   [ $# -eq 1 ] || return $FAILURE
  34 
  35   case $1 in
  36   *[!0-9]*|"") return $FAILURE;;
  37             *) return $SUCCESS;;
  38   esac
  39 }
  40 
  41 
  42 
  43 check_var ()  # 测试 isalpha ().
  44 {
  45 if isalpha "[email protected]"
  46 then
  47   echo "\"$*\" begins with an alpha character."
  48   if isalpha2 "[email protected]"
  49   then        # 不需要测试第一个字符是否是non-alpha.
  50     echo "\"$*\" contains only alpha characters."
  51   else
  52     echo "\"$*\" contains at least one non-alpha character."
  53   fi  
  54 else
  55   echo "\"$*\" begins with a non-alpha character."
  56               # 如果没有参数传递进来,也是"non-alpha".
  57 fi
  58 
  59 echo
  60 
  61 }
  62 
  63 digit_check ()  # 测试 isdigit ().
  64 {
  65 if isdigit "[email protected]"
  66 then
  67   echo "\"$*\" contains only digits [0 - 9]."
  68 else
  69   echo "\"$*\" has at least one non-digit character."
  70 fi
  71 
  72 echo
  73 
  74 }
  75 
  76 a=23skidoo
  77 b=H3llo
  78 c=-What?
  79 d=What?
  80 e=`echo $b`   #命令替换.
  81 f=AbcDef
  82 g=27234
  83 h=27a34
  84 i=27.34
  85 
  86 check_var $a
  87 check_var $b
  88 check_var $c
  89 check_var $d
  90 check_var $e
  91 check_var $f
  92 check_var     # 没有参数传进来,将发生什么?
  93 #
  94 digit_check $g
  95 digit_check $h
  96 digit_check $i
  97 
  98 
  99 exit 0        # S.C改进过这个脚本.
 100 
 101 # 练习:
 102 # --------
 103 #  编写一个 'isfloat ()'函数来测试浮点数.
 104 #  暗示: 这个函数基本上与'isdigit ()'一样,
 105 #+ 但是要添加一部分小数点的处理.

select

select结构是建立菜单的另一种工具,这种结构是从ksh中引入的.

select variable [in list]
do
?command...
?break
done

提示用户输入选择的内容(比如放在变量列表中).注意:select命令使用PS3提示符[默认为(#? )],但是可以修改PS3.


例子 10-29. 用select来创建菜单

   1 #!/bin/bash
   2 
   3 PS3='Choose your favorite vegetable: ' # 设置提示符字串.
   4 
   5 echo
   6 
   7 select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
   8 do
   9   echo
  10   echo "Your favorite veggie is $vegetable."
  11   echo "Yuck!"
  12   echo
  13   break  # 如果这里没有'break'会发生什么?
  14 done
  15 
  16 exit 0

如果忽略了in list列表,那么select命令将使用传递到脚本的命令行参数([email protected]),或者是函数参数(当select是在函数中时).

与忽略in list时的for语句相比较:

for variable [in list]


例子 10-30. 用函数中select结构来创建菜单

   1 #!/bin/bash
   2 
   3 PS3='Choose your favorite vegetable: '
   4 
   5 echo
   6 
   7 choice_of()
   8 {
   9 select vegetable
  10 # [in list] 被忽略, 所以'select'用传递给函数的参数.
  11 do
  12   echo
  13   echo "Your favorite veggie is $vegetable."
  14   echo "Yuck!"
  15   echo
  16   break
  17 done
  18 }
  19 
  20 choice_of beans rice carrots radishes tomatoes spinach
  21 #         $1    $2   $3      $4       $5       $6
  22 #         传递给choice_of() 函数的参数
  23 
  24 exit 0

参见例子 34-3.