while讀取文件 Shell中while循環的陷阱, 變量實效, 無法賦值變量

 

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

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

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

 

  1. 執行結果證明,$new_var的結果是空值。在google上查了查,才發現問題出在管道上。加上自己的總結,特分享兩個使用while循環時的陷阱。

先看看下面的內容。

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

while test_cmd_list; do cmd_list; done

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

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

echo "abc xyz" | while read line   
 
do 
 
    ...
 
done

 

寫法二:

  1. while read line
     
    do
     
        ...
     
    done <<< "abc xyz"

     

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

  1. while read line
     
    do
     
        ...
     
    done </path/filename

     

方法四:採用進程替換

  1. while read var
     
    do
     
        ...
     
    done < <(cmd_list)

     

方法五:改變標準輸入

  1. exec <filename
     
    while read var
     
    do
     
        ...
     
    done

     

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

 

陷阱一:

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

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

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

     

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

  1. #!/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並沒有位置標記的功能,因爲tail讀取的是後幾行,所以它必然要讀取到最後一行並計算要輸出的行,所以tail的性能比head要差。

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

  1. [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

     

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

  1. [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,則結果和上面的不一樣。

  1. [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匹配到內容就退出,但卻不做位置標記,而且數據資源使用一次就丟棄。

  1. [root@xuexi ~]# (head -n 2;head -n 2) </etc/fstab 
     
    #
    # /etc/fstab
    # Created by anaconda on Thu May 11 04:17:44 2017

     

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

 

轉載地址:http://justcode.ikeepstudying.com/2018/02/shell%EF%BC%9A-shell%E4%B8%ADwhile%E5%BE%AA%E7%8E%AF%E7%9A%84%E9%99%B7%E9%98%B1-%E5%8F%98%E9%87%8F%E5%AE%9E%E6%95%88-%E6%97%A0%E6%B3%95%E8%B5%8B%E5%80%BC%E5%8F%98%E9%87%8F/

 

https://blog.csdn.net/tanghuan0827/article/details/110962339

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章