shell中while循環的陷阱

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


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

一個簡單的測試腳本以下:數組

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

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

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

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

while test_cmd_list; do cmd_list; done測試

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

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

echo "abc xyz" | while read line   htm

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寫法中,使用最普遍的寫法一雖然最簡單、方便,但實際上是最爛的一種。

相關文章
相關標籤/搜索