一、嵌套循環
循環語句可以在循環內使用任意類型的命令,包括其他循環命令。這種循環叫作嵌套循環(nested loop)。注意,在使用嵌套循環時,你是在迭代中使用迭代,與命令運行的次數是乘積關系。不注意這點的話,有可能會在腳本中造成問題。
這里有個在for循環中嵌套for循環的簡單例子。
1 $ cat test14 2 #!/bin/bash 3 # nesting for loops 4 for (( a = 1; a <= 3; a++ )) 5 do
6 echo "Starting loop $a:"
7 for (( b = 1; b <= 3; b++ )) 8 do
9 echo " Inside loop: $b"
10 done
11 done
12 $ ./test14 13 Starting loop 1: 14 Inside loop: 1
15 Inside loop: 2
16 Inside loop: 3
17 Starting loop 2: 18 Inside loop: 1
19 Inside loop: 2
20 Inside loop: 3
21 Starting loop 3: 22 Inside loop: 1
23 Inside loop: 2
24 Inside loop: 3
25 $
這個被嵌套的循環(也稱為內部循環,inner loop)會在外部循環的每次迭代中遍歷一次它所有的值。注意,兩個循環的do和done命令沒有任何差別。bash shell知道當第一個done命令執行時是指內部循環而非外部循環。
在混用循環命令時也一樣,比如在while循環內部放置一個for循環。
1 $ cat test15 2 #!/bin/bash 3 # placing a for loop inside a while loop 4 var1=5
5 while [ $var1 -ge 0 ] 6 do
7 echo "Outer loop: $var1"
8 for (( var2 = 1; $var2 < 3; var2++ )) 9 do
10 var3=$[ $var1 * $var2 ] 11 echo " Inner loop: $var1 * $var2 = $var3"
12 done
13 var1=$[ $var1 - 1 ] 14 done
15 $ ./test15 16 Outer loop: 5
17 Inner loop: 5 * 1 = 5
18 Inner loop: 5 * 2 = 10
19 Outer loop: 4
20 Inner loop: 4 * 1 = 4
21 Inner loop: 4 * 2 = 8
22 Outer loop: 3
23 Inner loop: 3 * 1 = 3
24 Inner loop: 3 * 2 = 6
25 Outer loop: 2
26 Inner loop: 2 * 1 = 2
27 Inner loop: 2 * 2 = 4
28 Outer loop: 1
29 Inner loop: 1 * 1 = 1
30 Inner loop: 1 * 2 = 2
31 Outer loop: 0
32 Inner loop: 0 * 1 = 0
33 Inner loop: 0 * 2 = 0
34 $
同樣,shell能夠區分開內部for循環和外部while循環各自的do和done命令。
如果真的想挑戰腦力,可以混用until和while循環。
1 $ cat test16 2 #!/bin/bash 3 # using until and while loops 4 var1=3
5 until [ $var1 -eq 0 ] 6 do
7 echo "Outer loop: $var1"
8 var2=1
9 while [ $var2 -lt 5 ] 10 do
11 var3=$(echo "scale=4; $var1 / $var2" | bc) 12 echo " Inner loop: $var1 / $var2 = $var3"
13 var2=$[ $var2 + 1 ] 14 done
15 var1=$[ $var1 - 1 ] 16 done
17 $ ./test16 18 Outer loop: 3
19 Inner loop: 3 / 1 = 3.0000
20 Inner loop: 3 / 2 = 1.5000
21 Inner loop: 3 / 3 = 1.0000
22 Inner loop: 3 / 4 = .7500
23 Outer loop: 2
24 Inner loop: 2 / 1 = 2.0000
25 Inner loop: 2 / 2 = 1.0000
26 Inner loop: 2 / 3 = .6666
27 Inner loop: 2 / 4 = .5000
28 Outer loop: 1
29 Inner loop: 1 / 1 = 1.0000
30 Inner loop: 1 / 2 = .5000
31 Inner loop: 1 / 3 = .3333
32 Inner loop: 1 / 4 = .2500
33 $
外部的until循環以值3開始,並繼續執行到值等於0。內部while循環以值1開始並一直執行,只要值小於5。每個循環都必須改變在測試條件中用到的值,否則循環就會無止盡進行下去。
二、循環處理文件數據
通常必須遍歷存儲在文件中的數據。這要求結合已經講過的兩種技術:
- 使用嵌套循環
- 修改IFS環境變量
通過修改IFS環境變量,就能強制for命令將文件中的每行都當成單獨的一個條目來處理,即便數據中有空格也是如此。一旦從文件中提取出了單獨的行,可能需要再次利用循環來提取行中的數據。
典型的例子是處理/etc/passwd文件中的數據。這要求你逐行遍歷/etc/passwd文件,並將IFS變量的值改成冒號,這樣就能分隔開每行中的各個數據段了。
1 #!/bin/bash 2 # changing the IFS value 3 IFS.OLD=$IFS 4 IFS=$'\n'
5 for entry in $(cat /etc/passwd) 6 do
7 echo "Values in $entry –"
8 IFS=: 9 for value in $entry 10 do
11 echo " $value"
12 done
13 done
14 $
這個腳本使用了兩個不同的IFS值來解析數據。第一個IFS值解析出/etc/passwd文件中的單獨的行。內部for循環接着將IFS的值修改為冒號,允許你從/etc/passwd的行中解析出單獨的值。在運行這個腳本時,你會得到如下輸出。
1 Values in rich:x:501:501:Rich Blum:/home/rich:/bin/bash -
2 rich 3 x 4 501
5 501
6 Rich Blum 7 /home/rich 8 /bin/bash 9 Values in katie:x:502:502:Katie Blum:/home/katie:/bin/bash -
10 katie 11 x 12 506
13 509
14 Katie Blum 15 /home/katie 16 /bin/bash
內部循環會解析出/etc/passwd每行中的各個值。這種方法在處理外部導入電子表格所采用的逗號分隔的數據時也很方便。
三、控制循環
你可能會想,一旦啟動了循環,就必須苦等到循環完成所有的迭代。並不是這樣的。有兩個命令能幫我們控制循環內部的情況:
- break命令
- continue命令
每個命令在如何控制循環的執行方面有不同的用法。下面幾節將介紹如何使用這些命令來控制循環。
3.1、break 命令
break命令是退出循環的一個簡單方法。可以用break命令來退出任意類型的循環,包括while和until循環。
有幾種情況可以使用break命令,本節將介紹這些方法。
3.1.1、跳出單個循環
在shell執行break命令時,它會嘗試跳出當前正在執行的循環。
1 $ cat test17 2 #!/bin/bash 3 # breaking out of a for loop 4 for var1 in 1 2 3 4 5 6 7 8 9 10
5 do
6 if [ $var1 -eq 5 ] 7 then
8 break 9 fi
10 echo "Iteration number: $var1"
11 done
12 echo "The for loop is completed"
13 $ ./test17 14 Iteration number: 1
15 Iteration number: 2
16 Iteration number: 3
17 Iteration number: 4
18 The for loop is completed 19 $
for循環通常都會遍歷列表中指定的所有值。但當滿足if-then的條件時,shell會執行break命令,停止for循環。
這種方法同樣適用於while和until循環。
1 $ cat test18 2 #!/bin/bash 3 # breaking out of a while loop 4 var1=1
5 while [ $var1 -lt 10 ] 6 do
7 if [ $var1 -eq 5 ] 8 then
9 break 10 fi
11 echo "Iteration: $var1"
12 var1=$[ $var1 + 1 ] 13 done
14 echo "The while loop is completed"
15 $ ./test18 16 Iteration: 1
17 Iteration: 2
18 Iteration: 3
19 Iteration: 4
20 The while loop is completed 21 $
while循環會在if-then的條件滿足時執行break命令,終止。
3.1.2、跳出內部循環
在處理多個循環時,break命令會自動終止你所在的最內層的循環。
1 $ cat test19 2 #!/bin/bash 3 # breaking out of an inner loop 4 for (( a = 1; a < 4; a++ )) 5 do
6 echo "Outer loop: $a"
7 for (( b = 1; b < 100; b++ )) 8 do
9 if [ $b -eq 5 ] 10 then
11 break 12 fi
13 echo " Inner loop: $b"
14 done
15 done
16 $ ./test19 17 Outer loop: 1
18 Inner loop: 1
19 Inner loop: 2
20 Inner loop: 3
21 Inner loop: 4
22 Outer loop: 2
23 Inner loop: 1
24 Inner loop: 2
25 Inner loop: 3
26 Inner loop: 4
27 Outer loop: 3
28 Inner loop: 1
29 Inner loop: 2
30 Inner loop: 3
31 Inner loop: 4
32 $
內部循環里的for語句指明當變量b等於100時停止迭代。但內部循環的if-then語句指明當變量b的值等於5時執行break命令。注意,即使內部循環通過break命令終止了,外部循環依然繼續執行。
3.1.3、跳出外部循環
有時你在內部循環,但需要停止外部循環。break命令接受單個命令行參數值:
1 break n
其中n指定了要跳出的循環層級。默認情況下,n為1,表明跳出的是當前的循環。如果你將n設為2,break命令就會停止下一級的外部循環。
1 $ cat test20 2 #!/bin/bash 3 # breaking out of an outer loop 4 for (( a = 1; a < 4; a++ )) 5 do
6 echo "Outer loop: $a"
7 for (( b = 1; b < 100; b++ )) 8 do
9 if [ $b -gt 4 ] 10 then
11 break 2
12 fi
13 echo " Inner loop: $b"
14 done
15 done
16 $ ./test20 17 Outer loop: 1
18 Inner loop: 1
19 Inner loop: 2
20 Inner loop: 3
21 Inner loop: 4
22 $
注意,當shell執行了break命令后,外部循環就停止了。
3.2、continue 命令
continue命令可以提前中止某次循環中的命令,但並不會完全終止整個循環。可以在循環內部設置shell不執行命令的條件。這里有個在for循環中使用continue命令的簡單例子。
1 $ cat test21 2 #!/bin/bash 3 # using the continue command 4 for (( var1 = 1; var1 < 15; var1++ )) 5 do
6 if [ $var1 -gt 5 ] && [ $var1 -lt 10 ] 7 then
8 continue 9 fi
10 echo "Iteration number: $var1"
11 done
12 $ ./test21 13 Iteration number: 1
14 Iteration number: 2
15 Iteration number: 3
16 Iteration number: 4
17 Iteration number: 5
18 Iteration number: 10
19 Iteration number: 11
20 Iteration number: 12
21 Iteration number: 13
22 Iteration number: 14
23 $
當if-then語句的條件被滿足時(值大於5且小於10),shell會執行continue命令,跳過此次循環中剩余的命令,但整個循環還會繼續。當if-then的條件不再被滿足時,一切又回到正軌。
也可以在while和until循環中使用continue命令,但要特別小心。記住,當shell執行continue命令時,它會跳過剩余的命令。如果你在其中某個條件里對測試條件變量進行增值,問題就會出現。
1 $ cat badtest3 2 #!/bin/bash 3 # improperly using the continue command in a while loop 4 var1=0
5 while echo "while iteration: $var1"
6 [ $var1 -lt 15 ] 7 do
8 if [ $var1 -gt 5 ] && [ $var1 -lt 10 ] 9 then
10 continue 11 fi
12 echo " Inside iteration number: $var1"
13 var1=$[ $var1 + 1 ] 14 done
15 $ ./badtest3 | more
16 while iteration: 0
17 Inside iteration number: 0
18 while iteration: 1
19 Inside iteration number: 1
20 while iteration: 2
21 Inside iteration number: 2
22 while iteration: 3
23 Inside iteration number: 3
24 while iteration: 4
25 Inside iteration number: 4
26 while iteration: 5
27 Inside iteration number: 5
28 while iteration: 6
29 while iteration: 6
30 while iteration: 6
31 while iteration: 6
32 while iteration: 6
33 while iteration: 6
34 while iteration: 6
35 while iteration: 6
36 while iteration: 6
37 while iteration: 6
38 while iteration: 6
39 $
你得確保將腳本的輸出重定向到了more命令,這樣才能停止輸出。在if-then的條件成立之前,所有一切看起來都很正常,然后shell執行了continue命令。當shell執行continue命令時,它跳過了while循環中余下的命令。不幸的是,被跳過的部分正是$var1計數變量增值的地方,而這個變量又被用於while測試命令中。這意味着這個變量的值不會再變化了,從前面連續的輸出顯示中你也可以看出來。
和break命令一樣,continue命令也允許通過命令行參數指定要繼續執行哪一級循環:
1 continue n
其中n定義了要繼續的循環層級。下面是繼續外部for循環的一個例子。
1 $ cat test22 2 #!/bin/bash 3 # continuing an outer loop 4 for (( a = 1; a <= 5; a++ )) 5 do
6 echo "Iteration $a:"
7 for (( b = 1; b < 3; b++ )) 8 do
9 if [ $a -gt 2 ] && [ $a -lt 4 ] 10 then
11 continue 2
12 fi
13 var3=$[ $a * $b ] 14 echo " The result of $a * $b is $var3"
15 done
16 done
17 $ ./test22 18 Iteration 1: 19 The result of 1 * 1 is 1
20 The result of 1 * 2 is 2
21 Iteration 2: 22 The result of 2 * 1 is 2
23 The result of 2 * 2 is 4
24 Iteration 3: 25 Iteration 4: 26 The result of 4 * 1 is 4
27 The result of 4 * 2 is 8
28 Iteration 5: 29 The result of 5 * 1 is 5
30 The result of 5 * 2 is 10
31 $
其中的if-then語句:
1 if [ $a -gt 2 ] && [ $a -lt 4 ] 2 then
3 continue 2
4 fi
此處用continue命令來停止處理循環內的命令,但會繼續處理外部循環。注意,值為3的那次迭代並沒有處理任何內部循環語句,因為盡管continue命令停止了處理過程,但外部循環依然會繼續。
四、處理循環的輸出
最后,在shell腳本中,你可以對循環的輸出使用管道或進行重定向。這可以通過在done命令之后添加一個處理命令來實現。
1 for file in /home/rich/*
2 do 3 if [ -d "$file" ] 4 then 5 echo "$file is a directory" 6 elif 7 echo "$file is a file" 8 fi 9 done > output.txt
shell會將for命令的結果重定向到文件output.txt中,而不是顯示在屏幕上。考慮下面將for命令的輸出重定向到文件的例子。
1 $ cat test23 2 #!/bin/bash 3 # redirecting the for output to a file
4 for (( a = 1; a < 10; a++ )) 5 do
6 echo "The number is $a"
7 done > test23.txt 8 echo "The command is finished."
9 $ ./test23 10 The command is finished. 11 $ cat test23.txt 12 The number is 1
13 The number is 2
14 The number is 3
15 The number is 4
16 The number is 5
17 The number is 6
18 The number is 7
19 The number is 8
20 The number is 9
21 $
shell創建了文件test23.txt並將for命令的輸出重定向到這個文件。shell在for命令之后正常顯示了echo語句。
這種方法同樣適用於將循環的結果管接給另一個命令。
1 $ cat test24 2 #!/bin/bash 3 # piping a loop to another command 4 for state in "North Dakota" Connecticut Illinois Alabama Tennessee 5 do
6 echo "$state is the next place to go"
7 done | sort
8 echo "This completes our travels"
9 $ ./test24 10 Alabama is the next place to go 11 Connecticut is the next place to go 12 Illinois is the next place to go 13 North Dakota is the next place to go 14 Tennessee is the next place to go 15 This completes our travels 16 $
state值並沒有在for命令列表中以特定次序列出。for命令的輸出傳給了sort命令,該命令會改變for命令輸出結果的順序。運行這個腳本實際上說明了結果已經在腳本內部排好序了。
五、實例
現在你已經看到了shell腳本中各種循環的使用方法,來看一些實際應用的例子吧。循環是對系統數據進行迭代的常用方法,無論是目錄中的文件還是文件中的數據。下面的一些例子演示了如何使用簡單的循環來處理數據。
5.1、查找可執行文件
當你從命令行中運行一個程序的時候,Linux系統會搜索一系列目錄來查找對應的文件。這些目錄被定義在環境變量PATH中。如果你想找出系統中有哪些可執行文件可供使用,只需要掃描PATH環境變量中所有的目錄就行了。如果要徒手查找的話,就得花點時間了。不過我們可以編寫一個小小的腳本,輕而易舉地搞定這件事。
首先是創建一個for循環,對環境變量PATH中的目錄進行迭代。處理的時候別忘了設置IFS分隔符。
1 IFS=: 2 for folder in $PATH 3 do
現在你已經將各個目錄存放在了變量$folder中,可以使用另一個for循環來迭代特定目錄中的所有文件。
1 for file in $folder/*
2 do
最后一步是檢查各個文件是否具有可執行權限,你可以使用if-then測試功能來實現。
1 if [ -x $file ] 2 then
3 echo " $file"
4 fi
好了,搞定了!將這些代碼片段組合成腳本就行了。
1 $ cat test25 2 #!/bin/bash 3 # finding files in the PATH 4 IFS=: 5 for folder in $PATH 6 do
7 echo "$folder:"
8 for file in $folder/*
9 do 10 if [ -x $file ] 11 then 12 echo " $file" 13 fi 14 done 15 done 16 $
運行這段代碼時,你會得到一個可以在命令行中使用的可執行文件的列表。
1 $ ./test25 | more
2 /usr/local/bin: 3 /usr/bin: 4 /usr/bin/Mail 5 /usr/bin/Thunar 6 /usr/bin/X 7 /usr/bin/Xorg 8 /usr/bin/[ 9 /usr/bin/a2p 10 /usr/bin/abiword 11 /usr/bin/ac 12 /usr/bin/activation-client 13 /usr/bin/addr2line 14 ...
輸出顯示了在環境變量PATH所包含的所有目錄中找到的全部可執行文件,數量真是不少!
5.2、創建多個用戶賬戶
shell腳本的目標是讓系統管理員過得更輕松。如果你碰巧工作在一個擁有大量用戶的環境中,最煩人的工作之一就是創建新用戶賬戶。好在可以使用while循環來降低工作的難度。
你不用為每個需要創建的新用戶賬戶手動輸入useradd命令,而是可以將需要添加的新用戶賬戶放在一個文本文件中,然后創建一個簡單的腳本進行處理。這個文本文件的格式如下:
1 userid,user name
第一個條目是你為新用戶賬戶所選用的用戶ID。第二個條目是用戶的全名。兩個值之間使用逗號分隔,這樣就形成了一種名為逗號分隔值的文件格式(或者是.csv)。這種文件格式在電子表格中極其常見,所以你可以輕松地在電子表格程序中創建用戶賬戶列表,然后將其保存成.csv格
式,以備shell腳本讀取及處理。
要讀取文件中的數據,得用上一點shell腳本編程技巧。我們將IFS分隔符設置成逗號,並將其放入while語句的條件測試部分。然后使用read命令讀取文件中的各行。實現代碼如下:
1 while IFS=’,’ read –r userid name
read命令會自動讀取.csv文本文件的下一行內容,所以不需要專門再寫一個循環來處理。當read命令返回FALSE時(也就是讀取完整個文件時),while命令就會退出。妙極了!
要想把數據從文件中送入while命令,只需在while命令尾部使用一個重定向符就可以了。
將各部分處理過程寫成腳本如下。
1 $ cat test26 2 #!/bin/bash 3 # process new user accounts 4 input="users.csv"
5 while IFS=',' read -r userid name 6 do
7 echo "adding $userid"
8 useradd -c "$name" -m $userid 9 done < "$input"
10 $
$input變量指向數據文件,並且該變量被作為while命令的重定向數據。users.csv文件內容如下。
1 $ cat users.csv 2 rich,Richard Blum 3 christine,Christine Bresnahan 4 barbara,Barbara Blum 5 tim,Timothy Bresnahan 6 $
必須作為root用戶才能運行這個腳本,因為useradd命令需要root權限。
1 # ./test26 2 adding rich 3 adding christine 4 adding barbara 5 adding tim 6 #
來看一眼/etc/passwd文件,你會發現賬戶已經創建好了。
1 # tail /etc/passwd
2 rich:x:1001:1001:Richard Blum:/home/rich:/bin/bash 3 christine:x:1002:1002:Christine Bresnahan:/home/christine:/bin/bash 4 barbara:x:1003:1003:Barbara Blum:/home/barbara:/bin/bash 5 tim:x:1004:1004:Timothy Bresnahan:/home/tim:/bin/bash 6 #
恭喜,你已經在添加用戶賬戶這項任務上給自己省出了大量時間!
六、小結
循環是編程的一部分。bash shell提供了三種可用於腳本中的循環命令。
for命令允許你遍歷一系列的值,不管是在命令行里提供好的、包含在變量中的還是通過文件擴展匹配獲得的文件名和目錄名。
while命令使用普通命令或測試命令提供了基於命令條件的循環。只有在命令(或條件)產生退出狀態碼0時,while循環才會繼續迭代指定的一組命令。
until命令也提供了迭代命令的一種方法,但它的迭代是建立在命令(或條件)產生非零退出狀態碼的基礎上。這個特性允許你設置一個迭代結束前都必須滿足的條件。
可以在shell腳本中對循環進行組合,生成多層循環。bash shell提供了continue和break命令,允許你根據循環內的不同值改變循環的正常流程。
bash shell還允許使用標准的命令重定向和管道來改變循環的輸出。你可以使用重定向來將循環的輸出重定向到一個文件或是另一個命令。這就為控制shell腳本執行提供了豐富的功能。