簡單shell指令碼
!/bin/bash
這一行表明,不管使用者選擇的是那種互動式shell,該指令碼需要使用bash shell來執行。由於每種shell的語法大不相同,所以這句非常重要。
簡單例項
下面是一個非常簡單的shell指令碼。它只是執行了幾條簡單的命令
1 2 3 4 |
#!/bin/bash echo "hello, $USER. I wish to list some files of yours" echo "listing files in the current directory, $PWD" ls # 列出當前目錄所有檔案 |
首先,請注意第四行。在bash指令碼中,跟在#符號之後的內容都被認為是註釋(除了第一行)。Shell會忽略註釋。這樣有助於使用者閱讀理解指令碼。 ?$USER和 $PWD都是變數。它們是bash指令碼自定義的標準變數,無需在指令碼中定義即可使用。請注意,在雙引號中引用的變數會被展開(expanded)。“expanded”是一個非常合適的形容詞:基本上,當shell執行命令並遇到$USER變數時,會將其替換為該變數對應的值。
變數
任何程式語言都會用到變數。你可以使用下面的語句來定義一個變數:
1 |
X="hello" |
並按下面的格式來引用這個變數:
$X
更具體的說,$X表示變數X的值。關於語義方面有如下幾點需要注意:
- 等於號兩邊不可以有空格!例如,下面的變數宣告是錯誤的 :
1 |
X = hello |
- 在我所展示的例子中,引號並不都是必須的。只有當變數值包含空格時才需要加上引號。例如:
1 2 |
X = hello world # 錯誤 X = "hello world" # 正確 |
這是由於shell將每一行命令視為命令及其引數的集合,以空格分隔。 foo=bar就被視為一條命令。foo = bar 的問題就在於shell將空格分開的foo視為命令。同樣,X=hello world的問題就在於shell將X=hello視為一條完整的命令,而”world”則被徹底無視(因為賦值命令不需其他引數)。
單引號 VS 雙引號
基本上來說,變數名會在雙引號中展開,單引號中則不會。如果你不需要引用變數值,那麼使用單引號可以很直觀的輸出你期望的結果。 An example 示例
1 2 3 4 |
#!/bin/bash echo -n '$USER=' # -n選項表示阻止echo換行 echo "$USER" echo "\$USER=$USER" # 該命令等價於上面的兩行命令 |
輸出如下(假設你的使用者名稱為elflord)) $USER=elflord $USER=elflord
1 2 3 |
$USER=elflord $USER=elflord |
從例子中可以看出,在雙引號中使用轉義字元也是一種解決方案。雖然雙引號的使用更靈活,但是其結果不可預見。如果要在單引號和雙引號之間做出選擇,最好選擇單引號。
使用引號封裝變數
有時候,使用雙引號來保護變數名是個很好的點子。如果你的變數值存在空格或者變數值為空字串,這點就顯得尤其重要。看下面這個例子:
1 2 3 4 5 |
#!/bin/bash X="" if [ -n $X ]; then # -n 用來檢查變數是否非空 echo "the variable X is not the empty string" fi |
執行這個指令碼,輸出如下:
the variable X is not the empty string
為何?這是因為shell將$X展開為空字串,表示式[-n]返回真值(因為改表示式沒有提供引數)。再看這個指令碼:
1 2 3 4 5 |
#!/bin/bash X="" if [ -n "$X" ]; then # -n 用來檢查變數是否非空 echo "the variable X is not the empty string" fi |
在這個例子中,表示式展開為[ -n “”],由於引號中內容為空,因此該表示式返回false值。
在執行時展開變數
為了證實shell就像我上面說的那樣直接展開變數,請看下面的例子:
1 2 3 4 5 |
#!/bin/bash LS="ls" LS_FLAGS="-al" $LS $LS_FLAGS $HOME |
乍一看可能有點不好理解。其實最後一行就是執行這樣一條命令:
Ls -al /home/elflord
(假設當前使用者home目錄為/home/elflord)。這就說明了shell僅僅只是將變數替換為對應的值再執行命令而已。
使用大括號保護變數
這裡有一個潛在的問題。假設你想列印變數X的值,並在值後面緊跟著列印”abc”。那麼問題來了:你該怎麼做呢? 先試一試:
1 2 3 |
#!/bin/bash X=ABC echo "$Xabc" |
這個指令碼沒有任何輸出。究竟哪裡出了問題?這是由於shell以為我們想要列印變數Xabc的值,實際上卻沒有這個變數。為了解決這種問題可以用大括號將變數名包圍起來,從而避免其他字元的影響。下面這個指令碼可以正常工作:
!/bin/bashX=ABCecho “${X}abc”
1 2 3 |
#!/bin/bash X=ABC echo "${X}abc" |
條件語句, if/then/elif
在某些情況下,我們需要做條件判斷。比如判斷字串長度是否為0?判斷檔案foo是否存在?它是一個連結檔案還是實際檔案?首先,我們需要if命令來執行檢查。語法如下:
1 2 3 4 5 6 |
if condition then statement1 statement2 .......... fi |
當指定條件不滿足時,可以通過else來指定其他執行動作。
1 2 3 4 5 6 7 8 |
if condition then statement1 statement2 .......... else statement3 fi |
當if條件不滿足時,可以新增多個elif來檢查其他條件是否滿足。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if condition1 then statement1 statement2 .......... elif condition2 then statement3 statement4 ........ elif condition3 then statement5 statement6 ........ fi |
當相關條件滿足時,shell會執行在相應的if/elif與下個elif或fi之間的語句。事實上,判斷條件可以是任意命令,當且只當命令返回並且退出狀態為0時,才會執行該條件塊中的語句(換句話說,就是當命令成功返回時)。不過在本文的學習中,我們只會關注“test”或“[]”形式的條件判斷。
Test命令與操作符
條件判斷中的命令幾乎都是test命令。test根據測試條件通過或失敗來返回true或false(更準確的說是返回0或非0值)。如下所示:
1 |
test operand1 operator operand2 |
對某些測試來說,只需要一個運算元(operand2)通常是下面這種情況的簡寫:
1 |
[ operand1 operator operand2 ] |
為了讓我們的討論更接地氣一點,給出下面一些例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#!/bin/bash X=3 Y=4 empty_string="" if [ $X -lt $Y ] # is $X less than $Y ? then echo "$X=${X}, which is smaller than $Y=${Y}" fi if [ -n "$empty_string" ]; then echo "empty string is non_empty" fi if [ -e "${HOME}/.fvwmrc" ]; then # test to see if ~/.fvwmrc exists echo "you have a .fvwmrc file" if [ -L "${HOME}/.fvwmrc" ]; then # is it a symlink ? echo "it's a symbolic link elif [ -f "${HOME}/.fvwmrc" ]; then # is it a regular file ? echo "it's a regular file" fi else echo "you have no .fvwmrc file" fi |
需要注意的細節
Test命令的格式為“運算元< 空格 >操作符< 空格 >運算元”或者“操作符< 空格 >運算元”,這裡特別說明必須要有這些空格,因為shell將沒有空格的第一串字元視為一個操作符(以-開頭)或者運算元。比如下面這個:
if [ 1=2 ]; then echo “hello”fi
它會列印出hello,這明顯與預期結果是不一致的(因為shell只看到運算元1=2,沒看到操作符)。
還有一種隱藏陷阱是未加引號的變數。像我們之前例子說的-n測試時變數須加引號的情形。其實,不管在什麼情況下,加上引號總是沒有壞處的,還有可能規避一些很奇葩的錯誤。因為有時候不加引號的變數擴充套件開的測試結果會讓人非常困惑。例如:
1 2 3 4 5 6 |
#!/bin/bash X="-n" Y="" if [ $X = $Y ] ; then echo "X=Y" fi |
這個指令碼列印出來的結果是錯誤的,因為shell將判斷展開為 [ -n = ],但是”=”的長度不為0,所以條件判斷通過從而導致輸出結果為“X=Y”。
Test操作符簡介
下圖是test操作符的快速查詢列表。當然這個列表並不全面,但記下這些就足夠平常使用了(如果還需要了解其他操作符,可以檢視man手冊)。
operator | produces true if… | number of operands |
-n | operand non zero length | 1 |
-z | operand has zero length | 1 |
-d | there exists a directory whose name is operand | 1 |
-f | there exists a file whose name is operand | 1 |
-eq | the operands are integers and they are equal | 2 |
-neq | the opposite of -eq | 2 |
= | the operands are equal (as strings) | 2 |
!= | opposite of = | 2 |
-lt | operand1 is strictly less than operand2 (both operands should be integers) | 2 |
-gt | operand1 is strictly greater than operand2 (both operands should be integers) | 2 |
-ge | operand1 is greater than or equal to operand2 (both operands should be integers) | 2 |
-le | operand1 is less than or equal to operand2 (both operands should be integers) | 2 |
迴圈
迴圈結構允許我們執行重複的步驟或者在若干個不同條目上執行相同的程式。Bash中有下面兩種迴圈
- for 迴圈
- while 迴圈
For 迴圈
直接來個例子,來直觀地感受for迴圈的語法。
1 2 3 4 5 |
#!/bin/bash for X in red green blue do echo $X done |
For迴圈會遍歷空格分開的條目。注意,如果某一項含有空格,必須要用引號引起來,例子如下:
1 2 3 4 5 6 7 8 |
#!/bin/bash colour1="red" colour2="light blue" colour3="dark green" for X in "$colour1" $colour2" $colour3" do echo $X done |
如果我們漏掉for迴圈中的引號,你能猜想出會發生什麼嗎?這個例子說明,除非你確認變數中不會包含空格,否則最好都用引號將變數保護起來。
在for迴圈中使用萬用字元
如果shell解析字串時遇到*號,會將它展開為所有匹配的檔名。當且僅當目標檔案與號展開後的字串一致才會匹配成功。例如,單獨的*號展開為當前目錄的所有檔案,中間以空格分開(包含隱藏檔案)。
所以:
echo *
列出當前目錄下的所有檔案和目錄。
echo *.jpg
列出所有的jpeg圖片格式的檔案。
echo ${HOME}/public_html/*.jpg
列出home目錄中public_html目錄下的所有jpeg檔案。
正是由於這種特性,使得我們可以很方便的來操作目錄和檔案,尤其是和for迴圈結合使用時,更是便利。例子如下:
1 2 3 4 5 |
#!/bin/bash for X in *.html do grep -L '<UL>' "$X" done |
列印出當前目錄下所有不包含<UL>欄位的html檔案。
While 迴圈
當給定條件為真值時,while迴圈會重複執行。例如:
1 2 3 4 5 6 7 |
#!/bin/bash X=0 while [ $X -le 20 ] do echo $X X=$((X+1)) done |
這樣導致這樣的疑問: 為什麼bash不能使用C風格的for迴圈呢?
for (X=1,X<10; X++)
這也跟bash自身的特性有關,之所以不允許這種for迴圈是由於:bash是一種解釋性語言,因此其執行效率比較低。也正是由於這個原因,高負荷迭代是不允許的。
命令替換
Bash shell有個非常好用的特性叫做命令替換。允許我們將一個命令的輸出當做另一個命令的輸入。比如你想要將命令的輸出賦值給變數X,你可以通過變數替換來實現。
有兩種命令替換的方式:大括號擴充套件和反撇號擴充套件。
大括號擴充套件: $(commands) 會展開為命令commands的輸出結果。並且允許巢狀使用,所以commands中允許包含子大括號擴充套件。
反撇好擴充套件:將commands
擴充套件為命令commands的輸出結果。不允許巢狀。
這裡有一個例子:
1 2 3 4 5 6 7 |
#!/bin/bash files="$(ls)" web_files=`ls public_html` echo "$files" # we need the quotes to preserve embedded newlines in $files echo "$web_files" # we need the quotes to preserve newlines X=`expr 3 * 2 + 4` # expr evaluate arithmatic expressions. man expr for details. echo "$X" |
$()替換方式的優點不言自明:非常易於巢狀。並且大多數bourne shell的衍生版本都支援(POSIX shell 或者更好的都支援)。不過,反撇號替換更簡單明瞭,即使是最基本的shell它也提供了支援(任意版本的#!/bin/sh都可以)。