對代碼塊的操作是構造組織shell腳本的關鍵. 循環和分支結構爲腳本編程提供了操作代碼塊的工具.
循環就是重複一些命令的代碼塊,如果條件不滿足就退出循環.
for arg in [list]
這是一個基本的循環結構.它與C的for結構有很大不同.
forarg in [list]
do
command(s)...
done
在循環的每次執行中,arg將順序的存取list中列出的變量.. | |
1 for arg in "$var1" "$var2" "$var3" ... "$varN" 2 # 在第1次循環中, arg = $var1 3 # 在第2次循環中, arg = $var2 4 # 在第3次循環中, arg = $var3 5 # ... 6 # 在第N次循環中, arg = $varN 7 8 # 在[list]中的參數加上雙引號是爲了防止單詞被不合理地分割. |
list中的參數允許包含通配符.
如果do和for想在同一行出現,那麼在它們之間需要添加一個";".
forarg in [list] ; do
1 #!/bin/bash 2 # 列出所有的行星名稱. 3 4 for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto 5 do 6 echo $planet # 每個行星被單獨打印在一行上. 7 done 8 9 echo 10 11 for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto" 12 # 所有的行星名稱打印在同一行上. 13 # 整個'list'只有一個變量,用""封成一個變量. 14 do 15 echo $planet 16 done 17 18 exit 0 |
每個[list]中的元素都可能包含多個參數.在處理參數組時,這是非常有用的.在這種情況下,使用set命令(見例子 11-15)來強制解析每個[list]中的元素,並且分配每個解析出來的部分到一個位置參數中.
|
例子 10-2. 每個[list]元素帶兩個參數的for循環
1 #!/bin/bash 2 # 再訪行星. 3 4 # 分配行星的名字和它距太陽的距離. 5 6 for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483" 7 do 8 set -- $planet # Parses variable "planet" and sets positional parameters. 9 # "--" 將防止$planet爲空,或者是以一個破折號開頭. 10 11 # 可能需要保存原始的位置參數,因爲它們被覆蓋了. 12 # 一種方法就是使用數組, 13 # original_params=("$@") 14 15 echo "$1 $2,000,000 miles from the sun" 16 #-------two tabs---把後邊的0和$2連接起來 17 done 18 19 # (Thanks, S.C., for additional clarification.) 20 21 exit 0 |
可以在for循環中的[list]位置放入一個變量.
例子 10-3. 文件信息: 對包含在變量中的文件列表進行操作
1 #!/bin/bash 2 # fileinfo.sh 3 4 FILES="/usr/sbin/accept 5 /usr/sbin/pwck 6 /usr/sbin/chroot 7 /usr/bin/fakefile 8 /sbin/badblocks 9 /sbin/ypbind" # 你關心的文件列表. 10 # 扔進去一個假文件, /usr/bin/fakefile. 11 12 echo 13 14 for file in $FILES 15 do 16 17 if [ ! -e "$file" ] # 檢查文件是否存在. 18 then 19 echo "$file does not exist."; echo 20 continue # 繼續下一個. 21 fi 22 23 ls -l $file | awk '{ print $9 " file size: " $5 }' # 打印2個域. 24 whatis `basename $file` # 文件信息. 25 # 注意whatis數據庫需要提前建立好. 26 # 要想達到這個目的, 以root身份運行/usr/bin/makewhatis. 27 echo 28 done 29 30 exit 0 |
如果在for循環的[list]中有通配符(*和?),那將會產生文件名擴展,也就是文件名擴展(globbing).
1 #!/bin/bash 2 # list- glob.sh: 在for循環中使用文件名擴展產生 [list] 3 4 echo 5 6 for file in * 7 # ^ 在表達式中識別文件擴展符時, 8 #+ Bash 將執行文件名擴展. 9 do 10 ls -l "$file" # Lists all files in $PWD (current directory). 11 # 回想一下,通配符"*"能夠匹配所有文件, 12 #+ 然而,在"文件擴展符"中,是不能匹配"."文件的. 13 14 # 如果沒匹配到任何文件,那它將擴展成自己 15 # 爲了不讓這種情況發生,那就設置nullglob選項 16 #+ (shopt -s nullglob). 17 # Thanks, S.C. 18 done 19 20 echo; echo 21 22 for file in [jx]* 23 do 24 rm -f $file # 只刪除當前目錄下以"j"或"x"開頭的文件. 25 echo "Removed file \"$file\"". 26 done 27 28 echo 29 30 exit 0 |
在一個for循環中忽略in [list]部分的話,將會使循環操作$@(從命令行傳遞給腳本的參數列表).一個非常好的例子,見例子A-16.
1 #!/bin/bash 2 3 # 使用兩種方法來調用這個腳本,一種是帶參數的情況,另一種不帶參數. 4 #+ 觀察此腳本的行爲各是什麼樣的? 5 6 for a 7 do 8 echo -n "$a " 9 done 10 11 # 沒有[list],所以循環將操作'$@' 12 #+ (包括空白的命令參數列表). 13 14 echo 15 16 exit 0 |
也可以使用命令替換(command substitution)來產生for循環的[list].具體見例子12-49, 例子10-10 和例子12-43.
1 #!/bin/bash 2 # for-loopcmd.sh: 帶[list]的for循環 3 #+ [list]是由命令替換產生的. 4 5 NUMBERS="9 7 3 8 37.53" 6 7 for number in `echo $NUMBERS` # for number in 9 7 3 8 37.53 8 do 9 echo -n "$number " 10 done 11 12 echo 13 exit 0 |
下邊是一個用命令替換來產生[list]的更復雜的例子.
例子 10-7. 對於二進制文件的grep替換
1 #!/bin/bash 2 # bin-grep.sh: 在一個二進制文件中定位匹配字串. 3 4 # 對於二進制文件的一個grep替換 5 # 與"grep -a"的效果相似 6 7 E_BADARGS=65 8 E_NOFILE=66 9 10 if [ $# -ne 2 ] 11 then 12 echo "Usage: `basename $0` search_string filename" 13 exit $E_BADARGS 14 fi 15 16 if [ ! -f "$2" ] 17 then 18 echo "File \"$2\" does not exist." 19 exit $E_NOFILE 20 fi 21 22 23 IFS="\n" # 由Paulo Marcel Coelho Aragao提出的建議. 24 for word in $( strings "$2" | grep "$1" ) 25 # "strings" 命令列出二進制文件中的所有字符串. 26 # 輸出到管道交給"grep",然後由grep命令來過濾字符串. 27 do 28 echo $word 29 done 30 31 # S.C. 指出, 行23 - 29 可以被下邊的這行來代替, 32 # strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]' 33 34 35 # 試試用"./bin-grep.sh mem /bin/ls"來運行這個腳本. 36 37 exit 0 |
大部分相同.
1 #!/bin/bash 2 # userlist.sh 3 4 PASSWORD_FILE=/etc/passwd 5 n=1 # User number 6 7 for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" ) 8 # 域分隔 = : ^^^^^^ 9 # 打印出第一個域 ^^^^^^^^ 10 # 從password文件中取得輸入 ^^^^^^^^^^^^^^^^^ 11 do 12 echo "USER #$n = $name" 13 let "n += 1" 14 done 15 16 17 # USER #1 = root 18 # USER #2 = bin 19 # USER #3 = daemon 20 # ... 21 # USER #30 = bozo 22 23 exit 0 24 25 # 練習: 26 # -------- 27 # 一個普通用戶(或者是一個普通用戶運行的腳本) 28 #+ 怎麼能讀取/etc/password呢? 29 # 這是否是一個安全漏洞? 爲什麼是?爲什麼不是? |
關於用命令替換來產生[list]的最後的例子.
1 #!/bin/bash 2 # findstring.sh: 3 # 在一個指定目錄的所有文件中查找一個特定的字符串. 4 5 directory=/usr/bin/ 6 fstring="Free Software Foundation" # 查看那個文件中包含FSF. 7 8 for file in $( find $directory -type f -name '*' | sort ) 9 do 10 strings -f $file | grep "$fstring" | sed -e "s%$directory%%" 11 # 在"sed"表達式中, 12 #+ 我們必須替換掉正常的替換分隔符"/", 13 #+ 因爲"/"碰巧是我們需要過濾的字串之一. 14 # 如果不用"%"代替"/"作爲分隔符,那麼這個操作將失敗,並給出一個錯誤消息.(試試) 15 done 16 17 exit 0 18 19 # 練習 (容易): 20 # --------------- 21 # 將內部用的$directory和$fstring變量,用從 22 #+ 命令行參數代替. |
for循環的輸出也可以通過管道傳遞到一個或多個命令中.
例子 10-10. 列出目錄中所有的符號連接(symboliclinks)
1 #!/bin/bash 2 # symlinks.sh: 列出目錄中所有的符號連接文件. 3 4 5 directory=${1-`pwd`} 6 # 如果沒有其他的特殊指定, 7 #+ 默認爲當前工作目錄. 8 # 下邊的代碼塊,和上邊這句等價. 9 # ---------------------------------------------------------- 10 # ARGS=1 # 需要一個命令行參數. 11 # 12 # if [ $# -ne "$ARGS" ] # 如果不是一個參數的話... 13 # then 14 # directory=`pwd` # 當前工作目錄 15 # else 16 # directory=$1 17 # fi 18 # ---------------------------------------------------------- 19 20 echo "symbolic links in directory \"$directory\"" 21 22 for file in "$( find $directory -type l )" # -type l 就是符號連接文件 23 do 24 echo "$file" 25 done | sort # 否則列出的文件將是未排序的 26 # 嚴格上說,此處並不一定非要一個循環不可, 27 #+ 因爲"find"命令的結果將被擴展成一個單詞. 28 # 然而,這種方式很容易理解和說明. 29 30 # Dominik 'Aeneas' Schnitzer 指出, 31 #+ 如果沒將 $( find $directory -type l )用""引用起來的話 32 #+ 那麼將會把一個帶有空白部分的文件名拆成以空白分隔的兩部分(文件名中允許有空白). 33 # 即使這隻將取出每個參數的第一個域. 34 35 exit 0 36 37 38 # Jean Helou 建議使用下邊的方法: 39 40 echo "symbolic links in directory \"$directory\"" 41 # 當前IFS的備份.要小心使用這個值. 42 OLDIFS=$IFS 43 IFS=: 44 45 for file in $(find $directory -type l -printf "%p$IFS") 46 do # ^^^^^^^^^^^^^^^^ 47 echo "$file" 48 done|sort |
循環的輸出可以重定向到文件中,我們對上邊的例子做了一點修改.
例子 10-11. 將目錄中的符號連接文件名保存到一個文件中
1 #!/bin/bash 2 # symlinks.sh: 列出目錄中所有的符號連接文件. 3 4 OUTFILE=symlinks.list # 保存的文件 5 6 directory=${1-`pwd`} 7 # 如果沒有其他的特殊指定, 8 #+ 默認爲當前工作目錄. 9 10 11 echo "symbolic links in directory \"$directory\"" > "$OUTFILE" 12 echo "---------------------------" >> "$OUTFILE" 13 14 for file in "$( find $directory -type l )" # -type l 爲尋找類型爲符號鏈接的文件 15 do 16 echo "$file" 17 done | sort >> "$OUTFILE" # 循環的輸出 18 # ^^^^^^^^^^^^^ 重定向到一個文件中. 19 20 exit 0 |
有一種非常像C語言的for循環的語法形式.這需要使用(()).
1 #!/bin/bash 2 # 兩種循環到10的方法. 3 4 echo 5 6 # 標準語法. 7 for a in 1 2 3 4 5 6 7 8 9 10 8 do 9 echo -n "$a " 10 done 11 12 echo; echo 13 14 # +==========================================+ 15 16 # 現在, 讓我們用C風格的語法做同樣的事. 17 18 LIMIT=10 19 20 for ((a=1; a <= LIMIT ; a++)) # 雙圓括號, 並且"LIMIT"變量前邊沒有 "$". 21 do 22 echo -n "$a " 23 done # 這是一個借用'ksh93'的結構. 24 25 echo; echo 26 27 # +=========================================================================+ 28 29 # 讓我們使用C的逗號操作符,來同時增加兩個變量的值. 30 31 for ((a=1, b=1; a <= LIMIT ; a++, b++)) # 逗號將同時進行2條操作. 32 do 33 echo -n "$a-$b " 34 done 35 36 echo; echo 37 38 exit 0 |
---
現在來一個現實生活中使用的for循環.
1 #!/bin/bash 2 # Faxing ('fax' 必須已經被安裝過了). 3 4 EXPECTED_ARGS=2 5 E_BADARGS=65 6 7 if [ $# -ne $EXPECTED_ARGS ] 8 # 檢查命令行參數的個數是否正確. 9 then 10 echo "Usage: `basename $0` phone# text-file" 11 exit $E_BADARGS 12 fi 13 14 15 if [ ! -f "$2" ] 16 then 17 echo "File $2 is not a text file" 18 exit $E_BADARGS 19 fi 20 21 22 fax make $2 # 從文本文件中創建傳真格式的文件. 23 24 for file in $(ls $2.0*) # 連接轉換過的文件. 25 # 在變量列表中使用通配符. 26 do 27 fil="$fil $file" 28 done 29 30 efax -d /dev/ttyS3 -o1 -t "T$1" $fil # 幹活的地方. 31 32 33 # S.C. 指出, 通過下邊的命令可以省去for循環. 34 # efax -d /dev/ttyS3 -o1 -t "T$1" $2.0* 35 # 但這並不十分有講解意義[嘿嘿]. 36 37 exit 0 |
這種結構在循環的開頭判斷條件是否滿足,如果條件一直滿足,那就一直循環下去(0爲退出碼[exitstatus]).與for 循環的區別是,這種結構適合用在循環次數未知的情況下.
while [condition]
do
command...
done
和for循環一樣,如果想把do和條件放到同一行上還是需要一個";".
while [condition] ; do
注意一下某種特定的while循環,比如getopts結構,好像和這裏所介紹的模版有點脫節.
1 #!/bin/bash 2 3 var0=0 4 LIMIT=10 5 6 while [ "$var0" -lt "$LIMIT" ] 7 do 8 echo -n "$var0 " # -n 將會阻止產生新行. 9 # ^ 空格,數字之間的分隔. 10 11 var0=`expr $var0 + 1` # var0=$(($var0+1)) 也可以. 12 # var0=$((var0 + 1)) 也可以. 13 # let "var0 += 1" 也可以. 14 done # 使用其他的方法也行. 15 16 echo 17 18 exit 0 |
1 #!/bin/bash 2 3 echo 4 # 等價於: 5 while [ "$var1" != "end" ] # while test "$var1" != "end" 6 do 7 echo "Input variable #1 (end to exit) " 8 read var1 # 爲什麼不使用'read $var1'? 9 echo "variable #1 = $var1" # 因爲包含"#"字符,所以需要"" . . . 10 # 如果輸入爲'end',那麼就在這裏打印. 11 # 不在這裏判斷結束,在循環頂判斷. 12 echo 13 done 14 15 exit 0 |
一個while循環可以有多個判斷條件,但是隻有最後一個才能決定是否退出循環.然而這需要一種有點不同的循環語法.
1 #!/bin/bash 2 3 var1=unset 4 previous=$var1 5 6 while echo "previous-variable = $previous" 7 echo 8 previous=$var1 9 [ "$var1" != end ] # 記錄之前的$var1. 10 # 這個"while"循環中有4個條件, 但是隻有最後一個能控制循環. 11 # 退出狀態由第4個條件決定. 12 do 13 echo "Input variable #1 (end to exit) " 14 read var1 15 echo "variable #1 = $var1" 16 done 17 18 # 嘗試理解這個腳本的運行過程. 19 # 這裏還是有點小技巧的. 20 21 exit 0 |
與for循環一樣,while循環也可通過(())來使用C風格語法.(見例子9-30).
1 #!/bin/bash 2 # wh-loopc.sh: 循環10次的while循環. 3 4 LIMIT=10 5 a=1 6 7 while [ "$a" -le $LIMIT ] 8 do 9 echo -n "$a " 10 let "a+=1" 11 done # 到目前爲止都沒什麼令人驚奇的地方. 12 13 echo; echo 14 15 # +=================================================================+ 16 17 # 現在, 重複C風格的語法. 18 19 ((a = 1)) # a=1 20 # 雙圓括號允許賦值兩邊的空格,就像C語言一樣. 21 22 while (( a <= LIMIT )) # 雙圓括號, 變量前邊沒有"$". 23 do 24 echo -n "$a " 25 ((a += 1)) # let "a+=1" 26 # Yes, 看到了吧. 27 # 雙圓括號允許像C風格的語法一樣增加變量的值. 28 done 29 30 echo 31 32 # 現在,C程序員可以在Bash中找到回家的感覺了吧. 33 34 exit 0 |
while 循環的stdin可以用<來重定向到文件. whild循環的stdin支持管道. |
這個結構在循環的頂部判斷條件,並且如果條件一直爲false那就一直循環下去.(與while相反).
until [condition-is-true]
do
command...
done
注意: until循環的判斷在循環的頂部,這與某些編程語言是不同的.
與for循環一樣,如果想把do和條件放在一行裏,就使用";".
until [condition-is-true] ; do
1 #!/bin/bash 2 3 END_CONDITION=end 4 5 until [ "$var1" = "$END_CONDITION" ] 6 # 在循環的頂部判斷條件. 7 do 8 echo "Input variable #1 " 9 echo "($END_CONDITION to exit)" 10 read var1 11 echo "variable #1 = $var1" 12 echo 13 done 14 15 exit 0 轉自 shell在線中文手冊 http://manual.51yip.com/shell/ |