
上下文:我有一個複製文件的 Bash 腳本
function log () {
read IN
if [ "$IN" == "" ]; then
:
else
echo "$datetime"$'\t'"$IN" | tee -a logfile
fi
}
function copy () {
command cp -L --parents $@
}
...
copy -R /etc . 2>&1 | log
...
問題:當cp -L --parents -R /etc 2>&1
手動運行時,我遇到了大約 10 次失敗(符號鏈結損壞,這是預料之中的),並且整個 /etc 已被複製。
但是當腳本運行時,只報告 1 次失敗,並且 /etc 只複製到 1 次失敗發生的位置。
在嘗試排除故障時,我所做的就是2>&1
從腳本中刪除,然後複製按預期進行。
問題:是我的log
函數造成了麻煩,還是腳本編寫方式有語法問題(儘管不是破壞腳本)?
答案1
你的log
功能是罪魁禍首。它的作用是:讀取一行,如果該行不為空,則列印時間戳,然後列印該行內容。這就是它的全部作用:一旦處理完一行,它就會回來。
當cp
發出第一條錯誤訊息時,log
函數會讀取並處理它。由於log
函數隨後返回,因此管道右側的進程退出,這導致管道的讀取端關閉。當cp
發出第二個錯誤訊息時,它會嘗試寫入關閉的管道,這會導致它因SIGPIPE訊號。標準錯誤是行緩衝的(預設情況下,並且cp
不會嘗試更改它),因此緩衝不起作用。
要處理所有輸入行,您需要read
循環。
log () {
while IFS= read -r IN; do
echo "$datetime"$'\t'"$IN"
done | tee -a logfile >&2
}
我還修復了read
呼叫IFS= read -r
實際讀取一行。我刪除了對空白行的特殊處理,這是毫無意義的(輸入中不會有空行)。我認為您將其放入是為了處理輸入為空(零行輸入)時的情況,但處理該問題的正確方法是檢查命令的返回狀態read
。我還修復了log
列印其標準錯誤,因為它用於處理錯誤訊息。
看在命令的每一行輸出前添加時間戳了解其他方法來執行此操作。
請注意,將命令放在管道的左側有一個主要缺點:它導致退出狀態被忽略。因此,如果cp
失敗,您的腳本將愉快地繼續。錯誤將記錄在某處,但後續命令將正常執行,並且不會提醒您應該去閱讀日誌。在 bash、ksh 或 zsh 中,您可以設定pipefail
選項除此之外,set -e
一旦任何命令失敗,即使在管道的左側,您的腳本也會以錯誤狀態退出。
set -o errexit -o pipefail
copy … |& log
或者,使用流程替代而不是透過另一個程序透過管道傳輸錯誤輸出的管道。進程替代的注意事項與管道略有不同;中的錯誤log
會被有效地忽略,並且命令可能會在log
完成之前返回(在 bash 中,log
在後台運行)。
set -e
copy … 2> >(log)
幾乎,您不需要IFS= read -r IN
閱讀更多內容,也不可能破壞該行。