web-dev-qa-db-ja.com

シェル関数内で「set-e」の効果と有用性を得るにはどうすればよいですか?

set -e(または#!/bin/sh -eで始まるスクリプト)は、問題が発生した場合に自動的に爆破するのに非常に便利です。これにより、失敗する可能性のあるすべてのコマンドをエラーチェックする必要がなくなります。

関数内でこれに相当するものを取得するにはどうすればよいですか?

たとえば、エラー終了ステータスでエラーが発生するとすぐに終了する次のスクリプトがあります。

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

出力は期待どおりです。

the following command could fail:

これを関数にラップしたいと思います。

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

期待される出力:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

実際の出力:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(つまり、関数はset -eを無視しています)

これはおそらく予想される動作です。私の質問は、シェル関数内でset -eの効果と有用性を取得するにはどうすればよいですか?呼び出しごとに個別にエラーチェックを行う必要がないように設定できるようにしたいのですが、エラーが発生するとスクリプトは停止します。結果を確認するまでスタックを必要なだけ巻き戻すか、確認していない場合はスクリプト自体を終了する必要があります。これは、ネストしないことを除いて、set -eがすでに行っていることです。

私は見つけました 同じ質問 スタックオーバーフローの外で尋ねられましたが、適切な答えがありません。

35
Robie Basak

set -eのドキュメントから:

このオプションがオンの場合、シェルエラーの結果にリストされている理由のいずれかで単純なコマンドが失敗するか、終了ステータス値> 0を返し、whileuntil、またはifキーワードに続く複合リストの一部ではない場合。がANDまたはORリストの一部ではなく、!予約語が先行するパイプラインでもない場合、シェルはすぐに終了します。

あなたの場合、falseは、!およびifの一部が前に付いたパイプラインの一部です。したがって、解決策は、コードを書き直して、そうでないようにすることです。

言い換えれば、ここの関数について特別なことは何もありません。試してください:

set -e
! { false; echo hi; }
15
Roman Cheplyaka

関数定義としてサブシェルを直接使用し、set -eですぐに終了するように設定できます。これにより、set -eのスコープが関数サブシェルのみに制限され、後でset +eset -eの切り替えが回避されます。

さらに、ifテストで変数の割り当てを使用し、その結果を追加のelseステートメントにエコーすることができます。

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a
13
philz

私は最終的にこれで行きました、それは明らかにうまくいきます。最初はexportメソッドを試しましたが、スクリプトが使用するすべてのグローバル(定数)変数をエクスポートする必要があることがわかりました。

set -eを無効にしてから、set -eが有効になっているサブシェル内で関数呼び出しを実行します。サブシェルの終了ステータスを変数に保存し、set -eを再度有効にしてから、変数をテストします。

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

パイプラインの一部として、または&&コマンドリストの||の一部として(パイプまたはリストの最後のコマンドを除く)、またはとしてfを実行することはできません。 ifまたはwhileの条件、またはset -eを無視するその他のコンテキスト。 このコードもこれらのコンテキストのいずれにも含めることはできませんしたがって、これを関数で使用する場合、呼び出し元は同じサブシェル/ save-exit-statusトリックを使用する必要があります。例外のスロー/キャッチに似たセマンティクスにset -eを使用することは、制限と読みにくい構文を考えると、一般的な使用にはあまり適していません。

trap err_handler_function ERRにはset -eと同じ制限があり、失敗したコマンドでset -eが終了しないコンテキストでエラーが発生しません。

次のことがうまくいくと思うかもしれませんが、そうではありません。

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -eは、サブシェルがifの条件内にあることを記憶しているため、サブシェル内では有効になりません。サブシェルであるとそれが変わると思いましたが、別のファイルにあり、そのファイルで別のシェル全体を実行するだけで機能します。

11
antak

これは少し厄介ですが、次のことができます。

 export -f f 
 if sh -ec f;次に
 ... 

これは、シェルがexport -fをサポートしている場合に機能します(bashはサポートしています)。

これにより、スクリプトが終了することに注意してくださいnot。 fのfalseの後のエコーは実行されず、ifの本体も実行されませんが、ifの後のステートメントが実行されます。

Export -fをサポートしないシェルを使用している場合は、関数でshを実行することにより、必要なセマンティクスを取得できます。

 f(){sh -ec '
 echoこれは実行されます
 false 
 echoこれは実行されません
' 
} 
7
William Pursell

これは、設計およびPOSIX仕様によるものです。 man bashで読むことができます:

-eが無視されている状況で複合コマンドまたはシェル関数が実行される場合、-eであっても、複合コマンドまたは関数本体内で実行されるコマンドは-e設定の影響を受けません。 ] _が設定され、コマンドが失敗ステータスを返します。 -eが無視されるコンテキストで実行中に、複合コマンドまたはシェル関数が-eを設定した場合、その設定は、複合コマンドまたは関数呼び出しを含むコマンドが完了するまで効果がありません。

したがって、関数内でset -eに依存することは避けてください。

次の例を考えるとオースティングループ

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

set -eは関数内で無視されます。これは、関数が最後以外のAND-ORリスト内のコマンドであるためです。

上記の動作はPOSIXによって指定され、要求されます(参照: 望ましいアクション ):

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

3
kenorb

関数内のすべてのコマンドを&&演算子で結合します。それはそれほど問題ではなく、あなたが望む結果を与えるでしょう。

サブシェルがオプションでない場合(変数を設定するなどのクレイジーなことをする必要がある場合)、失敗する可能性のあるすべてのコマンドをチェックし、|| return $?を追加することで対処できます。これにより、関数は失敗時にエラーコードを返します。

それは醜いですが、それは機能します

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

与える

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
1
cobbal

これはあなたが尋ねたものではないことを私は知っていますが、あなたが求める行動が「make」に組み込まれていることに気付いているかもしれませんし、気付いていないかもしれません。失敗した「make」プロセスの一部は、実行を中止します。ただし、これはシェルスクリプトとはまったく異なる「プログラミング」方法です。

1
jcomeau_ictx

これを実現するには、サブシェル(角かっこ内())で関数を呼び出す必要があります。

次のようにスクリプトを記述したいと思います。

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

次に、出力は(必要に応じて)次のようになります。

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
1
lothar