循環和分支

對代碼塊的操作是構造組織shell腳本的關鍵. 循環和分支結構為腳本編程提供了操作代碼塊的工具.

10.1. Loops

循環就是重復一些命令的代碼塊,如果條件不滿足就退出循環.

for loops

for arg in [list]

這是一個基本的循環結構.它與Cfor結構有很大不同.

forarg in [list]
do
???command(s)...
done

http://manual.51yip.com/shell/common/note.png

在循環的每次執行中,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中的參數允許包含通配符.

如果dofor想在同一行出現,那么在它們之間需要添加一個";".

forarg in [list] ; do



例子 10-1. 循環的一個簡單例子

?? ?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



http://manual.51yip.com/shell/common/note.png

每個[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.



例子 10-4. for循環中操作文件

?? ?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.



例子 10-5. for循環中省略in [list]

????????????????????????????????????????????????????????????????????????????????????? ?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.



例子10-6. 使用命令替換來產生for循環的[list]

???????????????????????????????????????????????????????????????????????????????????? ?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



大部分相同.



例子 10-8. 列出系統上的所有用戶

?? ?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]的最后的例子.



例子 10-9. 在目錄的所有文件中查找源字串

?? ?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循環的語法形式.這需要使用(()).



例子 10-12. 一個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



參考例子26-15, 例子26-16, 例子A-6.

---

現在來一個現實生活中使用的for循環.



例子 10-13. batch mode中使用efax

?? ?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



while

這種結構在循環的開頭判斷條件是否滿足,如果條件一直滿足,那就一直循環下去(0退出碼[exitstatus]).for 循環的區別是,這種結構適合用在循環次數未知的情況下.

while [condition]
do
??command...
done

for循環一樣,如果想把do和條件放到同一行上還是需要一個";".

while [condition] ; do

注意一下某種特定的while循環,比如getopts結構,好像和這里所介紹的模版有點脫節.



例子 10-14. 簡單的while循環

?? ?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



例子 10-15. 另一個while循環

?? ?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循環可以有多個判斷條件,但是只有最后一個才能決定是否退出循環.然而這需要一種有點不同的循環語法.



例子 10-16. 多條件的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).



例子 10-17. C風格的while循環

?? ?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



http://manual.51yip.com/shell/common/note.png

while???????????????????????????????????????????????????????????????????????????????????????????????????? 循環的stdin可以用<重定向到文件.

whild循環的stdin支持管道.

until

這個結構在循環的頂部判斷條件,并且如果條件一直為false那就一直循環下去.(while相反).

until [condition-is-true]
do
??command...
done

注意: until循環的判斷在循環的頂部,這與某些編程語言是不同的.

for循環一樣,如果想把do和條件放在一行里,就使用";".

until [condition-is-true] ; do



例子 10-18. until循環

?? ?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/

?