腳本逐行讀了兩筆資料,迴圈裡的計數也有增加,走出迴圈後卻又變成 0。若寫法是 產生資料的指令 | while ...,先別急著改加法:在 Bash 常見的預設設定下,管線中的迴圈跑在另一個子 shell,改到的是那個環境裡的變數。
這種情況常出現在逐行處理網址、整理檔案清單或統計日誌的腳本。下面只用兩行示例文字,不讀寫正式站檔案。範例針對 Bash,請用 bash script.sh 執行;不要把它當成所有 sh 都支援的語法。
先重現:迴圈裡是 2,外面仍是 0
先在新的 Bash 測試環境執行。範例明確關閉 lastpipe,讓結果不受個人設定影響:
shopt -u lastpipe
count=0
printf '%s\n' alpha beta |
while IFS= read -r line; do
count=$((count + 1))
printf 'inside=%s\n' "$count"
done
printf 'outside=%s\n' "$count"
輸出依序是 inside=1、inside=2、outside=0。數值確實增加過,只是子 shell 結束時,不會把變數修改回傳給原來的 shell。把變數改成 export 也不會讓子程序把修改寫回父程序。
把輸入接到迴圈,讓計數留在原來的 shell
如果資料已經在檔案裡,將輸入重導向接在 done 後面即可。下面以你自己準備、每行一個網址的 urls.txt 為例;這段只讀檔案:
count=0
while IFS= read -r line; do
count=$((count + 1))
done < urls.txt
printf 'total=%s\n' "$count"
如果資料是另一段指令產生的,可以用 Bash 的程序替換,將那段指令的輸出交給迴圈讀取:
count=0
while IFS= read -r line; do
count=$((count + 1))
done < <(printf '%s\n' alpha beta)
printf 'total=%s\n' "$count"
這次會得到 total=2。兩個小於符號之間要有空格:第一個是迴圈的輸入重導向,後面的 <(...) 是程序替換。產生資料的指令仍在另一個執行環境,重點是 while 迴圈本身沒有放進管線。
最後一行沒有換行時,也要決定怎麼算
一般的 while read 寫法可能略過檔尾那一段沒有換行的文字。若你要把它也算成一筆,可以檢查 read 結束時是否仍讀到內容:
count=0
while IFS= read -r line || [[ -n $line ]]; do
count=$((count + 1))
done < <(printf 'alpha\nbeta')
printf 'total=%s\n' "$count"
這個例子沒有在 beta 後加換行,仍會得到 2。IFS= 保留行首尾空白,read -r 避免把反斜線當跳脫字元。這裡每個空白行也算一筆;若你的清單規則要排除空白或註解,請在迴圈內明確加上條件,而不是把「讀了幾行」當成「成功處理幾筆」。
lastpipe 能改變結果,但不是每個環境都一樣
Bash 開啟 lastpipe,而且沒有啟用工作控制時,管線最後一段可以在目前 shell 執行。因此有些腳本即使用管線,外部計數仍會增加。看到兩台機器結果不同,先查 shopt lastpipe 與工作控制設定,不要直接推論某個版本壞了。
維護既有腳本時,直接對迴圈做輸入重導向通常比較容易看出變數在哪裡修改。也要另核產生清單的指令是否成功:程序替換的產生端是非同步執行,迴圈讀到結束、印出 total,不能單憑這件事判定產生端成功。需要確認完整性的工作,可先讓產生端寫入受控的暫存清單並檢查退出狀態,確認成功後才讀取。
