如何在 Linux 上的 Bash 腳本中使用 set 和 pipefail

已發表: 2022-06-25
藍色背景下筆記本電腦屏幕上的 Linux 終端。
fatmawati achmad zaenuri/Shutterstock.com

Linux setpipefail命令指示 Bash 腳本中發生故障時會發生什麼。 除了它應該停止還是應該繼續之外,還有更多需要考慮的事情。

相關: Shell 腳本初學者指南:基礎

Bash 腳本和錯誤條件

Bash shell 腳本很棒。 它們寫得很快,而且不需要編譯。 您需要執行的任何重複或多階段操作都可以包含在一個方便的腳本中。 而且由於腳本可以調用任何標準的 Linux 實用程序,因此您不受限於 shell 語言本身的功能。

但是當您調用外部實用程序或程序時可能會出現問題。 如果失敗,外部實用程序將關閉並向 shell 發送返回碼,甚至可能向終端打印錯誤消息。 但是您的腳本將繼續處理。 也許那不是你想要的。 如果在腳本執行的早期發生錯誤,如果允許腳本的其餘部分運行,則可能會導致更嚴重的問題。

什麼是 Bash Shell,為什麼它對 Linux 如此重要?
相關什麼是 Bash Shell,為什麼它對 Linux 如此重要?

您可以在每個外部進程完成時檢查它們的返回碼,但是當進程通過管道傳輸到其他進程時,這變得很困難。 返回碼將來自管道末端的進程,而不是中間失敗的進程。 當然,腳本內部也可能發生錯誤,例如嘗試訪問未初始化的變量。

setpipefile命令讓您決定發生此類錯誤時會發生什麼。 即使錯誤發生在管道鏈的中間,它們也可以讓您檢測到錯誤。

以下是如何使用它們。

展示問題

這是一個簡單的 Bash 腳本。 它將兩行文本回顯到終端。 如果將文本複製到編輯器中並將其保存為“script-1.sh”,則可以運行此腳本。

 #!/bin/bash

echo 這將首先發生 
echo 這將發生在第二個

要使其可執行,您需要使用chmod

 chmod +x script-1.sh

如果要在計算機上運行它們,則需要在每個腳本上運行該命令。 讓我們運行腳​​本:

 ./script-1.sh 

運行一個沒有錯誤的簡單腳本。

兩行文本按預期發送到終端窗口。

讓我們稍微修改一下腳本。 我們將要求ls列出不存在的文件的詳細信息。 這將失敗。 我們將其保存為“script-2.sh”並使其可執行。

 #!/bin/bash

echo 這將首先發生
ls 虛構文件名
echo 這將發生在第二個

當我們運行這個腳本時,我們會看到來自ls的錯誤消息。

 ./script-2.sh 

運行腳本並生成失敗條件。

雖然ls命令失敗,但腳本繼續運行。 即使在腳本執行過程中出現錯誤,從腳本到 shell 的返回碼為零,這表明成功。 我們可以使用 echo 和$? 保存發送到 shell 的最後一個返回碼的變量。

 迴聲$? 

檢查最後執行的腳本的返回碼。

報告的零是腳本中第二個回顯的返回碼。 所以這個場景有兩個問題。 首先是腳本有錯誤但它繼續運行。 如果腳本的其餘部分期望或依賴於失敗的操作實際成功,這可能會導致其他問題。 第二個是如果另一個腳本或進程需要檢查這個腳本的成功或失敗,它會得到一個錯誤的讀數。

set -e 選項

如果腳本調用的任何進程生成非零返回碼, set -e (退出)選項會導致腳本退出。 任何非零都被視為失敗。

通過在腳本的開頭添加set -e選項,我們可以改變它的行為。 這是“script-3.sh”。

 #!/bin/bash 
設置-e

echo 這將首先發生
ls 虛構文件名
echo 這將發生在第二個

如果我們運行這個腳本,我們將看到set -e的效果。

 ./script-3.sh
 迴聲$? 

在錯誤條件下終止腳本,並正確設置返回碼。

腳本停止並且發送到 shell 的返回碼是一個非零值。

處理管道故障

管道增加了問題的複雜性。 來自管道命令序列的返回碼是鏈中最後一個命令的返回碼。 如果鏈條中間的命令出現故障,我們將回到第一方。 該返回碼丟失,腳本將繼續處理。

我們可以使用truefalse shell 內置函數查看具有不同返回碼的管道命令的效果。 這兩個命令只不過分別生成一個零或一的返回碼。

 真的
迴聲$?
 錯誤的
迴聲$? 

bash shell tr​​ue 和 false 內置命令。

如果我們通過管道將false轉換為true (使用false表示失敗的進程),我們會得到true的返回碼為零。

 假 | 真的
