一、Shell 腳本語法#
1.1 條件測試:test [#
命令test
或[
可以測試一個條件是否成立,如果測試結果為真,則該命令的 Exit Status 為 0,如果測試結果為假,則命令的 Exit Status 為 1(注意與 C 語言的邏輯表示正好相反)。
例如測試兩個數的大小關係:
$ VAR=2
$ test $VAR -gt 1
$ echo $?
0
$ test $VAR -gt 3
$ echo $?
1
$ [ $VAR -gt 3 ]
$ echo $?
1
雖然看起來很奇怪,但左方括號 [
確實是一個命令的名字,傳給命令的各參數之間應該用空格隔開,比如,$VAR
、-gt
、3
、]
是[
命令的四個參數,它們之間必須用空格隔開。命令 test
或 [
的參數形式是相同的,只不過 test
命令不需要 ]
參數。以 [
命令為例,常見的測試命令如下表所示:
測試命令
命令 | 描述 |
---|---|
[ -d DIR ] | 如果DIR 存在並且是一個目錄則為真 |
[ -f FILE ] | 如果FILE 存在且是一個普通文件則為真 |
[ -z STRING ] | 如果STRING 的長度為零則為真 |
[ -n STRING ] | 如果STRING 的長度非零則為真 |
[ STRING1 = STRING2 ] | 如果兩個字符串相同則為真 |
[ STRING1 != STRING2 ] | 如果字符串不相同則為真 |
[ ARG1 OP ARG2 ] | ARG1 和ARG2 應該是整數或者取值為整數的變量,OP 是-eq (等於)-ne (不等於)-lt (小於)-le (小於等於)-gt (大於)-ge (大於等於)之中的一個 |
帶與、或、非的測試命令
命令 | 描述 |
---|---|
[ ! EXPR ] | EXPR 可以是上表中的任意一種測試條件,! 表示邏輯反 |
[ EXPR1 -a EXPR2 ] | EXPR1 和EXPR2 可以是上表中的任意一種測試條件,-a 表示邏輯與 |
[ EXPR1 -o EXPR2 ] | EXPR1 和EXPR2 可以是上表中的任意一種測試條件,-o 表示邏輯或 |
例如:
$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0
注意,如果上例中的 $VAR
變量事先沒有定義,則被 Shell 展開為空字符串,會造成測試條件的語法錯誤(展開為 [ -d Desktop -a = 'abc' ]
),== 作為一種好的 Shell 編程習慣,應該總是把變量取值放在雙引號之中 ==(展開為[ -d Desktop -a "" = 'abc' ]
):
$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1
1.2 if/then/elif/else/fi#
和 C 語言類似,在 Shell 中用 if
、then
、elif
、else
、fi
這幾條命令實現分支控制。這種流程控制語句本質上也是由若干條 Shell 命令組成的,例如先前講過的
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
其實是三條命令,if [ -f ~/.bashrc ]
是第一條,then . ~/.bashrc
是第二條,fi
是第三條。如果兩條命令寫在同一行則需要用 ;
號隔開,一行只寫一條命令就不需要寫;
號了,另外,then
後面有換行,但這條命令沒寫完,Shell 會自動續行,把下一行接在then
後面當作一條命令處理。
和[
命令一樣,要注意命令和各參數之間必須用空格隔開。if
命令的參數組成一條子命令,如果該子命令的 Exit Status 為 0(表示真),則執行 then
後面的子命令,如果 Exit Status 非 0(表示假),則執行 elif
、else
或者 fi
後面的子命令。if
後面的子命令通常是測試命令,但也可以是其它命令。Shell 腳本沒有{}
括號,所以用fi
表示if
語句塊的結束。
見下例:
#! /bin/sh
if [ -f /bin/bash ]
then echo "/bin/bash is a file"
else echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi
:
是一個特殊的命令,稱為空命令,該命令不做任何事,但 Exit Status 總是真。此外,也可以執行 /bin/true
或 /bin/false
得到真或假的 Exit Status。
再看一個例子:
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
echo "Good afternoon!"
else
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1
fi
exit 0
上例中的read
命令的作用是等待用戶輸入一行字符串,將該字符串存到一個 Shell 變量中。
此外,Shell 還提供了&&
和||
語法,和 C 語言類似,具有 Short-circuit 特性,很多 Shell 腳本喜歡寫成這樣:
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
&&
相當於 “if...then...”,而||
相當於 “if not...then...”。&& 和 || 用於連接兩個命令,而上面講的-a
和-o
僅用於在測試表達式中連接兩個測試條件,要注意它們的區別,例如,
test "$VAR" -gt 1 -a "$VAR" -lt 3
和以下寫法是等價的
test "$VAR" -gt 1 && test "$VAR" -lt 3
1.3 case/esac#
case
命令可類比 C 語言的switch
/case
語句,esac
表示case
語句塊的結束。
C 語言的 case
只能匹配整型或字符型常量表達式,而Shell 腳本的 case
可以匹配字符串和 Wildcard,每個匹配分支可以有若干條命令,末尾必須以;;
結束,執行時找到第一個匹配的分支並執行相應的命令,然後直接跳到esac
之後,不需要像 C 語言一樣用break
跳出。
#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0
使用 case
語句的例子可以在系統服務的腳本目錄/etc/init.d/
中找到。這個目錄下的腳本大多具有這種形式(以/etc/apache2
為例):
case $1 in
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
*)
log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}"
exit 1
;;
esac
啟動apache2
服務的命令是
$ sudo /etc/init.d/apache2 start
$1
是一個特殊變量,在執行腳本時自動取值為第一個命令行參數,也就是start
,所以進入start)
分支執行相關的命令。同理,命令行參數指定為stop
、reload
或restart
可以進入其它分支執行停止服務、重新加載配置文件或重新啟動服務的相關命令。
1.4 for/do/done#
Shell 腳本的for
循環結構和 C 語言很不一樣,它類似於某些編程語言的foreach
循環。例如:
#! /bin/sh
for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done
FRUIT
是一個循環變量,第一次循環 $FRUIT
的取值是 apple
,第二次取值是 banana
,第三次取值是 pear
。再比如,要將當前目錄下的 chap0
、chap1
、chap2
等文件名改為 chap0~
、chap1~
、chap2~
等(按慣例,末尾有~
字符的文件名表示臨時文件),這個命令可以這樣寫:
$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done
也可以這樣寫:
$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done
1.5 while/do/done#
while
的用法和 C 語言類似。比如一個驗證密碼的腳本:
#! /bin/sh
echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done
下面的例子通過算術運算控制循環的次數:
#! /bin/sh
COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done
Shell 還有 until 循環,類似 C 語言的 do...while 循環。本章從略。
習題
1、把上面驗證密碼的程序修改一下,如果用戶輸錯五次密碼就報錯退出。
1.6 位置參數和特殊變量#
有很多特殊變量是被 Shell 自動賦值的,我們已經遇到了$?
和$1
,現在總結一下:
常用的位置參數和特殊變量
命令 | 描述 |
---|---|
$0 | 相當於 C 語言main 函數的argv[0] |
$1 、$2 ... | 這些稱為位置參數(Positional Parameter),相當於 C 語言main 函數的argv[1] 、argv[2] ... |
$# | 相當於 C 語言main 函數的argc - 1 ,注意這裡的# 後面不表示註釋 |
$@ | 表示參數列表"$1" "$2" ... ,例如可以用在for 循環中的in 後面。 |
$? | 上一條命令的 Exit Status |
$$ | 當前 Shell 的進程號 |
位置參數可以用shift
命令左移。比如shift 3
表示原來的$4
現在變成$1
,原來的$5
現在變成$2
等等,原來的$1
、$2
、$3
丟弃,$0
不移動。不帶參數的shift
命令相當於shift 1
。例如:
#! /bin/sh
echo "The program $0 is now running"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
shift
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
1.7 函數#
和 C 語言類似,Shell 中也有函數的概念,但是函數定義中沒有返回值也沒有參數列表。例如:
#! /bin/sh
foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"
注意函數體的左花括號{
和後面的命令之間必須有空格或換行,如果將最後一條命令和右花括號}
寫在同一行,命令末尾必須有;
號。
在定義 foo()
函數時並不執行函數體中的命令,就像定義變量一樣,只是給 foo
這個名字一個定義,到後面調用 foo
函數的時候(注意 Shell 中的函數調用不寫括號)才執行函數體中的命令。Shell 腳本中的函數必須先定義後調用,一般把函數定義都寫在腳本的前面,把函數調用和其它命令寫在腳本的最後(類似 C 語言中的 main
函數,這才是整個腳本實際開始執行命令的地方)。
Shell 函數沒有參數列表並不表示不能傳參數,事實上,函數就像是迷你腳本,調用函數時可以傳任意個參數,在函數內同樣是用 $0
、$1
、$2
等變量來提取參數,函數中的位置參數相當於函數的局部變量,改變這些變量並不會影響函數外面的 $0
、$1
、$2
等變量。函數中可以用 return
命令返回,如果 return
後面跟一個數字則表示函數的 Exit Status。
下面這個腳本可以一次創建多個目錄,各目錄名通過命令行參數傳入,腳本逐個測試各目錄是否存在,如果目錄不存在,首先打印信息然後試著創建該目錄。
#! /bin/sh
is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done
注意is_directory()
返回 0 表示真返回 1 表示假。
二、Shell 腳本的調試方法#
Shell 提供了一些用於調試腳本的選項,如下所示:
-n
讀一遍腳本中的命令但不執行,用於檢查腳本中的語法錯誤
-v
一邊執行腳本,一邊將執行過的腳本命令打印到標準錯誤輸出
-x
提供跟蹤執行信息,將執行的每一條命令和結果依次打印出來
使用這些選項有三種方法,一是在命令行提供參數
$ sh -x ./script.sh
二是在腳本開頭提供參數
#! /bin/sh -x
第三種方法是在腳本中用 set 命令啟用或禁用參數
#! /bin/sh
if [ -z "$1" ]; then
set -x
echo "ERROR: Insufficient Args."
exit 1
set +x
fi
set -x
和set +x
分別表示啟用和禁用-x
參數,這樣可以只對腳本中的某一段進行跟蹤調試。