有沒有人嘗試寫一個修復 errexit 的 shell(`set -e`)?

有沒有人嘗試寫一個修復 errexit 的 shell(`set -e`)?

errexit ( set -e) 通常被建議作為一種使簡單腳本更加健壯的方法。然而,任何見過它在更複雜的腳本(尤其是函數)中的行為的人通常都會認為它是一個可怕的陷阱。子 shell 也有非常相似的問題。舉個例子,改編自https://stackoverflow.com/questions/29926013/exit-subshel​​l-on-error

(
set -o errexit
false
true
) && echo "OK" || echo "FAILED";

這裡的陷阱是 shell 顯示“OK”,而不是“FAILED”。

雖然在特定情況下的行為set -e在歷史上以不幸的方式有所不同,但現代發行版上可用的 shell 聲稱遵循 POSIX shell 標準,並且測試顯示以下所有項目都有相同的行為(“OK”):

  • bash-4.4.19-2.fc28.x86_64(軟呢帽)
  • busybox-1.26.2-3.fc27.x86_64(軟呢帽)
  • dash 0.5.8-2.4(Debian 9)
  • zsh 5.3.1-4+b2(Debian 9)
  • posh 0.12.6+b1(Debian 9)
  • ksh 93u+20120801-3.1(Debian 9)。

問題

  1. 是否有任何技術原因導致您無法編寫具有與 POSIX 類似功能的 shell sh,除了它列印FAILED上述程式碼之外?

    我希望它也能改變,以便傳回 false 的 set -e頂級表達式被認為是致命的。&&https://serverfault.com/a/847016/133475 希望它能有一些更好的習慣用法與 grep 一起使用。

    set -e
    
    # print matching lines, but it's not an error if there are no matches in the file
    (
    set +e
    grep pattern file.txt
    RET=$?
    [[ $RET == 0 || $RET == 1 ]] || exit $RET
    )
    

    我想我還假設算術語句(let內建)將以某種方式重新定義,以避免隱式地將其數值強制為真/假退出狀態。

  2. 有沒有一個 shell 的範例可以做到這一點?

    我不介意查看與 Bourne 語法不同的內容。在編寫簡短的“粘合”腳本時,我對緊湊的東西感興趣,但它也有一個緊湊的策略來檢測錯誤。我不喜歡用作&&語句分隔符,如果這意味著意外省略語句分隔符將導致錯誤被默默忽略。


[*] 編輯。這也許不是一個完美的例子。討論顯示更好的例子是將set -o errexit子 shell 移到上面。

(false; echo foo) || echo bar; echo \ $?AFAICT 這與表中的測試案例非常相似這裡,它表示它將在原始 Bourne shell 及其大多數後代上顯示“foo”(然後是“0”)。例外情況是「hist.ash」和 bash 1.14.7。

當您set -e在子 shell 中新增 - (set -e; false; echo foo) || echo bar; echo \ $?- 時,有兩個額外的例外,「SVR4 sh sun5.10」和 dash-0.3.4。

就我的問題的精神而言,我認為進入set -e子外殼會分散注意力。我主要感興趣的是在set -e腳本頂部使用的習慣用法,它也適用於子 shell(這已經是 POSIX 行為)。

Jörg Schilling 表示,對於我在這個問題中使用的範例,原始 Unix 中的 Bourne shell 會列印“FAILED”,他已將此 shell 移植到 POSIX,如下所示希利工具中的“osh”,並且該結果已於 2018 年 6 月 11 日發佈時得到驗證。也許這是基於 1)set -e位於子 shell 內部和 2) “SVR4 sh sun5.10”。 「osh」「基於 OpenSolaris 來源,因此基於 SVR4 和 SVID3」。或者也許在&& echo "OK"subshel​​l 和 之間添加會導致一些可怕的額外差異|| echo "FAILED"

我認為狡猾的外殼不能回答我的問題。您可以對函數使用子 shell(使用(代替{在子 shell 中運行該函數),並以 . 啟動每個子 shell set -e。但是,使用子 shell 會帶來無法修改全域變數的限制。至少,我還沒有看到有人捍衛它作為通用編碼風格。

答案1

POSIX標準(參見-e)比 Bash 手冊中有關errexitshell 選項的內容更清楚。

啟用此選項後,當任何命令失敗時(由於 Shell 錯誤的後果中列出的任何原因或返回大於零的退出狀態),shell 應立即退出,就像執行 exit 特殊內置實用程式一樣不帶任何參數,但以下情況除外:

  1. 多命令管道中任何單一命令的失敗都不應導致 shell 退出。僅考慮管道本身的故障。

  2. 當執行 while、until、if 或 elif 保留字(以 ! 開頭的管道)後面的複合清單時,應忽略 -e 設定。保留字,或 AND-OR 清單中除最後一個以外的任何命令。

  3. 如果子 shell 指令以外的複合指令的退出狀態是在 -e 被忽略時失敗的結果,則 -e 不適用於該指令。

此要求分別適用於 shell 環境和每個子 shell 環境。例如,在:

set -e; (false; echo one) | cat; echo two

false 指令導致子 shell 退出而不執行 echo one;然而,執行 echo 2 是因為管道的退出狀態 (false; echo one) |貓為零。

顯然,範例程式碼等效於以下偽代碼。

( LIST; ) && COMMAND || COMMAND

的退出狀態清單 為零,因為:

  • shellerrexit選項被忽略。

  • 返回狀態是指定的最後一個命令的退出狀態清單, 這裡true

因此,執行 AND 清單的第二部分:echo "OK"

答案2

不是「有人嘗試過寫…」這個問題的答案,而是關於如何完成它的一些想法。好奇是否有人可以提供回饋。

目前腳本解決方案(Python、JavaScript 和較舊的 Perl 等)中的常見方法是使用 try ... catch 區塊。預期行為是任何錯誤都會觸發異常。此行為與 $? 中捕獲錯誤的 *sh 行為衝突。顯然,「-e」未能以大多數開發人員期望的方式正確工作。無需在這裡詳細說明,因為這已經被廣泛記錄。

兩種可能的方法:

  • 創建更好的set -e:例如,set -ocatcherr具有更好的錯誤處理能力。
  • 新增try { list ; }指令,遵循現代腳本解決方案

'-ofailerr` 可以定義如下:

  • 如果任何命令在控制流之外失敗(if/while/until),則當前執行上下文應傳回 1(如果這是頂級區塊,則退出)。此變更將有效地迫使開發人員對任何失敗的命令提供錯誤處理,或明確標記帶有可以忽略的錯誤的命令。
  • 適用於簡單的腳本,有效地在未處理的錯誤上中止。

方法try { list ; }類似,只是語法不同

例子:

function foo {
    cat $* > a.txt
    wc -l a.txt
}

set -oerrfail
foo /non/existing/file    # Will  fail, without executing the "wc -l"

if foo /non/existing/file ; then
   ...
else
   # This is the "catch" block
fi

使用 try 區塊:

try {
    foo /non/existing/file
    ...
} || { catch-block }

實際上,該 try 相當於“set -oerrofail”,在“if !”下運行命令列表。 { ... } ;然後 { catch 區塊 }

相關內容