迴聲$? 

將虛假變為真實。

Bash 確實有一個名為PIPESTATUS的數組變量,它捕獲了管道鏈中每個程序的所有返回代碼。

 假 | 真實 | 假 | 真的
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}" 

使用 PIPESTATUS 查看管道鏈中所有程序的返回碼。

PIPESTATUS只保留返回代碼,直到下一個程序運行,並且試圖確定哪個返回代碼與哪個程序一起使用會很快變得非常混亂。

這就是set -o (選項)和pipefail的用武之地。這是“script-4.sh”。 這將嘗試將不存在的文件的內容通過管道傳輸到wc中。

 #!/bin/bash 
設置-e

echo 這將首先發生
貓腳本-99.sh | wc -l
echo 這將發生在第二個

正如我們所料,這失敗了。

 ./script-4.sh
 迴聲$? 

在管道鏈中運行錯誤的腳本。

第一個零是wc的輸出,告訴我們它沒有讀取丟失文件的任何行。 第二個零是第二個echo命令的返回碼。

我們將添加-o pipefail ,將其保存為“script-5.sh”,並使其可執行。

 #!/bin/bash 
設置-eo pipefail

echo 這將首先發生
貓腳本-99.sh | wc -l
echo 這將發生在第二個

讓我們運行它並檢查返回碼。

 ./script-5.sh
 迴聲$? 

運行捕獲管道鏈中的錯誤並正確設置返回碼的腳本。

腳本停止,第二個echo命令沒有執行。 發送到 shell 的返回碼是 1,正確指示失敗。

相關:如何在 Linux 上使用 Echo 命令

捕獲未初始化的變量

未初始化的變量很難在真實腳本中被發現。 如果我們嘗試echo顯未初始化變量的值, echo顯只會打印一個空行。 它不會引發錯誤消息。 腳本的其餘部分將繼續執行。

這是腳本 6.sh。

 #!/bin/bash 
設置-eo pipefail

迴聲“$notset” 
echo "另一個回顯命令"

我們將運行它並觀察它的行為。

 ./script-6.sh
 迴聲$? 

運行不捕獲未初始化變量的腳本。

腳本會跳過未初始化的變量,並繼續執行。 返回碼為零。 試圖在一個非常長而復雜的腳本中找到這樣的錯誤可能非常困難。

如何在 Bash 中使用變量
相關如何在 Bash 中使用變量

我們可以使用set -u (unset) 選項來捕獲此類錯誤。 我們將把它添加到腳本頂部不斷增長的設置選項集合中,將其保存為“script-7.sh”,並使其可執行。

 #!/bin/bash 

設置-eou pipefail

迴聲“$notset” 

echo "另一個回顯命令"

讓我們運行腳​​本:

 ./script-7.sh
 迴聲$? 

運行一個捕獲未初始化變量的腳本。

檢測到未初始化的變量,腳本停止,返回碼設置為 1。

-u (未設置)選項足夠智能,不會被您可以合法地與未初始化變量交互的情況觸發

在“script-8.sh”中,腳本檢查變量New_Var是否已初始化。 您不希望腳本停在這裡,在真實世界的腳本中,您將執行進一步的處理並自己處理情況。

請注意,我們在 set 語句中添加了-u選項作為第二個選項。 -o pipefail選項必須放在最後。

 #!/bin/bash 

設置 -eu 管道故障

如果 [ -z "${New_Var:-}" ]; 然後 

echo "New_Var 沒有賦值。" 

菲

在“script-9.sh”中,測試未初始化的變量,如果未初始化,則提供默認值。

 #!/bin/bash
設置 -eu 管道故障

默認值=484
值=${New_Var:-$default_value}
迴聲“New_Var=$Value”

允許腳本運行到完成。

 ./script-8.sh
 ./script-9.sh 

運行兩個腳本,其中未初始化的變量在內部處理,並且 -u 選項不會觸發。

用斧頭密封

另一個方便使用的選項是set -x (執行和打印)選項。 當您編寫腳本時,這可以成為救命稻草。 它在執行命令時打印命令及其參數。

它為您提供了一種快速“粗略且準備就緒”的執行跟踪形式。 隔離邏輯缺陷和發現錯誤變得非常容易。

我們將 set -x 選項添加到“script-8.sh”,將其保存為“script-10.sh”,並使其可執行。

 #!/bin/bash
設置-euxo pipefail

如果 [ -z "${New_Var:-}" ]; 然後
  echo "New_Var 沒有賦值。"
菲

運行它以查看跟踪線。

 ./script-10.sh 

運行一個腳本,將 -x 跟踪行寫入終端。

在這些簡單的示例腳本中發現錯誤很容易。 當您開始編寫更多涉及的腳本時,這些選項將證明它們的價值。