5. Shell腳本語法

5.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-gt3][命令的四個參數,它們之間必須用空格隔開。命令test[的參數形式是相同的,只不過test命令不需要]參數。以[命令為例,常見的測試命令如下表所示:

表 31.2. 測試命令

[ -d DIR ]如果DIR存在並且是一個目錄則為真
[ -f FILE ]如果FILE存在且是一個普通檔案則為真
[ -z STRING ]如果STRING的長度為零則為真
[ -n STRING ]如果STRING的長度非零則為真
[ STRING1 = STRING2 ]如果兩個字元串相同則為真
[ STRING1 != STRING2 ]如果字元串不相同則為真
[ ARG1 OP ARG2 ]ARG1ARG2應該是整數或者取值為整數的變數,OP-eq(等於)-ne(不等於)-lt(小於)-le(小於等於)-gt(大於)-ge(大於等於)之中的一個

和C語言類似,測試條件之間還可以做與、或、非邏輯運算:

表 31.3. 帶與、或、非的測試命令

[ ! EXPR ]EXPR可以是上表中的任意一種測試條件,!表示邏輯反
[ EXPR1 -a EXPR2 ]EXPR1EXPR2可以是上表中的任意一種測試條件,-a表示邏輯與
[ EXPR1 -o EXPR2 ]EXPR1EXPR2可以是上表中的任意一種測試條件,-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

5.2. if/then/elif/else/fi

和C語言類似,在Shell中用ifthenelifelsefi這幾條命令實現分支控制。這種流程控制語句本質上也是由若干條Shell命令組成的,例如先前講過的

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

其實是三條命令,if [ -f ~/.bashrc ]是第一條,then . ~/.bashrc是第二條,fi是第三條。如果兩條命令寫在同一行則需要用;號隔開,一行只寫一條命令就不需要寫;號了,另外,then後面有換行,但這條命令沒寫完,Shell會自動續行,把下一行接在then後面當作一條命令處理。和[命令一樣,要注意命令和各參數之間必須用空格隔開。if命令的參數組成一條子命令,如果該子命令的Exit Status為0(表示真),則執行then後面的子命令,如果Exit Status非0(表示假),則執行elifelse或者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

5.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)分支執行相關的命令。同理,命令行參數指定為stopreloadrestart可以進入其它分支執行停止服務、重新加載配置檔案或重新啟動服務的相關命令。

5.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。再比如,要將當前目錄下的chap0chap1chap2等檔案名改為chap0~chap1~chap2~等(按慣例,末尾有~字元的檔案名表示臨時檔案),這個命令可以這樣寫:

$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

也可以這樣寫:

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done

5.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、把上面驗證密碼的程序修改一下,如果用戶輸錯五次密碼就報錯退出。

5.6. 位置參數和特殊變數

有很多特殊變數是被Shell自動賦值的,我們已經遇到了$?$1,現在總結一下:

表 31.4. 常用的位置參數和特殊變數

$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 $@"

5.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表示假。