web-dev-qa-db-ja.com

別にパイプされたプロセスの終了ステータスを取得する

パイプで接続された2つのプロセスfoobarがあります。

$ foo | bar

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

314
Michael Mrozek

bashを使用している場合は、PIPESTATUS配列変数を使用して、パイプラインの各要素の終了ステータスを取得できます。

$ 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またはzshで実行すると、同じ結果が得られます。 retval_bashretval_zshのどちらか一方のみが設定されます。もう一方は空白になります。これにより、関数はreturn $retval_bash $retval_zshで終了することができます(引用符がないことに注意してください)。

277
camh

これを行うには、3つの一般的な方法があります。

パイプフェイル

最初の方法は、pipefailオプション(kshzshまたはbash)を設定することです。これは最も単純で、基本的には終了ステータス$?を最後のプログラムの終了コードに設定して、ゼロ以外(またはすべてが正常に終了した場合はゼロ)で終了します。

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

$ PIPESTATUS

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

$ 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
253
Patrick

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

状況:

someprog | filter

someprogからの終了ステータスとfilterからの出力が必要です。

これが私の解決策です:

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

このコンストラクトの結果は、コンストラクトのstdoutとしてfilterからstdoutになり、コンストラクトの終了ステータスとしてsomeprogから終了ステータスになります。


この構成は、サブシェル{...}ではなく、単純なコマンドグループ(...)でも機能します。サブシェルにはいくつかの影響があり、特にパフォーマンスコストがありますが、ここでは必要ありません。詳細については、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の文法では中括弧がスペースとセミコロンを必要とするため、構成がより広くなります。

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


someprogおよびfilter

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を継承します。someprogがファイル記述子3に書き込むと、終了ステータスになります。 readは1回しか読み取らないため、実際の終了ステータスは無視されます。

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

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

