# 11.4 測試與分支
`case` 和 `select` 結構并不屬于循環結構,因為它們并沒有反復執行代碼塊。但是和循環結構相似的是,它們會根據代碼塊頂部或尾部的條件控制程序流。
下面介紹兩種在代碼塊中控制程序流的方法:
### `case (in)` / `esac`
在 shell 腳本中,`case` 模擬了 C/C++ 語言中的 `switch`,可以根據條件跳轉到其中一個分支。其相當于簡寫版的 `if/then/else` 語句。很適合用來創建菜單選項喲!
```bash
case "$variable" in
"$condition1" )
command...
;;
"$condition2" )
command...
;;
esac
```
> 
>
> - 對變量進行引用不是必須的,因為在這里不會進行字符分割。
>
> - 條件測試語句必須以右括號 ) 結束。[^1]
>
> - 每一段代碼塊都必須以雙分號 ;; 結束。
>
> - 如果測試條件為真,其對應的代碼塊將被執行,而后整個 `case` 代碼段結束執行。
>
> - `case` 代碼段必須以 `esac` 結束(倒著拼寫case)。
樣例 11-25. 如何使用 `case`
```bash
#!/bin/bash
# 測試字符的種類。
echo; echo "Hit a key, then hit return."
read Keypress
case "$Keypress" in
[[:lower:]] ) echo "Lowercase letter";;
[[:upper:]] ) echo "Uppercase letter";;
[0-9] ) echo "Digit";;
* ) echo "Punctuation, whitespace, or other";;
esac # 字符范圍可以用[方括號]表示,也可以用 POSIX 形式的[[雙方括號]]表示。
# 在這個例子的第一個版本中,用來測試是小寫還是大寫字符使用的是 [a-z] 和 [A-Z]。
# 這在一些特定的語言環境和 Linux 發行版中不起效。
# POSIX 形式具有更好的兼容性。
# 感謝 Frank Wang 指出這一點。
# 練習:
# -----
# 這個腳本接受一個單字符然后結束。
# 修改腳本,使得其可以循環接受輸入,并且檢測鍵入的每一個字符,直到鍵入 "X" 為止。
# 提示:將所有東西包在 "while" 中。
exit 0
```
樣例 11-26. 使用 `case` 創建菜單
```bash
#!/bin/bash
# 簡易的通訊錄數據庫
clear # 清屏。
echo " Contact List"
echo " ------- ----"
echo "Choose one of the following persons:"
echo
echo "[E]vans, Roland"
echo "[J]ones, Mildred"
echo "[S]mith, Julie"
echo "[Z]ane, Morris"
echo
read person
case "$person" in
# 注意變量是被引用的。
"E" | "e" )
# 同時接受大小寫的輸入。
echo
echo "Roland Evans"
echo "4321 Flash Dr."
echo "Hardscrabble, CO 80753"
echo "(303) 734-9874"
echo "(303) 734-9892 fax"
echo "revans@zzy.net"
echo "Business partner & old friend"
;;
# 注意用雙分號結束這一個選項。
"J" | "j" )
echo
echo "Mildred Jones"
echo "249 E. 7th St., Apt. 19"
echo "New York, NY 10009"
echo "(212) 533-2814"
echo "(212) 533-9972 fax"
echo "milliej@loisaida.com"
echo "Ex-girlfriend"
echo "Birthday: Feb. 11"
;;
# Smith 和 Zane 的信息稍后添加。
* )
# 缺省設置。
# 空輸入(直接鍵入回車)也是執行這一部分。
echo
echo "Not yet in database."
;;
esac
echo
# 練習:
# -----
# 修改腳本,使得其可以循環接受多次輸入而不是只顯示一個地址后終止腳本。
exit 0
```
你可以用 `case` 來檢測命令行參數。
```bash
#!/bin/bash
case "$1" in
"") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
# 沒有命令行參數,或者第一個參數為空。
# 注意 ${0##*/} 是參數替換 ${var##pattern} 的一種形式。
# 最后的結果是 $0.
-*) FILENAME=./$1;; # 如果傳入的參數以短橫線開頭,那么將其替換為 ./$1
#+ 以避免后續的命令將其解釋為一個選項。
* ) FILENAME=$1;; # 否則賦值為 $1。
esac
```
下面是一個更加直觀的處理命令行參數的例子:
```bash
#!/bin/bash
while [ $# -gt 0 ]; do # 遍歷完所有參數
case "$1" in
-d|--debug)
# 檢測是否是 "-d" 或者 "--debug"。
DEBUG=1
;;
-c|--conf)
CONFFILE="$2"
shift
if [ ! -f $CONFFILE ]; then
echo "Error: Supplied file doesn't exist!"
exit $E_CONFFILE # 找不到文件。
fi
;;
esac
shift # 檢測下一個參數
done
# 摘自 Stefano Falsetto 的 "Log2Rot" 腳本中 "rottlog" 包的一部分。
# 已授權使用。
```
樣例 11-27. 使用命令替換生成 `case` 變量
```bash
#!/bin/bash
# case-cmd.sh: 使用命令替換生成 "case" 變量。
case $( arch ) in # $( arch ) 返回設備架構。
# 等價于 'uname -m"。
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
exit 0
```
`case` 還可以用來做字符串模式匹配。
樣例 11-28. 簡單的字符串匹配
```bash
#!/bin/bash
# match-string.sh: 使用 'case' 結構進行簡單的字符串匹配。
match_string ()
{ # 字符串精確匹配。
MATCH=0
E_NOMATCH=90
PARAMS=2 # 需要2個參數。
E_BAD_PARAMS=91
[ $# -eq $PARAMS ] || return $E_BAD_PARAMS
case "$1" in
"$2") return $MATCH;;
* ) return $E_NOMATCH;;
esac
}
a=one
b=two
c=three
d=two
match_string $a # 參數個數不夠
echo $? # 91
match_string $a $b # 匹配不到
echo $? # 90
match_string $a $d # 匹配成功
echo $? # 0
exit 0
```
樣例 11-29. 檢查輸入
```bash
#!/bin/bash
# isaplpha.sh: 使用 "case" 結構檢查輸入。
SUCCESS=0
FAILURE=1 # 以前是FAILURE=-1,
#+ 但現在 Bash 不允許返回負值。
isalpha () # 測試字符串的第一個字符是否是字母。
{
if [ -z "$1" ] # 檢測是否傳入參數。
then
return $FAILURE
fi
case "$1" in
[a-zA-Z]*) return $SUCCESS;; # 是否以字母形式開始?
* ) return $FAILURE;;
esac
} # 可以與 C 語言中的函數 "isalpha ()" 作比較。
isalpha2 () # 測試整個字符串是否都是字母。
{
[ $# -eq 1 ] || return $FAILURE
case $1 in
*[!a-zA-Z]*|"") return $FAILURE;;
*) return $SUCCESS;;
esac
}
isdigit () # 測試整個字符串是否都是數字。
{ # 換句話說,也就是測試是否是一個整型變量。
[ $# -eq 1 ] || return $FAILURE
case $1 in
*[!0-9]*|"") return $FAILURE;;
*) return $SUCCESS;;
esac
}
check_var () # 包裝后的 isalpha ()。
{
if isalpha "$@"
then
echo "\"$*\" begins with an alpha character."
if isalpha2 "$@"
then # 其實沒必要檢查第一個字符是不是字母。
echo "\"$*\" contains only alpha characters."
else
echo "\"$*\" contains at least one non-alpha character."
fi
else
echo "\"$*\" begins with a non-alpha character."
# 如果沒有傳入參數同樣同樣返回“存在非字母”。
fi
echo
}
digit_check () # 包裝后的 isdigit ()。
{
if isdigit "$@"
then
echo "\"$*\" contains only digits [0 - 9]."
else
echo "\"$*\" has at least one non-digit character."
fi
echo
}
a=23skidoo
b=H3llo
c=-What?
d=What?
e=$(echo $b) # 命令替換。
f=AbcDef
g=27234
h=27a34
i=27.34
check_var $a
check_var $b
check_var $c
check_var $d
check_var $e
check_var $f
check_var # 如果不傳入參數會發送什么?
#
digit_check $g
digit_check $h
digit_check $i
exit 0 # S.C. 改進了本腳本。
# 練習:
# -----
# 寫一個函數 'isfloat ()' 來檢測輸入值是否是浮點數。
# 提示:可以參考函數 'isdigit ()',在其中加入檢測合法的小數點即可。
```
### `select`
`select` 結構是學習自 Korn Shell。其同樣可以用來構建菜單。
```bash
select variable [in list]
do
command...
break
done
```
而效果則是終端會提示用戶輸入列表中的一個選項。注意,`select` 默認使用提示字串3(Prompt String 3,`$PS3`, 即#?),但同樣可以被修改。
樣例 11-30. 使用 `select` 創建菜單
```bash
#!/bin/bash
PS3='Choose your favorite vegetable: ' # 設置提示字串。
# 否則默認為 #?。
echo
select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
do
echo
echo "Your favorite veggie is $vegetable."
echo "Yuck!"
echo
break # 如果沒有 'break' 會發生什么?
done
exit
# 練習:
# -----
# 修改腳本,使得其可以接受其他輸入而不是 "select" 語句中所指定的。
# 例如,如果用戶輸入 "peas,",那么腳本會通知用戶 "Sorry. That is not on the menu."
```
如果 *in list* 被省略,那么 `select` 將會使用傳入腳本的命令行參數(`$@`)或者傳入函數的參數作為 *list*。
可以與 `for variable [in list]` 中 *in list* 被省略的情況做比較。
樣例 11-31. 在函數中使用 `select` 創建菜單
```bash
#!/bin/bash
PS3='Choose your favorite vegetable: '
echo
choice_of()
{
select vegetable
# [in list] 被省略,因此 'select' 將會使用傳入函數的參數作為 list。
do
echo
echo "Your favorite veggie is $vegetable."
echo "Yuck!"
echo
break
done
}
choice_of beans rice carrorts radishes rutabaga spinach
# $1 $2 $3 $4 $5 $6
# 傳入了函數 choice_of()
exit 0
```
還可以參照 [樣例37-3](http://tldp.org/LDP/abs/html/bashver2.html#RESISTOR)。
[^1]: 在寫匹配行的時候,可以在左邊加上左括號 (,使整個結構看起來更加優雅。<pre>case $( arch ) in # $( arch ) 返回設備架構。<br> ( i386 ) echo "80386-based machine";;<br># ^ ^<br> ( i486 ) echo "80486-based machine";;<br> ( i586 ) echo "Pentium-based machine";;<br> ( i686 ) echo "Pentium2+-based machine";;<br> ( * ) echo "Other type of machine";;<br>esac</pre>
- 第一部分 初見shell
- 1. 為什么使用shell編程
- 2. 和Sha-Bang(#!)一起出發
- 2.1 調用一個腳本
- 2.2 牛刀小試
- 第二部分 shell基礎
- 3. 特殊字符
- 4. 變量與參數
- 4.1 變量替換
- 4.2 變量賦值
- 4.3 Bash弱類型變量
- 4.4 特殊變量類型
- 5. 引用
- 5.1 引用變量
- 5.2 轉義
- 6. 退出與退出狀態
- 7. 測試
- 7.1 測試結構
- 7.2 文件測試操作
- 7.3 其他比較操作
- 7.4 嵌套 if/then 條件測試
- 7.5 牛刀小試
- 8. 運算符相關話題
- 8.1 運算符
- 8.2 數字常量
- 8.3 雙圓括號結構
- 8.4 運算符優先級
- 第三部分 shell進階
- 10. 變量處理
- 10.1 字符串處理
- 10.1.1 使用 awk 處理字符串
- 10.1.2 參考資料
- 10.2 參數替換
- 11. 循環與分支
- 11.1 循環
- 11.2 嵌套循環
- 11.3 循環控制
- 11.4 測試與分支
- 12. 命令替換
- 13. 算術擴展
- 14. 休息時間
- 第五部分 進階話題
- 19. 嵌入文檔
- 20. I/O 重定向
- 20.1 使用 exec
- 20.2 重定向代碼塊
- 20.3 應用程序
- 22. 限制模式的Shell
- 23. 進程替換
- 26. 列表結構
- 25. 別名