shell中while循環的陷阱


bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html


在寫while循環的時候,發現了一個問題,在while循環內部對變量賦值、定義變量、數組定義等等環境,在循環外面失效。

一個簡單的測試腳本如下:

#!/bin/bash
echo "abc xyz" | while read line
do
    new_var=$line
done
echo new_var is null: $new_var?

執行結果證明,$new_var的結果是空值。

問題出在管道上。先看看下面的內容。

while循環的寫法有好幾種,它的語法結構為:

while test_cmd_list; do cmd_list; done

但更經常地,while循環更多地用於讀取標准輸入的內容來實現循環。有以下幾種寫法:

寫法一:使用管道傳遞內容,這是用的最多、但卻最爛的寫法

echo "abc xyz" | while read line   

do 

    ...

done

寫法二:

while read line

do

    ...

done <<< "abc xyz"

寫法三:從文件中讀取內容

while read line

do

    ...

done </path/filename

方法四:采用進程替換

while read var

do

    ...

done < <(cmd_list)           

方法五:改變標准輸入

exec <filename

while read var

do

    ...

done        

盡管寫法有多種,但它們並不等價。

陷阱一:

方法一中使用的是管道符號,這使得while語句在子shell中執行,這意味着while語句內部設置的變量、數組、函數等在循環外部都不再生效。這正是文章開頭所說的陷阱。更簡單的:echo haha | a=5,在命令執行結束后,變量a的值也不再是5。其余4種寫法,while語句都不在子shell中執行,因此都不會出現文章開頭所說的問題。

例如,使用寫法二的here string代替寫法一:

#!/bin/bash
while read line
do
    new_var=$line
done <<< "abc xyz"
echo new_var is null: $new_var?

或者使用寫法四的進程替換:

#!/bin/bash
while read line
do
    new_var=$line
done < <(echo "abc xyz")
echo new_var is null: $new_var?

陷阱二:

關於這幾種while循環的寫法,還有一點要注意:寫法一和寫法四傳遞數據的源都是一個單獨的進程,它們傳遞的數據一被while循環讀取,所有數據就丟棄了,而以實體文件作為重定向傳遞的數據,while讀取了之后並不會丟棄。更標准一些的說法是,當標准輸入是非實體文件時(如管道傳遞的、獨立進程產生的)只供一次讀取;當標准輸入是直接重定向實體文件時,可供多次讀取,但只要某一次讀取了該文件的全部內容就無法再提供讀取。

舉個例子,老師讓我們聽寫10個單詞,而我記憶力比較爛,他念完10個單詞時我可能只寫出了3個,剩余的7個因為記不住就沒法再寫出來。但如果我有小抄,我就可以慢悠悠的一個一個寫,寫了一個還可以等一段時間再寫第二個,但當我寫完10個之后,小抄這種東西就應該銷毀掉。

回到IO重定向上,無論什么數據資源,只要被讀取完畢或者主動丟棄,那么該資源就不可再得。①對於獨立進程傳遞的數據(管道左側進程產生的數據、進程替換產生的數據),它們都是"虛擬"數據,要不被一次讀取完畢,要不讀一部分剩余的丟棄,這是真正的一次性資源。②而實體文件重定向傳遞的數據,只要不是一次性被全部讀取,它就是可再得資源,直到該文件數據全部讀取結束,這是"偽"一次性資源。其實①是進程間通信時數據傳遞的現象,只不過這個問題容易被人忽略。

大多數時候,獨立進程傳遞的數據和文件直接傳遞的數據並沒有什么區別,但有些命令可以標記當前讀取到哪個位置,使得下次該命令的讀取動作可以從標記位置處恢復並繼續讀取,特別是這些命令用在循環中時。據我到目前的總結,這樣的命令有"head -n N"和"grep -m",經測試,tail並沒有位置標記的功能。

說了這么多,現在終於開始驗證。下面的循環中,本該head每次讀取2行,但實際執行結果中總共就只讀取了一次2行。

[root@xuexi ~]# i=0
[root@xuexi ~]# cat /etc/fstab | while head -n 2 ; [[ "$i" -le 3 ]];do echo $i;let ++i;done     

#
0
1
2
3

使用進程替換的結果是一樣的。

[root@xuexi ~]# i=0
[root@xuexi ~]# while head -n 2; [[ "$i" -le 3 ]];do echo $i;let ++i;done < <(cat /etc/fstab)

#
0
1
2
3

但如果是直接將實體文件進行重定向傳遞給head,則結果和上面的不一樣。

[root@xuexi ~]# i=0;while head -n 2 ; [[ "$i" -le 3 ]];do echo $i;let ++i;done < /etc/fstab

#
0
# /etc/fstab
# Created by anaconda on Thu May 11 04:17:44 2017
1
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
2
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
3
UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /                       xfs     defaults        0 0
UUID=367d6a77-033b-4037-bbcb-416705ead095 /boot                   xfs     defaults        0 0

可以看到結果中每次讀取兩行並echo一次"$i",而且每次讀取的兩行是不同的,后一次讀取的兩行是從前一次讀取結束的地方開始的,這是因為head有"讀取到指定行數后做上位置標記"的功能。

要確定命令、工具是否具有做位置標記的能力,只需像下面例子一樣做個簡單的測試。以head和sed為例,即使sed的"q"命令能讓sed匹配到內容就退出,但卻不做位置標記,而且數據資源使用一次就丟棄。

[root@xuexi ~]# (head -n 2;head -n 2) </etc/fstab 

#
# /etc/fstab
# Created by anaconda on Thu May 11 04:17:44 2017
[root@xuexi ~]# (sed -n /default/'{p;q}' ;sed -n /default/'{p;q}') </etc/fstab     
UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /                       xfs     defaults        0 0

其實在實際應用過程中,這根本就不是個問題,因為搜索和處理文本數據的工具雖然不少,但絕大多數都是用一次文本就"丟"一次,幾乎不可能因此而產生問題。之所以說這么多廢話,主要是想說上面的5種while寫法中,使用最廣泛的寫法一雖然最簡單、方便,但其實是最爛的一種。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM