shell 腳本基本語法

一. Shell的基本語法

1. 變量
按照慣例,Shell變量由全大寫字母加下劃線組成,有兩種類型的Shell變量:

環境變量

環境變量可以從父進程傳給子進程,因此Shell進程的環境變量可以從當前Shell進程傳給fork出來的子進程。用printenv命令可以顯示當前Shell進程的環境變量。


本地變量

只存在於當前Shell進程,用set命令可以顯示當前Shell進程中定義的所有變量(包括本地變量和環境變量)和函數。

環境變量是任何進程都有的概念,而本地變量是Shell特有的概念。在Shell中,環境變量和本地變量的定義和用法相似。在Shell中定義或賦值一個變量:

$VARNAME=value

注意等號兩邊都不能有空格,否則會被Shell解釋成命令和命令行參數。
一個變量定義後僅存在於當前Shell進程,它是本地變量,用export命令可以把本地變量導出爲環境變量,定義和導出環境變量通常可以一步完成:

$ export VARNAME=value

也可以分兩步完成:

$ VARNAME=value$ export VARNAME

用unset命令可以刪除已定義的環境變量或本地變量。

$ unset VARNAME

如果一個變量叫做VARNAME,用${VARNAME}可以表示它的值,在不引起歧義的情況下也可以用$VARNAME表示它的值。通過以下例子比較這兩種表示法的不同:

$ echo $SHELL$ echo $SHELLabc$ echo $SHELL abc$ echo ${SHELL}abc

注意,在定義變量時不用$,取變量值時要用$。和C語言不同的是,Shell變量不需要明確定義類型,事實上Shell變量的值都是字符串,比如我們定義VAR=45,其實VAR的值是字符串45而非整數。Shell變量不需要先定義後使用,如果對一個沒有定義的變量取值,則值爲空字符串。

2. 文件名代換(Globbing):* ?[]
這些用於匹配的字符稱爲通配符(Wildcard),具體如下:

2.1. 通配符

*
匹配0個或多個任意字符
?
匹配一個任意字符
[若干字符]
匹配方括號中任意一個字符的一次出現


$ ls /dev/ttyS*$ ls ch0?.doc$ ls ch0[0-2].doc$ ls ch[012][0-9].doc

注意,Globbing所匹配的文件名是由Shell展開的,也就是說在參數還沒傳給程序之前已經展開了,比如上述lsch0[012].doc命令,如果當前目錄下有ch00.doc和ch02.doc,則傳給ls命令的參數實際上是這兩個文件名,而不是一個匹配字符串。

3. 命令代換:`或 $()
由反引號括起來的也是一條命令,Shell先執行該命令,然後將輸出結果立刻代換到當前命令行中。例如定義一個變量存放date命令的輸出:

$ DATE=`date`$ echo $DATE

命令代換也可以用$()表示:

$ DATE=$(date)


4. 算術代換:$(())
用於算術計算,$(())中的Shell變量取值將轉換成整數,例如:

$ VAR=45$ echo $(($VAR+3))

$(())中只能用+-*/和()運算符,並且只能做整數運算。

5. 轉義字符\
和C語言類似,\在Shell中被用作轉義字符,用於去除緊跟其後的單個字符的特殊意義(回車除外),換句話說,緊跟其後的字符取字面值。例如:

$ echo $SHELL/bin/bash$ echo \$SHELL$SHELL$ echo \\\

比如創建一個文件名爲“$ $”的文件可以這樣:

$ touch \$\ \$

還有一個字符雖然不具有特殊含義,但是要用它做文件名也很麻煩,就是-號。如果要創建一個文件名以-號開頭的文件,這樣是不行的:

$ touch -hellotouch: invalid option -- hTry `touch --help' for more information.

即使加上\轉義也還是報錯:

