なぜ-eが括弧()の後にORリスト||が付いたサブシェル内では機能しないのですか?


30

最近、次のようなスクリプトに遭遇しました。

( set -e ; do-stuff; do-more-stuff; ) || echo failed

これは私には問題ありませんが、機能しません!set -eあなたが追加した場合、適用されません||。それがなければ、うまくいきます:

$ ( set -e; false; echo passed; ); echo $?
1

ただし、を追加する||と、set -e無視されます:

$ ( set -e; false; echo passed; ) || echo failed
passed

実際の独立したシェルを使用すると、期待どおりに機能します。

$ sh -c 'set -e; false; echo passed;' || echo failed
failed

複数の異なるシェル(bash、dash、ksh93)でこれを試しましたが、すべて同じように動作するため、バグではありません。誰かがこれを説明できますか?


`(....)` `構造は、その内容を実行するために別のシェルを起動します。その中の設定は外部には適用されません。
フォンブランド

@vonbrand、あなたはポイントを逃しました。彼はそれをサブシェルの内側に適用したいのですが||、サブシェルの外側はサブシェルの内側の動作に影響を与えます。
cjm

1
と比較(set -e; echo 1; false; echo 2)して(set -e; echo 1; false; echo 2) || echo 3
ヨハン

回答:


32

このスレッドによると、これはPOSIX set -eがサブシェルで「」を使用するために指定する動作です。

(私も驚いた。)

まず、動作:

-eしばらく以下の化合物のリストを実行するとき場合、または予約語のelif、パイプラインが始まるまでの設定は、無視されなければなりません!予約語、または最後以外のAND-ORリストのコマンド。

2番目の投稿ノート、

要約すると、-e in(サブシェルコード)を周囲のコンテキストとは無関係に動作させるべきではありませんか?

いいえ。POSIXの説明は、周囲のコンテキストがサブシェルでset -eが無視されるかどうかに影響することは明らかです。

4番目の投稿には、Eric Blakeによるもう少しの記事があります

ポイント3は、無視されるコンテキストをオーバーライドするためにサブシェルを必要としませんset -e。つまり、-e無視されるコンテキストにいると、サブシェルでなくても、従うためにできることはもありませ-e

$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' 
hi 
0 

set -eサブシェル-eが無視されるコンテキスト(ifステートメントの条件)に存在するという事実(親とサブシェルの両方で)を2回呼び出しましたが、サブシェルで再度有効にすることはできません。-e

この動作は間違いなく驚くべきことです。それは直観に反します。再有効化がset -e効果を持つことを期待し、周囲のコンテキストが先例をとらないことを期待するでしょう。さらに、POSIX標準の文言はこれを特に明確にしません。コマンドが失敗するコンテキストでそれを読むと、ルールは適用されません。周囲のコンテキストにのみ適用されますが、完全に適用されます。


それらのリンクのおかげで、とても興味深いものでした。ただし、私の例は(IMO)とは実質的に異なります。その議論のほとんどは、親シェルのset -eがサブシェルに継承されるかどうかですset -e; (false; echo passed;) || echo failed。実際、標準の文言を考えると、この場合-eが無視されることは驚きではありません。ただし、私の場合、サブシェルに-e 明示的に設定し、失敗時にサブシェルが終了することを期待しています。サブシェルにはAND-ORリストはありません...
MadScientist

同意しません。2番目の投稿(アンカーを機能させることができません)は、「POSIXの説明は、周囲のコンテキストがset -eがサブシェルで無視されるかどうかに影響することは明らかです。」-サブシェルはAND-ORリストにあります
アーロンD.マラスコ

4番目の投稿(Erik Blakeも)は、「set -eを2回(親とサブシェルの両方で)呼び出しましたが、-eが無視されるコンテキストにサブシェルが存在するという事実(ifの条件ステートメント)、サブシェルで-eを再度有効にするためにできることは何もありません。 "
アーロンD.マラスコ

あなたが正しい; 私はそれらをどのように誤読したかわかりません。ありがとう。
MadScientist

1
私が髪を引き裂くこの行動がPOSIX仕様にあることが判明したことを私は喜んで学びます。それでは、回避策は何ですか?!ifそして||&&感染性ですか?これはばかげています
スティーブンルー

7

実際、set -eサブシェルの||後に演算子を使用した場合、サブシェル内では効果がありません。たとえば、これは機能しません:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

アーロン・D・マラスコはその答えで、なぜこのように振る舞うのかを説明する素晴らしい仕事をしています。

これを修正するために使用できる小さなトリックを次に示します。バックグラウンドで内部コマンドを実行し、すぐにそれを待ちます。wait組み込みは、内部コマンドの終了コードを返し、今は使っている||の後にwaitそうではなく、内部機能、set -e後者の内部で適切に動作します:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

このアイデアに基づいた汎用関数を次に示します。localキーワードを削除した場合、つまり、すべてlocal x=yをjustに置き換えた場合、すべてのPOSIX互換シェルで動作するはずですx=y

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

使用例:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

例の実行:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

このメソッドを使用する際に注意する必要があるのはrun、コマンドがサブシェルで実行されるため、渡すコマンドから行われたシェル変数のすべての変更が呼び出し関数に伝播しないことだけです。


2

いくつかのシェルがそのように動作するからといって、それがバグであることを除外しません。;-)

より多くの楽しみを提供します:

start cmd:> ( eval 'set -e'; false; echo passed; ) || echo failed
passed

start cmd:> ( eval 'set -e; false'; echo passed; ) || echo failed
failed

start cmd:> ( eval 'set -e; false; echo passed;' ) || echo failed
failed

man bash(4.2.24)から引用できますか:

失敗したコマンドが[&]または||で実行されたコマンドの一部である場合、シェルは終了しません。最後の&&または||に続くコマンドを除くリスト [...]

おそらく、いくつかのコマンドに対する評価は、||を無視することにつながります。コンテキスト。


まあ、もしすべてのシェルがそのように振る舞うなら、それは定義ではバグではありません...それは標準的な振る舞いです:-)。動作が直感的ではないと嘆くかもしれませんが、... evalのトリックは非常に興味深いものです。
MadScientist

どのシェルを使用していますか?evalトリックは私のために動作しません。bash、posixモードでのbash、dashを試しました。
ドゥナトタトス

@Dunatotatosは、Haukeが言ったように、bash4.2でした。これは、bash4.3で「修正」されました。pdkshベースのシェルにも同じ「問題」があります。また、いくつかのシェルのいくつかのバージョンには、さまざまな「問題」がありset -eます。set -e設計上壊れています。制御構造、サブシェル、またはコマンド置換のない最も単純なシェルスクリプト以外には使用しません。
ステファンシャゼラス

1

トップレベルを使用する場合の回避策 set -e

set -eエラー検出方法として使用していたため、私はこの質問に来ました:

/usr/bin/env bash
set -e
do_stuff
( take_best_sub_action_1; take_best_sub_action_2 ) || do_worse_fallback
do_more_stuff

そしてなし||では、スクリプトは実行を停止し、到達しませんdo_more_stuff

きれいな解決策がないように思えるので、私はset +e自分のスクリプトで単純なことをするだけだと思います:

/usr/bin/env bash
set -e
do_stuff
set +e
( take_best_sub_action_1; take_best_sub_action_2 )
exit_status=$?
set -e
if [ "$exit_status" -ne 0 ]; then
  do_worse_fallback
fi
do_more_stuff
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.