<!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } -->
Bash 程式設計
控制結構:控制結構有 if...then 、 for...in 、 while 、 until 、 case 語句 。與控制結構配合使用的還有break 和continue 語句也可用于調整shell 腳本中的指令執行順序。
if...then :
if test-command
then
command
fi
test 内置指令:if 測試test-command 傳回狀态,并基于這個狀态轉移控制。if 語句的結束由fi 标記。
echo -n “word 1:”
read word1
echo -n “word 2:”
read word2
if test “$word1” = ”$word2”
then
echo “Match”
fi
echo “End of program.”
上面echo 利用'-n' 選項來表示輸出echo 參數後不用換行。
在bash 中test 是一個内置指令,也就是它是shell 的一部分。同時還有一個單獨的工具test 。通常某個指令的工具版本不存在,而内置指令版本可用時,使用後者。test 測試語句中使用的'=' 兩邊的空白字元不可以省略。
test 的操作符'-eq' 比較兩個整數,特殊變量$# 表示指令行參數的個數。
if test $# -eq 0
then
echo “You must supply at least one argument.”
exit 1
fi
echo “Program running.”
可以利用'info test' 得到完整的test 用法資訊:
測試檔案類型:'-b FILE' '-c FILE' '-d FILE' '-f FILE' '-h FILE' '-L FILE' '-p FILE' '-S FILE' ‘-t FD’
通路權限測試:'-g FILE' '-k FILE' '-r FILE' '-u FILE' '-w FILE' '-x FILE' '-O FILE'
'-G FILE'
檔案特征比較:'-e FILE' '-s FILE' 'FILE1 -nt FILE2' 'FILE1 -ot FILE2'
'FILE1 -ef FILE2'
字元串測試:'-z STRING' '-n STRING' 'STRING' 'STRING1 = STRING2'
'STRING1 != STRING2'
整數測試:'ARG1 -eq ARG2' 'ARG1 -ne ARG2' 'ARG1 -lt ARG2' 'ARG1 -le ARG2'
'ARG1 -gt ARG2' 'ARG1 -ge ARG2'
測試條件連接配接詞:'! EXPR ' 'EXPR1 -a EXPR2 ' 'EXPR1 -o EXPR2 '( 注意中間的空白符号)
[] 與test 同義 可以把test 的參數用方括号括起來,以代替在腳本中使用關鍵字test 。括号兩邊必須有空白符
if [ $# -eq 0 ]
then
ehco “Usage : chkarg2 argument...” 1>&2
exit 1
fi
echo “Program running.”
exit 0
用法資訊 上面例子顯示的資訊稱為用法資訊,使用 '1>&2' 标記可把這個輸出重定位到标準錯誤輸出 。在腳本正常運作後一般退出狀态為 0 ,如果退出狀态非 0 表示遇到一個錯誤。
if...then...else
if test-command
then
commands
else
commands
fi
if test-command;then
commands
else
commands
fi
下面的shell 腳本,參數為檔案名,該腳本将檔案内容顯示在終端上。如果第一個參數為'-v' ,該腳本将使用less 指令分頁顯示。
if [ $# -eq 0 ]
then
echo “Usage: out [-v] filename...” 1>&2
exit 1
fi
if [ “$1” = “-v” ]
then
shift
less -- “$@”
else
cat -- “$@”
fi
在上面的腳本中,cat 和 less 指令的 '--' 參數通知調用這些工具的指令行它後面再沒有選項了,即不再把 '--' 後面以連字元開頭的參數作為選項。‘ rm -- -example.sh’( 删除檔案 -example.sh).
shift 和 set 對位置參數的操作。
if … then … elif :
if test-command
then
commands
elif test-command
then
commands
…
else
commands
fi
示例:
echo -n “Word 1:”
read word1
echo -n “Word 2:”
read word2
echo -n “Word 3:”
read word3
if [ “$word1” = “$word2” -a “$word1” = “$word3” ]
then
echo “Match : words 1,2 & 3”
elif [ “$word1” = “$word2” ]
then
echo “Match : word 1 & 2”
elif [ “$word2” = “$word3” ]
then
echo “Match : word 2 & 3”
else
echo “No Match”
fi
上面的腳本第一個if 語句使用布爾操作符 AND(-a) 作為 test 的一個參數。注意 '=' '[' 和 ']' 兩邊的空白符不可省略。
1 #!/bin/bash
2 # Identify links to a file
3 # Usage: lnks file [directory]
4 if [ $# -eq 0 -o $# -gt 2 ]
5 then
6 echo "Usage:lnks file [directory]" 1>&2
7 exit 1
8 fi
9
10 if [ -d "$1" ]
11 then
12 echo "First argument cannot be a directory." 1>&2
13 exit 1
14 else
15 file="$1"
16 fi
17 if [ $# -eq 1 ]
18 then
19 directory="."
20 elif [ -d "$2" ]
21 then
22 directory="$2"
23 else
24 echo "Optional second argument must be a directory." 1>&2
25 echo "Usage:lnks file [directory]." 1>&2
26 exit 1
27 fi
28
29 #check that file exists and is a regular file:
30 if [ ! -f "$file" ]
31 then
32 echo "lnks:$file not found or special file" 1>&2
33 exit 1
34 fi
35 #check link count on file
36 set -- $(ls -l "$file")
37 linkcnt=$2
38 if [ "$linkcnt" -eq 1 ]
39 then
40 echo "lnks : no other hard links to $file" 1>&2
41 exit 0
42 fi
43 #get the inode of the given file
44 set $(ls -i "$file")
45
46 inode=$1
47 #find and print the files with that incite number
48 echo "lnks:using find to search for links..." 1>&2
49 find "$directory" -xdev -inum $inode -print
50
黑體黃底指令行使用了指令替換 $(command)
确定shell ,用法消息,注釋,測試參數。
内置指令 set 使用指令替換機制把位置參數設定為 ls -l 指令的輸出。 輸出的第二個字段就是連結的數目,是以就将linkcnt 就被設定為$2 。set 中的參數'--' 防止把'-ls -l'( 第一個輸出字段是對檔案的通路許可,一般以字元‘-’ 開頭) 産生的資訊作為選項。
比較與檔案名關聯的inode 來确定多個檔案名是否連結到同一個檔案,這是一種好方法。
find 工具用于查找滿足其參數所指定條件的檔案,查找的位置由第一個參數($directory) 指定并搜尋其所有子目錄。餘下的參數指定把值為$inode 的檔案的檔案名送到标準輸出。由于不同檔案系統中的檔案可能擁有相同的inode 号,并且沒有連結,find 必須隻查找同一檔案系統中的目錄。參數-xdev 防止find 指令搜尋其他檔案系統。
可以使用 -x 選項來幫助調試腳本。該選項使得 shell 執行每條指令前把指令顯示出來。
可以在帶參數'-x' 的shell 中運作上面的shell 腳本,在執行每條指令前先将該指令顯示出來。要麼為目前shell 設定'-x' 選項( 使用 set -x 指令) ,以使所有腳本的指令在執行前都顯示出來。要麼隻在目前執行腳本中通過shell 使用'-x' 選項。
>bash -x lnks.sh example
腳本中的每條指令前都會加上PS4 變量的值,預設是加号'+'. 是以可通過該值來區分是腳本輸出還是調試輸出。
通過在腳本的開始位置加入帶有 '-x' 參數的 set ,就可以設定運作該腳本的 shell 。 'set -x'
若要關閉調試則引應使用指令:'set +x'
'set -x' 同 'set -xtrace'
控制結構for...in 的文法如下:
for loop-index in argument-list
do
commands
done
依照argument-list 中的每個參數( 将對應的每一個參數指派給loop-index 變量) 重複執行do 和done 語句之間的指令。
例子1 :
1 for fruit in apples oranges pears bananas
2 do
3 echo "$fruit"
4 done
例子2 :
1 for i in *
2 do
3 if [ -d "$i" ]
4 then
5 echo "$i"
6 fi
7 done
'*' 比對目前目錄下所有可見檔案名,與運作該 shell 腳本的目前目錄有關,而與 shell 腳本所在目錄無關。
for 控制結構文法如下:
for loop-index
do
commands
done
loop-index 用指令行參數中的每個參數值取代 ,重複執行do 和done 之間的語句。除了參數來源不同,該結構文法同'for...in' 結構文法。
例子:
>cat -n for_test.sh
1 for arg
2 do
3 echo "$arg"
4 done
5
>chmod a+rwx for_test.sh
>./for_test.sh example example2 example4 example6
'for arg' 隐含表示 'for arg in “$@”' ,這裡将” $@” 擴充為一個指令行參數清單, 如” $1””$2””$3” 等。
>cat -n whos.sh
1 #!/bin/bash
2 # adapted from finger.sh by Lee Sailer
3 # UNIX/WORLD
4
5 if [ $# -eq 0 ]
6 then
7 echo "Usage: whos id ..." 1>&2
8 exit 1
9 fi
10
11 for id
12 do
13 gawk -F: ' {print $1,$5} ' /etc/passwd |
14 grep -i "$id"
15 done
16
>./whos.sh root “terry Zeng”
腳本whos 示範了for 結構中$@ 符号所代表的含義。whos 腳本後面可以跟一個或幾個使用者名作為參數,執行whos 腳本就可以将這些與使用者相關的資訊顯示出來。
在這個腳本中的 for 循環中隐含的 $@ 的使用特别有效果,因為它使得 for 循環可以把帶空格的參數視為一個單獨的參數 ( 如參數” terry Zeng”) 。
對于每個指令行參數,whos 搜尋/etc/passwd 檔案。在for 循環結構中gawk 的作用是從檔案/etc/passwd 的行中提取第一個($1) 和第5 個($5) 字段的内容。第1 個和第5 個字段的内容通過一個管道傳遞給grep 。grep 在其輸入中搜尋内容為$id($id 就是輸入的指令行參數) 的對象。選項'-i' 的作用是讓grep 在搜尋過程中忽略其他的事件,grep 在其輸入中按照每行的格式顯示包含$id 内容的對象。
行末的| 即便在管道标志後面還有換行,管道也能照常工作。
while :
while 控制結構文法如下:
while test-command
do
commands
done
隻要測試條件的傳回值為真,while 結構語句就要執行do 與done 語句之間的指令。
例子:
>cat -n count.sh
1 #!/bin/bash
2 number=0
3 while [ "$number" -lt 10 ]
4 do
5 echo -n "$number"
6 ((number += 1))
7 done
8 echo
test 内置指令:腳本使用了'-lt' 來執行數值比較測試。對于數值比較測試由以下幾種測試選項:'-ne','-eq','-gt','-ge','-lt','-le' 。對于字元串的比較可以用'=' 或者'!=' 來進行測試比較。
使用 '-n' 選項用來防止 echo 在其輸出之後輸出換行。最後的 echo 使腳本在标準輸出上輸出一個新的字元行。 ((number += 1)) 指派表達式。
例子:
1 #!/bin/bash
2 # remove correct spellings from aspell output
3
4 if [ $# -ne 2 ]
5 then
6 echo "Usage spell_check.sh file1 file2" 1>&2
7 echo "file1 list of correct spellings" 1>&2
8 echo "file2 file to be checked" 1>&2
9 exit 1
10 fi
11
12 if [ ! -r "$1" ]
13 then
14 echo "spell ..check; $1 is not readable" 1>&2
15 exit 1
16 fi
17
18 if [ ! -r "$2" ]
19 then
20 echo "spell ..check; $2 is not readable" 1>&2
21 exit 1
22 fi
23
24 aspell -l < "$2" |
25 while read line
26 do
27 if ! grep "^$line$" "$1" > /dev/null
28 then
29 echo $line
30 fi
31 done
32
上面的腳本中'-r' 參數判斷一個檔案是否是可讀的,括号裡的感歎号是對跟着的操作符進行相反的運算,與'-r' 參數聯合起來用以判斷一個檔案是否是不可讀的。
上面的腳本把aspell 腳本的輸出( 使用參數-l 可以讓aspell 腳本把所檢查出的錯誤單詞的清單送到标準輸出上) 通過一個管道送到while 結構的标準輸入上,while 結構從他的标準輸入上一次讀入一行内容( 每行隻有一個單詞) 。隻要測試條件( 也就是read line) 能從标準輸入上得到一個單詞,那麼它就傳回一個true 狀态。
在while 循環中if 語句用來檢測grep 條件測試的傳回值,grep 是用來判斷被讀取行是否在使用者的正确單詞清單中。grep 搜尋的模式( 也就是$line) 前後都有特殊字元,這些字元分别用來指明一行的開始和結束( 分别是'^' 和'$' ;'^$line$' 了解為 '^---$line---$' 其中 '^' 代表行的開始,第一個 '$' 代表對變量 'line' 的引用,第二個 '$' 代表行的結束 ) 。 這些特殊符号的作用是確定grep 搜尋時隻有當變量$line 的内容與使用者輸入的正确單詞清單的一整行的内容相同時才形成比對。grep 的标準輸出被重定向到檔案/dev/null 中,因為我們不關心輸出結果,隻關心傳回狀态。
' if ! grep "^$line$" "$1" > /dev/null ' 可以替換為 ' if ! grep -qw “$line” “$1” ' 。 其中'-q' 抑制了grep 的輸出,這樣隻傳回退出狀态。'-w' 使得grep 隻比對整個單詞。
until
until 與while 語句的文法結構相似。差別僅在于條件語句的測試位置:一個在語句的開始測試,一個在語句的結束測試。
until test-commands
do
commands
done
例子:
1 #! /bin/bash
2 secretname=jenny
3 name=noname
4 echo "Try to guess the secret name!"
5 echo
6 until [ "$name" = "$secretname" ]
7 do
8 echo -n "Your guess:"
9 read name
10 done
11 echo "Very good."
12
例子:
1 #!/bin/bash
2 # UNIX/WORLD
3 trap ' ' 1 2 3 18
4 stty -echo
5 echo -n "Key: "
6 read key_1
7 echo
8 echo -n "Again:"
9 read key_2
10 echo
11 key3=
12 if [ "$key_1" = "$key_2" ]
13 then
14 tput clear
15 until [ "$key_3" = "$key_2" ]
16 do
17 read key_3
18 done
19 else
20 echo "locktty: keys do not match" 1>&2
21 fi
22 stty echo
23
如果運作上面的例子而忘記了腳本密碼,可以登入另一個虛拟終端并終止該程序。
trap 内置指令:利用 trap 内置指令可以防止使用者通過發送中斷的方式來終止腳本的運作。通過捕獲信号 18 可以保證使用者無法用 CONTROL+Z 組合鍵來挂起運作該腳本的程序。 stty -echo 指令來防止終端把鍵盤輸入的字元顯示出來 ,這樣可以保證輸入的密碼不被顯示出來。
break 與continue :同c 語言可以用來中斷for while until 語句的執行
1 #!/bin/bash
2 for index in 1 2 3 4 5 6 7 8 9
3 do
4 if [ $index -le 3 ]
5 then
6 echo "continue"
7 continue
8 fi
9 echo $index
10 if [ $index -ge 8 ]
11 then
12 echo "break"
13 break
14 fi
15 done
16
case :一種多分支選擇機制,同C 語言。
case test-string in
pattern-1)
commands-1
;;
pattern-2)
commands-2
;;
pattern-3)
commands-3
;;
...
esac
case 結構中的比對類型類似于一個模糊檔案引用。
字元‘*’: 比對任意字元串,用作預設的case 比對。
字元‘?’: 比對 單個字元。
[…]: 定義一個字元類,對處于方括号中的每個字元依次進行單字元比對;兩個字元之間的連字元用來指定字元範圍。
| : 分離帶有選擇的選項,這些選項滿足case 結構的一個特别的分支。
例子:
1 echo -n "Enter A,B,or C:"
2 read letter
3 case "$letter" in
4 a|A)
5 echo "You Entered A"
6 ;;
7 b|B)
8 echo "You Entered B"
9 ;;
10 c|C)
11 echo "You Entered C"
12 ;;
13 *)
14 echo "You did not enter A,B,or C"
15 ;;
16 esac
例子:
1 #!/bin/bash
2
3 echo -e "/n COMMAND MENU/n"
4 echo "a.Current date and time"
5 echo "b.Users currently logged in"
6 echo "c.Name of the working directory"
7 echo -e "d.Contents of the working directory/n"
8 echo -n "Enter a,b,c,or d:"
9 read answer
10 echo
11 case "$answer" in
12 a)
13 date
14 ;;
15 b)
16 who
17 ;;
18 c)
19 pwd
20 ;;
21 d)
22 ls
23 ;;
24 *)
25 echo "There is no selection:$answer"
26 ;;
27 esac
'echo -e' 選項 -e 使 echo 把後面的 '/n' 解釋為一個換行符,如果 echo 後面不加這個參數 '-e' , echo 就會輸出兩個字元 '/n' ,而不是一個空行。參數 '-e' 使得 echo 解釋由反斜杠 '/' 轉義的字元。 帶有反斜杠的字元一定要引起來,否則反斜杠就要由 shell 來解釋而不會傳到 echo 由 echo 解釋。
例子:
>cat -n safedit
1 #!/bin/bash
2 PATH=/bin:/usr/bin
3 script=$(basename $0)
4 case $# in
5 0)
6 vim
7 exit 0
8 ;;
9 1)
10 if [ ! -f "$1" ]
11 then
12 vim "$1"
13 exit 0
14 fi
15 if [ ! -r "$1" -o ! -w "$1" ]
16 then
17 echo "$script:check permissions on $1" 1>&2
18 exit 1
19 else
20 editfile="$1"
21 fi
22 if [ ! -w "." ]
23 then
24 echo "$script:backup cannont be"/
25 "created in the working directory" 1>&2
26 exit 1
27 fi
28 ;;
29 *)
30 echo "Usage:$script [file-to-edit]" 1>&2
31 exit 1
32 ;;
33 esac
34
35 tempfile=/tmp/$$.$script
36 cp $editfile $tempfile
37 if vim $editfile
38 then
39 mv $tempfile bak.$(basename $editfile)
40 echo "$script:backup file created"
41 else
42 mv $tempfile editerr
43 echo "$script:edit error--copy of" /
44 "original file is in editerr"1>&2
45 fi
46
>chmod u+x safedit
>./safedit example.sh
再另一個控制台上進入tmp 目錄可以檢視到一個nxxn.safedit 的臨時檔案,回到原來的控制台退出vim 編輯器,檢視目前控制台目前工作目錄下會生成一個備份檔案格式bak.example.sh 。再打開新的控制台檢視/tmp 目錄發現最開始生成的臨時檔案已經被删除。
>ln safedit safedit.ln
>./safedit.ln example.sh
注意觀察該指令行運作的過程,生成的中間檔案,最後結果與上面文字描述的不同。
設定 PATH 變量:該腳本設定了 PATH 變量,目的是為了保證腳本中執行的指令是系統目錄中的标準指令。避免了使用者自己可能設定的 PATH 包含自己定義的目錄,而在該自定義目錄下,使用者可能編寫了一些與腳本中調用指令同名的腳本或程式。
程式名: basename 去掉前導的目錄部分後列印名稱,是以腳本中将 script 設定為運作腳本的基本名稱。通過指令替換實作。 $0 存儲腳本被調用時的指令。好處在于對腳本進行重命名或則建立連結後運作該腳本會得到正确的提示資訊。
給臨時檔案命名:腳本中設定變量 tempfile 為臨時檔案名,以 shell 程序的 PID 号作為開始并以腳本的名字作為結束。使用 PID 号是為了確定檔案名的唯一性,腳本名字附在臨時檔案名後面,為了讓使用者知道其來源。 PID 号放在前面是由于一些老版本 unix 上對檔案名有長度限制,而 PID 号可以確定其唯一性,是以放在前面避免由于檔案名字長的限制而被切去。
測試條件:腳本中使用了一個測試條件 vim $editfile 。測試 vim 編輯器編輯檔案完成後傳回的 exit 代碼, if 控制結構就是利用這個 exit 代碼來決定分支。成功則傳回 0 , then 被執行;不成功則傳回非 0 , else 語句被執行。
select :首先顯示一個菜單,然後根據使用者的選擇給變量賦予相應的值,最後執行一系列指令。
select varname [in arg ….]
do
command
done
例子:
1 #! /bin/bash
2 PS3= 'Choose your favorite fruit from these from these possibilities:'
3 select FRUIT in apple banana blueberry kiwi orange watermelon STOP
4 do
5 if [ "$FRUIT" = "" ];then
6 echo -e "Invalid entry./n"
7 continue
8 elif [ "$FRUIT " = STOP ];then
9 echo "Thanks for playing."
10 break
11 fi
12 echo "You Chose $FRUIT as your favorite."
13 echo -e "That is choice number $REPLY ./n"
14 done
15
PS3 :是 select 特有的提示符,在 select 語句輸出菜單後,就會顯示出 PS3 的值。 slect 不斷的發出 PS3 提示并按照使用者的輸入執行指令,直到有事情使其停止。
select 将 varname 設定為輸入的值 ( 例子中是變量 FRUIT) ,同時将使用者的響應存儲在鍵盤變量 REPLY 中。如果用使用者非法輸入, shell 将 varname 設定為空 ($FRUIT) 。