web-dev-qa-db-ja.com

パイプ出力とBashでの終了ステータスの取得

私はBashで長時間実行するコマンドを実行したいと思います、そして両方ともその終了ステータスと tee その出力を取得します。

だから私はこれを行います:

command | tee out.txt
ST=$?

問題は、変数STがcommandではなくteeの終了状況をキャプチャーすることです。どうすればこれを解決できますか?

Commandは長時間実行され、後で表示するために出力をファイルにリダイレクトするのは私にとっては良い解決策ではありません。

374
flybywire

$PIPESTATUSと呼ばれる内部Bash変数があります。これは、最後の前景のコマンドパイプラインの各コマンドの終了ステータスを保持する配列です。

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

または他のシェル(zshなど)でも機能する別の方法は、pipefailを有効にすることです。

set -o pipefail
...

最初のオプションは、構文が多少異なるため、zshに対してnot動作します。

472
cODAR

bashのset -o pipefailを使うと便利です

pipefail:パイプラインの戻り値は、ゼロ以外のステータスで終了する最後のコマンドのステータス、またはゼロ以外のステータスで終了したコマンドがない場合はゼロです。

137
Felipe Alvarez

ダム解決策:名前付きパイプ(mkfifo)を通してそれらを接続する。その後、コマンドを2番目に実行できます。

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
112
EFraim

パイプ内の各コマンドの終了ステータスを示す配列があります。

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1
34
Stefano Borini

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

状況:

someprog | filter

someprogからの終了状況とfilterからの出力が必要です。

これが私の解決策です:

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

echo $?

詳細な説明と、サブシェルや注意事項のない代替方法については、 nix.stackexchange.comでの同じ質問に対する私の回答 を参照してください。

22
lesmana

PIPESTATUS[0]exitコマンドの実行結果をサブシェルで組み合わせることで、初期コマンドの戻り値に直接アクセスできます。

command | tee ; ( exit ${PIPESTATUS[0]} )

これが例です:

# the "false" Shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

あなたを与えるでしょう:

return value: 1

19
par

だから私はlesmanaのような答えを貢献したいと思いました、しかし私は私のものがおそらくもう少し簡単でそしてわずかにより有利な純粋なBourneシェル解決策であると思います:

# 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が実行してicommand1の終了コードを標準出力に出力します。ファイル記述子3.

Command1が実行されている間、その標準出力はcommand2にパイプされています(printfの出力は1ではなくファイル記述子3に送信されるため、command2に送信されることはありません)。それから、command2の出力をファイルディスクリプタ4にリダイレクトし、ファイルディスクリプタ1からも外れるようにします。ファイルディスクリプタ3のprintf出力をファイルディスクリプタ3に戻します。 1 - それがコマンド置換(バッククォート)で、これがキャプチャされ、それが変数に入れられるためです。

最後のちょっとしたことは、最初にexec 4>&1を別のコマンドとして実行したことです。これは、ファイル記述子4を外部シェルの標準出力のコピーとして開きます。コマンド置換は、その内部のコマンドの観点から標準で書かれているものすべてをキャプチャします - しかし、コマンド置換に関する限り、command2の出力はファイル記述子4に行くので、コマンド置換はそれをキャプチャしませんコマンド置換から「抜け出す」ことができますが、それでもスクリプトの全体的なファイル記述子1に行き着きます。

exec 4>&1は別のコマンドでなければなりません。多くの一般的なシェルでは、コマンド置換の中でファイル記述子に書き込もうとしたときにそれが気に入らないため、置換を使用している "external"コマンドで開きます。最も簡単な方法です。)

コマンドの出力がお互いに飛び越えているように、それほど技術的ではない遊び心のある方法で見ることができます:command1はcommand2にパイプし、printfの出力はcommand2を飛び越えてcommand2がキャッチしないようにします。 printfが置換に捉えられるためにちょうど間に合うようにコマンド2の出力がコマンド置換を飛び越えて飛び越え、変数に入り、command2の出力が標準出力に書き込まれるのと同じ賢い方法で進みます。通常のパイプで。

また、$?はパイプ内の2番目のコマンドの戻りコードを含みます。これは、変数割り当て、コマンド置換、および複合コマンドはすべて、それらの中のコマンドの戻りコードに対して実質的に透過的であるためです。私はこれが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を直接使用する頻度がわからない - ほとんどの場合、プログラムでは未使用のファイル記述子を返すsyscallを使用しますが、ファイル記述子3に直接コードを書き込むこともあります。推測します(ファイル記述子が開いているかどうかを確認するためにファイル記述子をチェックし、開いている場合はそれを使用し、開いていない場合はそれに応じて動作が異なる)。そのため、後者を念頭に置いて汎用目的の場合に使用することをお勧めします。

