1、for 命令
重復執行一系列命令在編程中很常見。通常你需要重復一組命令直至達到某個特定條件,比如處理某個目錄下的所有文件、系統上的所有用戶或是某個文本文件中的所有行。
bash shell提供了for命令,允許你創建一個遍歷一系列值的循環。每次迭代都使用其中一個值來執行已定義好的一組命令。下面是bash shell中for命令的基本格式。
1 for var in list 2 do
3 commands 4 done
在list參數中,你需要提供迭代中要用到的一系列值。可以通過幾種不同的方法指定列表中的值。
在每次迭代中,變量var會包含列表中的當前值。第一次迭代會使用列表中的第一個值,第二次迭代使用第二個值,以此類推,直到列表中的所有值都過一遍。
在do和done語句之間輸入的命令可以是一條或多條標准的bash shell命令。在這些命令中,$var變量包含着這次迭代對應的當前列表項中的值。
說明 只要你願意,也可以將do語句和for語句放在同一行,但必須用分號將其同列表中的值分開:for var in list; do。
1.1、讀取列表中的值
for命令最基本的用法就是遍歷for命令自身所定義的一系列值。
1 $ cat test1 2 #!/bin/bash 3 # basic for command 4 for test in Alabama Alaska Arizona Arkansas California Colorado 5 do
6 echo The next state is $test 7 done
8 $ ./test1 9 The next state is Alabama 10 The next state is Alaska 11 The next state is Arizona 12 The next state is Arkansas 13 The next state is California 14 The next state is Colorado 15 $
每次for命令遍歷值列表,它都會將列表中的下個值賦給$test變量。$test變量可以像for命令語句中的其他腳本變量一樣使用。在最后一次迭代后,$test變量的值會在shell腳本的剩余部分一直保持有效。它會一直保持最后一次迭代的值(除非你修改了它)。
1 $ cat test1b 2 #!/bin/bash 3 # testing the for variable after the looping 4 for test in Alabama Alaska Arizona Arkansas California Colorado 5 do
6 echo "The next state is $test"
7 done
8 echo "The last state we visited was $test"
9 test=Connecticut 10 echo "Wait, now we're visiting $test"
11 $ ./test1b 12 The next state is Alabama 13 The next state is Alaska 14 The next state is Arizona 15 The next state is Arkansas 16 The next state is California 17 The next state is Colorado 18 The last state we visited was Colorado 19 Wait, now we're visiting Connecticut
20 $
$test變量保持了其值,也允許我們修改它的值,並在for命令循環之外跟其他變量一樣使用。
1.2、讀取列表中的復雜值
事情並不會總像你在for循環中看到的那么簡單。有時會遇到難處理的數據。下面是給shell腳本程序員帶來麻煩的典型例子。
1 $ cat badtest1 2 #!/bin/bash 3 # another example of how not to use the for command 4 for test in I don't know if this'll work 5 do
6 echo "word:$test"
7 done
8 $ ./badtest1 9 word:I 10 word:dont know if thisll 11 word:work 12 $
真麻煩。shell看到了列表值中的單引號並嘗試使用它們來定義一個單獨的數據值,這真是把事情搞得一團糟。
有兩種辦法可解決這個問題:
- 使用轉義字符(反斜線)來將單引號轉義;
- 使用雙引號來定義用到單引號的值。
這兩種解決方法並沒有什么出奇之處,但都能解決這個問題。
1 $ cat test2 2 #!/bin/bash 3 # another example of how not to use the for command 4 for test in I don\'t know if "this'll" work
5 do
6 echo "word:$test"
7 done
8 $ ./test2 9 word:I 10 word:don't
11 word:know 12 word:if
13 word:this'll
14 word:work 15 $
在第一個有問題的地方添加了反斜線字符來轉義don't中的單引號。在第二個有問題的地方將this'll用雙引號圈起來。兩種方法都能正常辨別出這個值。
你可能遇到的另一個問題是有多個詞的值。記住,for循環假定每個值都是用空格分割的。如果有包含空格的數據值,你就陷入麻煩了。
1 $ cat badtest2 2 #!/bin/bash 3 # another example of how not to use the for command 4 for test in Nevada New Hampshire New Mexico New York North Carolina 5 do
6 echo "Now going to $test"
7 done
8 $ ./badtest1 9 Now going to Nevada 10 Now going to New 11 Now going to Hampshire 12 Now going to New 13 Now going to Mexico 14 Now going to New 15 Now going to York 16 Now going to North 17 Now going to Carolina 18 $
這不是我們想要的結果。for命令用空格來划分列表中的每個值。如果在單獨的數據值中有空格,就必須用雙引號將這些值圈起來。
1 $ cat test3 2 #!/bin/bash 3 # an example of how to properly define values 4 for test in Nevada "New Hampshire" "New Mexico" "New York"
5 do
6 echo "Now going to $test"
7 done
8 $ ./test3 9 Now going to Nevada 10 Now going to New Hampshire 11 Now going to New Mexico 12 Now going to New York 13 $
現在for命令可以正確區分不同值了。另外要注意的是,在某個值兩邊使用雙引號時,shell並不會將雙引號當成值的一部分。
1.3、從變量讀取列表
通常shell腳本遇到的情況是,你將一系列值都集中存儲在了一個變量中,然后需要遍歷變量中的整個列表。也可以通過for命令完成這個任務。
1 $ cat test4 2 #!/bin/bash 3 # using a variable to hold the list 4 list="Alabama Alaska Arizona Arkansas Colorado"
5 list=$list" Connecticut"
6 for state in $list 7 do
8 echo "Have you ever visited $state?"
9 done
10 $ ./test4 11 Have you ever visited Alabama?
12 Have you ever visited Alaska?
13 Have you ever visited Arizona?
14 Have you ever visited Arkansas?
15 Have you ever visited Colorado?
16 Have you ever visited Connecticut?
17 $
$list 變量包含了用於迭代的標准文本值列表。注意,代碼還是用了另一個賦值語句向 $list 變量包含的已有列表中添加(或者說是拼接)了一個值。這是向變量中存儲的已有文本字符串尾部添加文本的一個常用方法。
1.4、從命令讀取值
生成列表中所需值的另外一個途徑就是使用命令的輸出。可以用命令替換來執行任何能產生輸出的命令,然后在for命令中使用該命令的輸出。
1 $ cat test5 2 #!/bin/bash 3 # reading values from a file
4 file="states"
5 for state in $(cat $file) 6 do
7 echo "Visit beautiful $state"
8 done
9 $ cat states 10 Alabama 11 Alaska 12 Arizona 13 Arkansas 14 Colorado 15 Connecticut 16 Delaware 17 Florida 18 Georgia 19 $ ./test5 20 Visit beautiful Alabama 21 Visit beautiful Alaska 22 Visit beautiful Arizona 23 Visit beautiful Arkansas 24 Visit beautiful Colorado 25 Visit beautiful Connecticut 26 Visit beautiful Delaware 27 Visit beautiful Florida 28 Visit beautiful Georgia 29 $
這個例子在命令替換中使用了cat命令來輸出文件states的內容。你會注意到states文件中每一行有一個州,而不是通過空格分隔的。for命令仍然以每次一行的方式遍歷了cat命令的輸出,假定每個州都是在單獨的一行上。但這並沒有解決數據中有空格的問題。如果你列出了一個名字中有空格的州,for命令仍然會將每個單詞當作單獨的值。
說明 test5的代碼范例將文件名賦給變量,文件名中沒有加入路徑。這要求文件和腳本位於同一個目錄中。如果不是的話,你需要使用全路徑名(不管是絕對路徑還是相對路徑)來引用文件位置。
1.5、更改字段分隔符
造成這個問題的原因是特殊的環境變量IFS,叫作內部字段分隔符(internal field separator)。IFS環境變量定義了bash shell用作字段分隔符的一系列字符。默認情況下,bash shell會將下列字符當作字段分隔符:
- 空格
- 制表符
- 換行符
如果bash shell在數據中看到了這些字符中的任意一個,它就會假定這表明了列表中一個新數據字段的開始。在處理可能含有空格的數據(比如文件名)時,這會非常麻煩,就像你在上一個腳本示例中看到的。
要解決這個問題,可以在shell腳本中臨時更改IFS環境變量的值來限制被bash shell當作字段分隔符的字符。例如,如果你想修改IFS的值,使其只能識別換行符,那就必須這么做:
1 IFS=$'\n'
將這個語句加入到腳本中,告訴bash shell在數據值中忽略空格和制表符。對前一個腳本使用這種方法,將獲得如下輸出。
1 $ cat test5b 2 #!/bin/bash 3 # reading values from a file
4 file="states"
5 IFS=$'\n'
6 for state in $(cat $file) 7 do
8 echo "Visit beautiful $state"
9 done
10 $ ./test5b 11 Visit beautiful Alabama 12 Visit beautiful Alaska 13 Visit beautiful Arizona 14 Visit beautiful Arkansas 15 Visit beautiful Colorado 16 Visit beautiful Connecticut 17 Visit beautiful Delaware 18 Visit beautiful Florida 19 Visit beautiful Georgia 20 Visit beautiful New York 21 Visit beautiful New Hampshire 22 Visit beautiful North Carolina 23 $
現在,shell腳本舊能夠使用列表中含有空格的值了。
警告 在處理代碼量較大的腳本時,可能在一個地方需要修改IFS的值,然后忽略這次修改,在腳本的其他地方繼續沿用IFS的默認值。一個可參考的安全實踐是在改變IFS之前保存原來的IFS值,之后再恢復它。
這種技術可以這樣實現:
1 IFS.OLD=$IFS 2 IFS=$'\n'
<在代碼中使用新的IFS值>
IFS=$IFS.OLD
這就保證了在腳本的后續操作中使用的是IFS的默認值。
還有其他一些IFS環境變量的絕妙用法。假定你要遍歷一個文件中用冒號分隔的值(比如在/etc/passwd文件中)。你要做的就是將IFS的值設為冒號。
1 IFS=:
如果要指定多個IFS字符,只要將它們在賦值行串起來就行。
1 IFS=$'\n':;"
這個賦值會將換行符、冒號、分號和雙引號作為字段分隔符。如何使用IFS字符解析數據沒有任何限制。
1.6、用通配符讀取目錄
最后,可以用for命令來自動遍歷目錄中的文件。進行此操作時,必須在文件名或路徑名中使用通配符。它會強制shell使用文件擴展匹配。文件擴展匹配是生成匹配指定通配符的文件名或路徑名的過程。
如果不知道所有的文件名,這個特性在處理目錄中的文件時就非常好用。
1 $ cat test6 2 #!/bin/bash 3 # iterate through all the files in a directory 4 for file in /home/rich/test/*
5 do 6 if [ -d "$file" ] 7 then 8 echo "$file is a directory" 9 elif [ -f "$file" ] 10 then 11 echo "$file is a file" 12 fi 13 done 14 $ ./test6 15 /home/rich/test/dir1 is a directory 16 /home/rich/test/myprog.c is a file 17 /home/rich/test/myprog is a file 18 /home/rich/test/myscript is a file 19 /home/rich/test/newdir is a directory 20 /home/rich/test/newfile is a file 21 /home/rich/test/newfile2 is a file 22 /home/rich/test/testdir is a directory 23 /home/rich/test/testing is a file 24 /home/rich/test/testprog is a file 25 /home/rich/test/testprog.c is a file 26 $
for命令會遍歷/home/rich/test/*輸出的結果。該代碼用test命令測試了每個條目(使用方括號方法),以查看它是目錄(通過-d參數)還是文件(通過-f參數)(參見上一篇博文)。
注意,我們在這個例子的if語句中做了一些不同的處理:
1 if [ -d "$file" ]
在Linux中,目錄名和文件名中包含空格當然是合法的。要適應這種情況,應該將 $file 變量用雙引號圈起來。如果不這么做,遇到含有空格的目錄名或文件名時就會有錯誤產生。
1 ./test6: line 6: [: too many arguments 2 ./test6: line 9: [: too many arguments
在test命令中,bash shell會將額外的單詞當作參數,進而造成錯誤。
也可以在for命令中列出多個目錄通配符,將目錄查找和列表合並進同一個for語句。
1 $ cat test7 2 #!/bin/bash 3 # iterating through multiple directories 4 for file in /home/rich/.b* /home/rich/badtest 5 do
6 if [ -d "$file" ] 7 then
8 echo "$file is a directory"
9 elif [ -f "$file" ] 10 then
11 echo "$file is a file"
12 else
13 echo "$file doesn't exist"
14 fi
15 done
16 $ ./test7 17 /home/rich/.backup.timestamp is a file
18 /home/rich/.bash_history is a file
19 /home/rich/.bash_logout is a file
20 /home/rich/.bash_profile is a file
21 /home/rich/.bashrc is a file
22 /home/rich/badtest doesn't exist
23 $
for語句首先使用了文件擴展匹配來遍歷通配符生成的文件列表,然后它會遍歷列表中的下一個文件。可以將任意多的通配符放進列表中。
警告 注意,你可以在數據列表中放入任何東西。即使文件或目錄不存在,for語句也會嘗試處理列表中的內容。在處理文件或目錄時,這可能會是個問題。你無法知道你正在嘗試遍歷的目錄是否存在:在處理之前測試一下文件或目錄總是好的。
2、C 語言風格的for 命令
如果你從事過C語言編程,可能會對bash shell中for命令的工作方式有點驚奇。在C語言中,for循環通常定義一個變量,然后這個變量會在每次迭代時自動改變。通常程序員會將這個變量用作計數器,並在每次迭代中讓計數器增一或減一。bash的for命令也提供了這個功能。
2.1、C 語言的for 命令
C語言的for命令有一個用來指明變量的特定方法,一個必須保持成立才能繼續迭代的條件,以及另一個在每個迭代中改變變量的方法。當指定的條件不成立時,for循環就會停止。條件等式通過標准的數學符號定義。比如,考慮下面的C語言代碼:
1 for (i = 0; i < 10; i++) 2 { 3 printf("The next number is %d\n", i); 4 }
這段代碼產生了一個簡單的迭代循環,其中變量i作為計數器。第一部分將一個默認值賦給該變量。中間的部分定義了循環重復的條件。當定義的條件不成立時,for循環就停止迭代。最后一部分定義了迭代的過程。在每次迭代之后,最后一部分中定義的表達式會被執行。在本例中,i變量會在每次迭代后增一。
bash shell也支持一種for循環,它看起來跟C語言風格的for循環類似,但有一些細微的不同,其中包括一些讓shell腳本程序員困惑的東西。以下是bash中C語言風格的for循環的基本格式。
1 for (( variable assignment ; condition ; iteration process ))
C語言風格的for循環的格式會讓bash shell腳本程序員摸不着頭腦,因為它使用了C語言風格的變量引用方式而不是shell風格的變量引用方式。C語言風格的for命令看起來如下。
1 for (( a = 1; a < 10; a++ ))
注意,有些部分並沒有遵循bash shell標准的for命令:
- 變量賦值可以有空格;
- 條件中的變量不以美元符開頭;
- 迭代過程的算式未用expr命令格式。
shell開發人員創建了這種格式以更貼切地模仿C語言風格的for命令。這雖然對C語言程序員來說很好,但也會把專家級的shell程序員弄得一頭霧水。在腳本中使用C語言風格的for循環時要小心。
以下例子是在bash shell程序中使用C語言風格的for命令。
1 $ cat test8 2 #!/bin/bash 3 # testing the C-style for loop 4 for (( i=1; i <= 10; i++ )) 5 do
6 echo "The next number is $i"
7 done
8 $ ./test8 9 The next number is 1
10 The next number is 2
11 The next number is 3
12 The next number is 4
13 The next number is 5
14 The next number is 6
15 The next number is 7
16 The next number is 8
17 The next number is 9
18 The next number is 10
19 $
for 循環通過定義好的變量(本例中是變量 i )來迭代執行這些命令。在每次迭代中,$i 變量包含了 for 循環中賦予的值。在每次迭代后,循環的迭代過程會作用在變量上,在本例中,變量增一。
2.2、使用多個變量
C語言風格的for命令也允許為迭代使用多個變量。循環會單獨處理每個變量,你可以為每個變量定義不同的迭代過程。盡管可以使用多個變量,但你只能在for循環中定義一種條件。
1 $ cat test9 2 #!/bin/bash 3 # multiple variables 4 for (( a=1, b=10; a <= 10; a++, b-- )) 5 do
6 echo "$a - $b"
7 done
8 $ ./test9 9 1 - 10
10 2 - 9
11 3 - 8
12 4 - 7
13 5 - 6
14 6 - 5
15 7 - 4
16 8 - 3
17 9 - 2
18 10 - 1
19 $
變量a和b分別用不同的值來初始化並且定義了不同的迭代過程。循環的每次迭代在增加變量a的同時減小了變量b。