errexit を修正するシェル (`set -e`) を作成しようとした人はいますか?

errexit を修正するシェル (`set -e`) を作成しようとした人はいますか?

errexit( set -e) は、単純なスクリプトをより堅牢にする方法としてよく提案されます。しかし、より複雑なスクリプト、特に関数でどのように動作するかを見たことがある人なら、これは一般的に恐ろしい罠だと考えています。そして、非常によく似た問題がサブシェルでも見られます。次の例を見てください。https://stackoverflow.com/questions/29926013/exit-subshel​​l-on-error

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

ここでの落とし穴は、シェルが「FAILED」ではなく「OK」と表示することです。[*]

特定のケースにおけるの動作はset -e歴史的に残念ながらさまざまですが、最新のディストリビューションで利用可能なシェルは POSIX シェル標準に準拠していると主張しており、テストでは次のすべてに対して同じ動作 (「OK」) が示されました。

  • bash-4.4.19-2.fc28.x86_64(フェドーラ)
  • busybox-1.26.2-3.fc27.x86_64(フェドーラ)
  • dash 0.5.8-2.4(デビアン9)
  • zsh 5.3.1-4+b2(デビアン9)
  • posh 0.12.6+b1(デビアン9)
  • ksh 93u+20120801-3.1(Debian 9)。

質問

  1. 上記のコードshを出力する以外に、POSIX と同様の機能を備えたシェルを作成できない技術的な理由はありますか?FAILED

    set -eまた、false を返すトップレベルの&&式が致命的であるとみなさ れるように変更されることを望みます。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また、算術ステートメント (組み込み) は、数値を暗黙的に true/false 終了ステータスに強制変換することを回避する何らかの方法で再定義されると想定しています。

  2. これらのいずれかを実行するシェルの例はありますか?

    Bourne とは異なる構文のものを検討しても構いません。短い「接着」スクリプトを書くときにコンパクトでありながら、エラーを検出するためのコンパクトな戦略も備えたものに興味があります。&&ステートメント セパレーターとして を使用することは、ステートメント セパレーターを誤って省略するとエラーが黙って無視されることを意味するため、好きではありません。


set -o errexit[*] 編集。これはおそらく完璧な例ではないでしょう。議論によれば、より良い例はサブシェルの上に移動することです。

(false; echo foo) || echo bar; echo \ $?私の知る限り、これは表のテストケースと非常によく似ている。ここ、これはオリジナルの Bourne シェルとその子孫のほとんどで「foo」(そして「0」) を表示することを示しています。例外は「hist.ash」と bash 1.14.7 です。

set -eサブシェル内に追加すると(set -e; false; echo foo) || echo bar; echo \ $?、「SVR4 sh sun5.10」と dash-0.3.4 という 2 つの例外が追加されます。

私の質問の趣旨からすると、set -eサブシェル内に存在することは気が散る原因だったと思います。私の主な関心は、スクリプトの先頭で使用する慣用句にありset -e、これはサブシェルにも適用されます (これは既に POSIX の動作です)。

Jörg Schillingは、この質問で使用した例では、オリジナルのUnixのBourneシェルは「FAILED」と表示し、このシェルをPOSIXに移植したと述べています。schilytools の「osh」、そしてこの結果はリリース 2018-06-11 時点で検証済みです。おそらくこれは、1) サブシェル内にあることと、2) 「SVR4 sh sun5.10」に基づいています。「osh」は「OpenSolaris ソースに基づいており、したがって SVR4 と SVID3 に基づいています」。または、サブシェルと の間に をset -e追加することによって、恐ろしい追加の差異が発生している可能性があります。&& echo "OK"|| echo "FAILED"

schily シェルは私の質問に答えてくれないと思います。関数にサブシェルを使用することもできます ((の代わりにを使用する{と、サブシェルで関数を実行できます)。また、すべてのサブシェルを で開始することもできset -eます。ただし、サブシェルを使用すると、グローバル変数を変更できないという制限が生じます。少なくとも、これを汎用コーディング スタイルとして擁護する人を私はまだ見たことがありません。

答え1

POSIX標準(参照-e) は、シェル オプションに関する Bash マニュアルよりも明確ですerrexit

このオプションがオンの場合、コマンドが失敗すると (シェル エラーの結果に記載されているいずれかの理由により、または 0 より大きい終了ステータスを返すことにより)、シェルは、引数なしで exit 特殊組み込みユーティリティを実行した場合と同様に、直ちに終了します。ただし、次の例外があります。

  1. 複数コマンドのパイプライン内の個々のコマンドが失敗しても、シェルは終了しません。パイプライン自体の失敗のみが考慮されます。

  2. while、until、if、またはelifの予約語に続く複合リスト、!の予約語で始まるパイプライン、または最後のもの以外のAND-ORリストのコマンドを実行する場合、-e設定は無視されます。

  3. サブシェル コマンド以外の複合コマンドの終了ステータスが -e が無視されている間に失敗の結果であった場合、 -e はこのコマンドには適用されません。

この要件は、シェル環境と各サブシェル環境に個別に適用されます。たとえば、次のようになります。

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

false コマンドにより、サブシェルは echo one を実行せずに終了します。ただし、パイプライン (false; echo one) | cat の終了ステータスが 0 であるため、echo two が実行されます。

明らかに、サンプル コードは次の疑似コードと同等です。

( LIST; ) && COMMAND || COMMAND

終了ステータスリスト ゼロになるのは、次の理由による:

  • シェルerrexitオプションは無視されます。

  • 戻りステータスは、リスト、 ここtrue

したがって、AND リストの 2 番目の部分が実行されますecho "OK"

答え2

「誰か ... を書こうとした人はいますか」という質問への回答ではありませんが、どのように書けばよいかについての考えです。フィードバックを提供できる人がいるかどうか知りたいです。

現在のスクリプト ソリューション (Python、JavaScript、古い Perl など) の一般的なアプローチは、try ... catch ブロックを使用することです。想定される動作は、エラーが発生すると例外がトリガーされることです。この動作は、エラーが $? でキャプチャされる *sh の動作と競合します。明らかに、'-e' は、ほとんどの開発者が期待するとおりに動作するように正しく動作しませんでした。これはすでに詳細に文書化されているため、ここで詳しく説明する必要はありません。

2 つの可能なアプローチ:

  • より良い を作成します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 ! { ... } ; then { catch-block }」の下で実行します。

関連情報