someprogを実行する前にexec 3>&- 4>&- before 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. ファイル記述子4がstdoutにリダイレクトされたサブシェルが作成されます。つまり、サブシェルのファイル記述子4に出力されるものはすべて、構造全体のstdoutになります。
  2. パイプが作成され、左側(#part3)と右側(#part2)のコマンドが実行されます。 exit $xsは、パイプの最後のコマンドでもあります。つまり、stdinからの文字列が構成全体の終了ステータスになります。
  3. ファイル記述子3がstdoutにリダイレクトされたサブシェルが作成されます。つまり、このサブシェルのファイル記述子3に出力されるものはすべて#part2になり、構成全体の終了ステータスになります。
  4. パイプが作成され、左側のコマンド(#part5および#part6)と右側のコマンド(filter >&4)が実行されます。 filterの出力はファイル記述子4にリダイレクトされます。#part1では、ファイル記述子4がstdoutにリダイレクトされました。これは、filterの出力が構成全体のstdoutであることを意味します。
  5. #part6の終了ステータスがファイル記述子3に出力されます。#part3では、ファイル記述子3が#part2にリダイレクトされました。つまり、#part6からの終了ステータスは、構成全体の最終的な終了ステータスになります。
  6. someprogが実行されます。終了ステータスは#part5で取得されます。 stdoutは#part4のパイプによって取得され、filterに転送されます。 filterからの出力は、#part4で説明されているように、次にstdoutに到達します。
58
lesmana

あなたが尋ねたとおりではありませんが、あなたは使うことができます

#!/bin/bash -o pipefail

パイプが最後のゼロ以外の戻り値を返すようにします。

コーディングが少し少ないかもしれません

編集:例

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
37
Chris

可能な場合は、fooからbarに終了コードをフィードします。たとえば、fooが数字のみの行を生成しないことがわかっている場合は、終了コードを追加できます。

{ 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、ただしbar:Y foo:X if barは、すべての入力を読み取る前に終了するか、運が悪い場合。 512バイトまでのパイプへの書き込みはすべてのuniceでアトミックであるため、foo:$?およびbar:$?タグ文字列が507バイト未満である限り、パーツが混在することはありません。

barからの出力をキャプチャする必要がある場合は、難しくなります。 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')

そしてもちろん、ステータスを保存するための 一時ファイルを使用 の簡単なオプションがあります。単純ですがthat単純ではありません:

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

パイプラインから始める:

foo | bar | baz

以下は、POSIXシェルのみを使用し、一時ファイルを使用しない一般的なソリューションです。

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statusesには、失敗したプロセスのステータスコードがランダムな順序で含まれ、どのコマンドが各ステータスを発行したかを示すインデックスが付いています。

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

テストで$error_statusesを囲む引用符に注意してください。それらがないと、改行がスペースに強制されるため、grepを区別できません。

17
Jander

だから私はレスマナのような答えを提供したかったのですが、私はおそらく少しシンプルで少し有利な純粋なボーンシェルソリューションだと思います:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

これは裏返しに説明するのが一番だと思います。command1は標準出力(ファイル記述子1)で実行され、通常の出力を出力します。それが完了すると、printfが実行され、command1の終了コードを標準出力に出力しますが、その標準出力はファイル記述子3。

Command1が実行されている間、そのstdoutはcommand2にパイプされます(パイプが読み取るものである1ではなくファイル記述子3に送信するため、printfの出力は決してcommand2になりません)。次に、command2の出力をファイル記述子4にリダイレクトします。これにより、ファイル記述子1にも含まれなくなります。ファイル記述子3のprintf出力をファイル記述子に戻すため、ファイル記述子1を少しだけ解放するためです。 1 –コマンド置換(バックティック)がキャプチャするものであり、それが変数に入れられるものだからです。

魔法の最後のビットは、最初に別のコマンドとしてexec 4>&1を実行したことです。これは、ファイル記述子4を外部シェルのstdoutのコピーとして開きます。コマンド置換は、その中のコマンドの観点から標準出力に書き込まれたものをすべてキャプチャします。ただし、コマンド置換に関する限り、command2の出力はファイル記述子4に送られるため、コマンド置換はキャプチャしません。コマンド置換から「外れる」と、スクリプトの全体的なファイル記述子1に実質的に移動します。

exec 4>&1は、置換を使用している「外部」コマンドで開かれているコマンド置換内のファイル記述子に書き込もうとすると、多くの一般的なシェルが気に入らないため、別のコマンドにする必要があります。したがって、これはそれを行うための最も簡単な移植可能な方法です。)

コマンドの出力がお互いを飛び越えているかのように、それほど技術的ではなく、より遊び心のある方法でそれを見ることができます:command1はcommand2にパイプし、printfの出力はcommand 2を飛び越えてcommand2がそれをキャッチしないようにします。コマンド2の出力は、printfが適切なタイミングで着地して置換によってキャプチャされ、コマンド2の出力が標準出力に書き込まれるのと同じように、コマンド置換の前後にジャンプします。通常のパイプで。

また、私が理解しているように、$?には、パイプ内の2番目のコマンドの戻りコードが含まれています。したがって、command2の戻りステータスが伝達されるはずです。これは、追加の関数を定義する必要がないため、これがlesmanaによって提案されたものよりもいくらか優れた解決策になると思うのです。

Lesmanaが言及している警告に従って、ある時点でcommand1がファイル記述子3または4を使用することになる可能性があるため、より堅牢にするためには、次のようにします。

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

この例では複合コマンドを使用していますが、サブシェル(( )の代わりに{ }を使用しても機能しますが、効率が低下する可能性があります)。

コマンドは、コマンドを起動するプロセスからファイル記述子を継承するため、2行目全体がファイル記述子4を継承し、複合コマンドの後に3>&1が続くと、ファイル記述子3が継承されます。したがって、4>&-は、内部の複合コマンドがファイル記述子4を継承せず、3>&-がファイル記述子3を継承しないようにするため、command1はよりクリーンで標準的な環境になります。内部の4>&-3>&-の横に移動することもできますが、そのスコープをできるだけ制限しないのはなぜでしょうか。

ファイル記述子3と4を直接使用する頻度がわかりません。ほとんどの場合、プログラムは、未使用のファイル記述子を返すシステムコールを使用しますが、コードがファイル記述子3に直接書き込む場合もあります。推測(開いているかどうかを確認するためにファイル記述子をチェックし、開いている場合はそれを使用するプログラム、または開いていない場合はそれに応じて異なる動作をするプログラムを想像できます)。したがって、後者を念頭に置き、汎用のケースで使用するのがおそらく最善です。

12
mtraceur

moreutils パッケージがインストールされている場合は、mispipeユーティリティを使用して、要求どおりの処理を行うことができます。

11
Emanuele Aina

上記のlesmanaのソリューションは、代わりに{ .. }を使用してネストされたサブプロセスを開始するオーバーヘッドなしで実行することもできます(この形式のグループ化されたコマンドは常にセミコロンで終了する必要があることを思い出してください)。このようなもの:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

このコンストラクトをダッシュ​​バージョン0.5.5およびbashバージョン3.2.25および4.2.42で確認したので、一部のシェルが{ .. }グループ化をサポートしていない場合でも、POSIXに準拠しています。

7
pkeller

以下は、一般的な解決策の1つを使用できない場合に備えて、@ Patrikの回答のアドオンを意味します。

この回答は次のことを前提としています。

  • $PIPESTATUSset -o pipefailも知らないシェルがあります
  • 並列実行にパイプを使用するため、一時ファイルは必要ありません。
  • スクリプトを中断した場合、突然の停電などにより、余分な混乱が生じないようにします。
  • この解決策は、追跡が比較的簡単で、読みやすくする必要があります。
  • 追加のサブシェルを導入したくない。
  • 既存のファイル記述子をいじることはできないため、stdin/out/errに触れないでください(ただし、一時的に新しいファイル記述子を導入することはできます)

追加の仮定。あなたはすべてを取り除くことができますが、これはレシピを過度に壊しているので、ここではカバーされていません:

  • 知りたいのは、PIPEのすべてのコマンドの終了コードが0であることです。
  • 追加のサイドバンド情報は必要ありません。
  • シェルは、すべてのパイプコマンドが返されるのを待ちます。

以前:foo | bar | baz、ただし、これは最後のコマンド(baz)の終了コードのみを返します

募集:パイプ内のいずれかのコマンドが失敗した場合、$?0(true)であってはなりません。

後:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

説明:

  • 一時ファイルはmktempで作成されます。これは通常、すぐに/tmpにファイルを作成します
  • この一時ファイルは、書き込みの場合はFD 9に、読み取りの場合はFD 8にリダイレクトされます。
  • その後、一時ファイルはすぐに削除されます。ただし、両方のFDが存在しなくなるまで、開いたままです。
  • これでパイプが開始されました。エラーが発生した場合、各ステップはFD 9にのみ追加されます。
  • waitは、kshに必要です。これは、kshが他のすべてのパイプコマンドの終了を待機しないためです。ただし、バックグラウンドタスクが存在する場合は不要な副作用があるため、デフォルトでコメントアウトしていることに注意してください。待ち時間が問題にならない場合は、コメントで記入できます。
  • その後、ファイルの内容が読み込まれます。空の場合(すべて機能したため)readfalseを返すため、trueはエラーを示します

これは、単一のコマンドのプラグインの置き換えとして使用でき、次のものが必要です。

  • 未使用のFD 9および8
  • 一時ファイルの名前を保持する単一の環境変数
  • そして、このレシピは、IOリダイレクトを許可するシェルのほとんどすべてに適用できます。
  • また、プラットフォームにとらわれず、/proc/fd/Nのようなものは必要ありません。

バグ:

このスクリプトには、/tmpがスペース不足になる場合のバグがあります。この人為的なケースからの保護も必要な場合は、次のようにして行うことができますが、0内の000の数がパイプ内のコマンドの数に依存するという欠点があります。少し複雑です:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

移植性に関する注記:

  • kshおよび最後のパイプコマンドのみを待機する同様のシェルでは、waitのコメントを解除する必要があります

  • 最後の例では、移植性が高いため、printf "%1s" "$?"ではなくecho -n "$?"を使用しています。すべてのプラットフォームが-nを正しく解釈するわけではありません。

  • printf "$?"でも同じことができますが、printf "%1s"は、実際に壊れたプラットフォームでスクリプトを実行した場合に、いくつかのまれなケースを検出します。 (読み取り:paranoia_mode=extremeでプログラムを実行した場合。)

  • FD 8とFD 9は、複数の桁をサポートするプラットフォームで上位になることがあります。 AFAIR a POSIX準拠シェルは、1桁のみをサポートする必要があります。

  • Debian 8.2 shbashkshashsash、およびcshでテストされました

5
Tino

これは移植可能です。つまり、POSIX準拠のシェルで動作し、現在のディレクトリを書き込み可能にする必要がなく、同じトリックを使用する複数のスクリプトを同時に実行できます。

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

編集:ここにGillesのコメントに続くより強力なバージョンがあります:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2:そして、これはdubiousjimコメントに続くわずかに軽い変種です:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
4
jlliagre

少し注意して、これはうまくいくはずです:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
3
alex

次の「if」ブロックは、「command」が成功した場合にのみ実行されます。

if command; then
   # ...
fi

具体的には、次のように実行できます。

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

実行されるのはhaconf -makerwそして、その標準出力と標準エラー出力を "$ haconf_out"に保存します。 haconfからの戻り値がtrueの場合、 'if'ブロックが実行され、grepは「$ haconf_out」を読み取り、「すでに書き込み可能なクラスター」と照合します。

パイプは自動的にクリーンアップされることに注意してください。リダイレクトを行うと、完了時に「$ haconf_out」を削除するように注意する必要があります。

pipefailほどエレガントではありませんが、この機能が利用できない場合の正当な代替手段です。

2
Rany Albeg Wein
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
1
C.G.

(少なくともbashで)set -eと組み合わせると、サブシェルを使用してpipefailを明示的にエミュレートし、パイプエラーで終了することができます

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

そのため、何らかの理由でfooが失敗した場合、残りのプログラムは実行されず、スクリプトは対応するエラーコードで終了します。 (これは、fooが独自のエラーを出力することを前提としています。これは、失敗の理由を理解するのに十分です)

0
noonex