9
mtraceur

UbuntuとDebianではapt-get install moreutilsができます。これには、パイプ内の最初のコマンドの終了ステータスを返すmispipeというユーティリティが含まれています。

6
Bryan Larsen

PIPESTATUS [@]は、pipeコマンドが戻った直後に配列にコピーする必要があります。 PIPESTATUS [@]を読み取ると、内容が消去されます。すべてのパイプコマンドのステータスをチェックする予定の場合は、それを別の配列にコピーしてください。 「$?」は "$ {PIPESTATUS [@]}"の最後の要素と同じ値であり、それを読むことで "$ {PIPESTATUS [@]}"が破壊されるようですが、これは絶対に検証されていません。

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

パイプがサブシェル内にある場合、これは機能しません。その問題を解決するために、
backtickedコマンドの bash pipestatusを参照してください?

3
maxdev137
(command | tee out.txt; exit ${PIPESTATUS[0]})

@ cODARの答えとは異なり、これは最初のコマンドの元の終了コードを返し、成功の場合は0、失敗の場合は127だけではありません。しかし@Chaoranが指摘したように、あなたは${PIPESTATUS[0]}を呼び出すことができます。しかし、すべてを括弧でくくることが重要です。

3
jakob-r

Bashの外では、次のことができます。

bash -o pipefail  -c "command1 | tee output"

これは、シェルが/bin/shであることが期待される忍者スクリプトなどに役立ちます。

2
Anthony Scemama

これをプレーンbashで行う最も簡単な方法は、パイプラインの代わりに プロセス置換 を使用することです。いくつかの違いがありますが、それらはあなたのユースケースにとってあまり重要ではないでしょう:

  • パイプラインを実行しているとき、bashはすべてのプロセスが完了するまで待ちます。
  • Ctrlキーを押しながらCキーを押すと、メインのプロセスだけでなく、パイプラインのすべてのプロセスが強制終了されます。
  • pipefailオプションおよびPIPESTATUS変数は、プロセス置換とは無関係です。
  • おそらくもっと

プロセス置換では、bashはプロセスを開始してそれを忘れるだけで、jobsには表示されません。

consumer < <(producer)producer | consumerの違いは別にしても、本質的に同等です。

どちらが「メイン」プロセスかを反転したい場合は、コマンドと置換の方向をproducer > >(consumer)に反転するだけです。あなたの場合:

command > >(tee out.txt)

例:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

私が言ったように、パイプ式とは違いがあります。プロセスは、パイプのクローズに影響されない限り、実行を停止することはありません。特に、それはあなたの標準出力に物事を書き続けているかもしれず、それは混乱を招くかもしれません。

2
clacke

@ brian-s-wilsonの答えに基づく。このbashヘルパー関数:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

このように使われる:

1:get_bad_thingsは成功する必要がありますが、出力は生成されません。しかし私達はそれが作り出す出力を見たいです

get_bad_things | grep '^'
pipeinfo 0 1 || return

2:すべてのパイプラインは成功する必要があります

thing | something -q | thingy
pipeinfo || return
1
Sam Liddicott

Bashの詳細を掘り下げるのではなく、外部コマンドを使用する方が簡単で明確な場合があります。 pipeline 、最小プロセススクリプト言語から execline は、shパイプラインと同じように、2番目のコマンド*のリターンコードで終了しますが、shとは異なり、これを逆にすることができます。プロデューサプロセスのリターンコードを取得できるように、パイプの方向を指定します(以下はすべてshコマンドラインですが、execlineがインストールされている場合)。

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

pipelineを使用すると、ネイティブのbashパイプラインと同じ違いがあります。回答で使用されているbashプロセスの置換え #43972501

*実際にはエラーがない限りpipelineはまったく終了しません。これは2番目のコマンドで実行されるので、戻るのは2番目のコマンドです。

1
clacke

ピュアシェルソリューション:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

そして2番目のcatfalseに置き換えます。

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

最初の猫も失敗することに注意してください。それは標準出力が閉じられているからです。ログ内の失敗したコマンドの順序はこの例では正しいですが、それに頼らないでください。

この方法では、個々のコマンドのstdoutとstderrを取り込むことができるので、エラーが発生した場合はそれをログファイルにダンプし、エラーがない場合は削除します(ddの出力のように)。

1
Coroos