終了状態を別のプロセスにパイプにインポートする

終了状態を別のプロセスにパイプにインポートする

パイプを介して接続された2つのプロセスfooがあります。bar

$ foo | bar

bar常に0で終了します。終了コードに興味がありますfoo。それを得る方法はありますか?

答え1

bashまた、zshシェルが実行した最後のパイプラインの各要素(コマンド)の終了状態を保持する配列変数があります。

を使用すると、配列がbash呼び出されPIPESTATUS(大文字と小文字が区別されます)、配列インデックスは0から始まります。

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

を使用すると、配列がzsh呼び出されpipestatus(大文字と小文字が区別されます)、配列インデックスは1から始まります。

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

値を失うことなく関数から結合するには、次のようにします。

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

bash上記のコードを or で実行すると、and の 1 つだけをzsh設定すると同じ結果が得られますretval_bashretval_zshもう一つは空です。これにより、関数は次のようになりますreturn $retval_bash $retval_zsh(引用符がないことに注意してください!)。

答え2

これを達成する3つの一般的な方法があります。

パイプ故障

最初の方法はpipefailオプション(ksh、、zshまたはbash)を設定することです。これが最も簡単です。デフォルトでは、終了ステータスを$?最後のプログラムの終了コードに設定し、ゼロ以外の値(またはすべて正常に終了した場合は0)で終了します。

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$パイプラインステータス

Bashには、最後のパイプラインのすべてのプログラムの終了ステータスを含む$PIPESTATUS$pipestatusin)という配列変数もあります。zsh

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

3番目のコマンド例を使用して、パイプラインから目的の特定の値を取得できます。

別の処刑

これは最も不器用な解決策です。各コマンドを個別に実行し、状態をキャプチャします。

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

答え3

このソリューションは、bash固有の機能や一時ファイルを使用せずに機能します。ボーナス:最終終了ステータスは、実際にはファイルの一部の文字列ではなく終了ステータスです。

状態:

someprog | filter

希望の終了状態someprogと出力filter

これが私の解決策です。

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

この構成の結果は、filterコンストラクタの stdout の stdout であり、someprogコンストラクタの終了状態の終了状態です。


{...}この構造は、サブシェルではなく単純なコマンドのグループ化にも機能します(...)。サブシェルには、ここでは不要なパフォーマンスコストを含むいくつかの意味があります。詳細についてはnice bashのマニュアルを読んでください:https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

残念ながら、bash構文には構造がより広くなるように中括弧内にスペースとセミコロンが必要です。

この記事の残りの部分では、サブシェルバ​​リアントを使用します。


someprogfilter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

出力例:

filtered line1
filtered line2
filtered line3
42

注:子プロセスは、親プロセスから開かれたファイル記述子を継承します。これはsomeprog、開かれたファイル記述子3と4が継承されることを意味します。ファイル記述子3に書き込むと、someprogこれが終了状態になります。実際の終了状態はread一度だけ読み取られるので無視されます。

ファイル記述子3または4に書き込む可能性が懸念される場合は、呼び出すsomeprog前にファイル記述子を閉じることをお勧めしますsomeprog

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

before は実行exec 3>&- 4>&-前にsomeprogファイル記述子を閉じるので、someprogそのsomeprogファイル記述子は単に存在しません。

次のように書くこともできます。someprog 3>&- 4>&-


建設の段階的な説明:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

下から上へ:

  1. stdoutにリダイレクトされたファイル記述子4を使用してサブシェルを作成します。これは、サブシェルのファイル記述子4に印刷されたすべての内容が、構成全体の標準出力で終了することを意味します。
  2. パイプラインを作成し、#part3左()と右()のコマンドを実行します。また、パイプラインの最後のコマンドは、stdinの文字列が構成全体の終了状態になることを意味します。#part2exit $xs
  3. stdoutにリダイレクトされたファイル記述子3を使用してサブシェルを作成します。これは、このサブシェルのファイル記述子3に印刷されたすべての内容が最終的に#part2全体構成の終了状態になることを意味します。
  4. パイプを作成し、左(および#part5#part6と右()のコマンドを実行します。filter >&4出力はfilterファイル記述子4にリダイレクトされます。#part1ファイル記述子4のstdoutにリダイレクトされます。これは、出力がfilter全体構成の標準出力であることを意味します。
  5. 終了状態は#part6ファイル記述子3に印刷される。#part3ファイル記述子3からリダイレクトします#part2。これは、終了状態が#part6全体構成の最終終了状態になることを意味します。
  6. someprog処刑される。終了ステータスが受信されました#part5。標準出力はパイプ入力として得られ、#part4に渡されますfilter。で説明されているように、willの出力はfilter順番に標準出力に移動します。#part4

答え4

可能であれば終了コードfoobarfoo

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

または、出力にfoo次の行だけが含まれていないことがわかっている場合.

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

bar最後の行を除くすべての項目を処理し、最後の行を終了コードに渡す方法がある場合は、常に実行できます。

bar複雑なパイプで出力が不要な場合は、他のファイル記述子に終了コードを印刷してパイプの一部を無視できます。

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

それ以降は通常発生しますが、$exit_codesすべての入力を読み取る前に終了したり、運が悪い場合に発生する可能性foo:X bar:Yがあります。私の考えでは、最大512バイトまで書き込むすべてのユニークパイプはアトミックなので、タグ文字列が507バイト未満の場合と部分が混在しません。bar:Y foo:Xbarfoo:$?bar:$?

キャプチャされた出力が必要な場合はbar難しくなります。終了コードが示すように見える行を含むようにneverの出力をソートすることで、上記の手法を組み合わせることができますが、bar退屈です。

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

もちろん簡単なオプションもあります一時ファイルの使用状態を保存します。シンプルだがそうではないそれ作りやすさ:

  • 複数のスクリプトが同時に実行されている場合、または同じスクリプトが複数の場所でこの方法を使用している場合は、異なる一時ファイル名を使用していることを確認する必要があります。
  • 共有ディレクトリに一時ファイルを安全に作成することは困難です。通常、/tmpこれはスクリプトがファイルに書き込むことができるかどうかを確認する唯一の場所です。使用mktemp、これはPOSIXではありませんが、現在、すべての深刻なユニスで利用可能です。
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

関連情報