$ touch \-hellotouch: invalid option -- hTry `touch --help' for more information.

因爲各種UNIX命令都把-號開頭的命令行參數當作命令的選項,而不會當作文件名。如果非要處理以-號開頭的文件名,可以有兩種辦法:

$ touch ./-hello

或者

$ touch -- -hello

\還有一種用法,在\後敲回車表示續行,Shell並不會立刻執行命令,而是把光標移到下一行,給出一個續行提示符>,等待用戶繼續輸入,最後把所有的續行接到一起當作一個命令執行。例如:

$ ls \> -l(ls -l命令的輸出)


6. 單引號
和 C語言不一樣,Shell腳本中的單引號和雙引號一樣都是字符串的界定符(雙引號下一節介紹),而不是字符的界定符。單引號用於保持引號內所有字符的字面值,即使引號內的\和回車也不例外,但是字符串中不能出現單引號。如果引號沒有配對就輸入回車,Shell會給出續行提示符,要求用戶把引號配上對。例如:

$ echo '$SHELL'$SHELL$ echo 'ABC\(回車)> DE'(再按一次回車結束命令)ABC\DE


7. 雙引號
雙引號用於保持引號內所有字符的字面值(回車也不例外),但以下情況除外:

·        $加變量名可以取變量的值

·        反引號仍表示命令替換

·        \$表示$的字面值

·        \`表示`的字面值

·        \"表示"的字面值

·        \\表示\的字面值

·        除以上情況之外,在其它字符前面的\無特殊含義,只表示字面值

$ echo "$SHELL"/bin/bash$ echo "`date`"Sun Apr 20 11:22:06 CEST 2003$ echo "I'd say: \"Go for it\""I'd say: "Go for it"$ echo "\"(回車)>"(再按一次回車結束命令)" $ echo "\\"\


二、Shell腳本語法

1. 條件測試:test[
命令test或[可以測試一個條件是否成立,如果測試結果爲真,則該命令的ExitStatus爲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命令不需要]參數。以[命令爲例,常見的測試命令如下表所示:

表 1.1. 測試命令

[ -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語言類似,測試條件之間還可以做與、或、非邏輯運算:

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

[ ! 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


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

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

其實是三條命令,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"fiif :; then echo "always true"; fi

:是一個特殊的命令,稱爲空命令,該命令不做任何事,但ExitStatus總是真。此外,也可以執行/bin/true或/bin/false得到真或假的Exit Status。再看一個例子:

#! /bin/sh echo "Is it morning? Please answer yes or no."read YES_OR_NOif [ "$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 1fiexit 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


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_NOcase "$YES_OR_NO" inyes|y|Yes|YES)  echo "Good Morning!";;[nN]*)  echo "Good Afternoon!";;*)  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."  exit 1;;esacexit 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可以進入其它分支執行停止服務、重新加載配置文件或重新啓動服務的相關命令。

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


5. while/do/done
while的用法和C語言類似。比如一個驗證密碼的腳本:

#! /bin/sh echo "Enter password:"read TRYwhile [ "$TRY" != "secret" ]; do  echo "Sorry, try again"  read TRYdone

下面的例子通過算術運算控制循環的次數:

#! /bin/sh COUNTER=1while [ "$COUNTER" -lt 10 ]; do  echo "Here we go again"  COUNTER=$(($COUNTER+1))done

Shell還有until循環,類似C語言的do...while循環。本章從略。

習題
1、把上面驗證密碼的程序修改一下,如果用戶輸錯五次密碼就報錯退出。

6. 位置參數和特殊變量
有很多特殊變量是被Shell“自動賦值”的,我們已經遇到了$?和$1,現在總結一下:

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

$0
相當於C語言main函數的argv[0]
$1$2...
這些稱爲位置參數(PositionalParameter),相當於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 $@"shiftecho "The first parameter is $1"echo "The second parameter is $2"echo "The parameter list is $@"


7. 函數
和C語言類似,Shell中也有函數的概念,但是函數定義中沒有返回值也沒有參數列表。例如:

#! /bin/sh foo(){ echo "Function foo is called";}echo "-=start=-"fooecho "-=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  fidone

注意is_directory()返回0表示真返回1表示假。

三 Shell腳本的調試方法
Shell提供了一些用於調試腳本的選項,如下所示:

-n

讀一遍腳本中的命令但不執行,用於檢查腳本中的語法錯誤

-v

一邊執行腳本,一邊將執行過的腳本命令打印到標準錯誤輸出

-x

提供跟蹤執行信息,將執行的每一條命令和結果依次打印出來

使用這些選項有三種方法,一是在命令行提供參數

$ sh -x ./script.sh

二是在腳本開頭提供參數

#! /bin/sh -x

第三種方法是在腳本中用set命令啓用或禁用參數

#! /bin/shif [ -z "$1" ]; then  set -x  echo "ERROR: Insufficient Args."  exit 1  set +xfi

set-x和set+x分別表示啓用和禁用-x參數,這樣可以只對腳本中的某一段進行跟蹤調試。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章