出力をパイプし、Bashで終了ステータスをキャプチャする


421

Bashで長時間実行されているコマンドを実行し、両方の終了ステータスをキャプチャし、その出力をTシャツにしたいと考えています。

だから私はこれをします:

command | tee out.txt
ST=$?

問題は、変数STがteeコマンドではなくコマンドの終了ステータスをキャプチャすることです。どうすればこれを解決できますか?

コマンドは実行時間が長く、後で表示するために出力をファイルにリダイレクトすることは、私にとって良い解決策ではないことに注意してください。


1
[["$ {PIPESTATUS [@]}" =〜[^ 0 \]]] && echo -e "一致-エラーが見つかりました" || echo -e "一致なし-すべて良好"これは、配列のすべての値を一度にテストし、返されたパイプ値のいずれかがゼロでない場合にエラーメッセージを表示します。これは、パイプされた状況でエラーを検出するための非常に堅牢な一般化されたソリューションです。
ブライアンS.ウィルソン

回答:


519

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

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

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

set -o pipefail
...

最初のオプションはないではないと動作zsh少し異なる構文に起因します。


21
PIPESTATUS AND Pipefailの例については、unix.stackexchange.com/ a / 73180 / 7453に説明があります。
slm 2013

18
注:$ PIPESTATUS [0]はパイプ内の最初のコマンドの終了ステータスを保持し、$ PIPESTATUS [1]は2番目のコマンドの終了ステータスを保持し、以下同様に続きます。
simpleuser 2013

18
もちろん、これはBash固有であることを覚えておく必要があります。たとえば、AndroidデバイスのBusyBoxの「sh」実装、または他の「sh」を使用する他の組み込みプラットフォームで実行するスクリプトを記述した場合バリアント、これは機能しません。
Asfand Qazi 2014

4
引用符で囲まれていない変数の展開に関心がある場合:終了ステータスは常にBashの符号なし8ビット整数あるため、引用符で囲む必要はありません。これは一般的にUnixでも当てはまり、終了ステータスは明示的8ビットであると定義されており、POSIX自体でも、たとえば論理否定を定義する場合は、署名されていないものと見なされます。
Palec、2016年

3
も使用できますexit ${PIPESTATUS[0]}
Chaoran 16

142

bashを使用するset -o pipefailと便利です

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


23
あなたは、スクリプト全体のpipefail設定を変更したくない場合は、あなただけのローカルオプションを設定することができます( set -o pipefail; command | tee out.txt ); ST=$?
Jaan

7
@Jaanこれはサブシェルを実行します。それを避けたい場合set -o pipefailは、コマンドを実行してset +o pipefailから実行し、その直後にオプションの設定を解除することができます。
Linus Arver 2016

2
注:質問の投稿者はパイプの「一般的な終了コード」を望んでおらず、「コマンド」の戻りコードを望んでいます。-o pipefailパイプが失敗した場合、彼は知っているだろうが、「コマンド」と「ティー」の両方が失敗した場合、彼は「ティー」からの終了コードを受け取ることになります。
t0r0X 2018年

@LinusArverは、成功したコマンドなので、終了コードをクリアしませんか?
carlin.scott

127

ばかげた解決策:名前付きパイプ(mkfifo)を介してそれらを接続します。次に、コマンドを2番目に実行できます。

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

20
これは、この質問の唯一の回答であり、単純なsh Unixシェルでも機能します。ありがとう!
JamesThomasMoon1979

3
@DaveKennedy:「bash構文の複雑な知識を必要としない明白な」のように
ばかげている

10
bashの追加機能の利点がある場合、bashの回答はよりエレガントになりますが、これはクロスプラットフォームソリューションです。また、長時間実行するコマンドを実行しているときはいつでも、名前パイプが最も柔軟な方法である場合が多いので、一般的に検討する価値があります。一部のシステムにはないのでmkfifomknod -p私が正しく覚えている場合は代わりに必要になる可能性があることに注意してください。
Haravikk、2016年

3
スタックオーバーフローでは、100倍に賛成票を投じて、人々が意味のない他のことをやめるという回答がある場合があります。これはその1つです。ありがとうございます。
ダンチェイス

1
mkfifoまたは誰かに問題がある場合mknod -p:私の場合、パイプファイルを作成する適切なコマンドがありましたmknod FILE_NAME p
Karol Gil

36

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

$ 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

26

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

状況:

someprog | filter

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

これが私の解決策です:

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

echo $?

詳細な説明とサブシェルといくつかの警告のない代替案については、unix.stackexchange.com同じ質問に対する私の回答を参照しください。


20

サブシェルでコマンド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


4
おかげで、これによりコンストラクトを使用できるようになりました。VALUE=$(might_fail | piping)これはマスターシェルでPIPESTATUSを設定しませんが、エラーレベルを設定します。使用することで:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})欲しかった。
vaab 2014年

@vaab、その構文は本当にきれいに見えますが、私はあなたのコンテキストで「パイプ」が何を意味するのか混乱していますか?それは、tightや、might_failの出力に対する処理を行う場所ですか?ty!
AnneTheAgile 14

1
この例の@AnneTheAgile 'piping'は、errlvlを表示したくないコマンドを表しています。たとえば、 'tee'、 'grep'、 'sed'、...のいずれか、またはパイプで連結された組み合わせこれらのパイピングコマンドは、メインのより大きな出力またはログ出力から情報をフォーマットまたは抽出することを目的としていることは珍しくありませんコマンド:次に、メインコマンドのerrlevel(この例では「might_fail」と呼んだもの)に関心がありますが、私の構成がないと、割り当て全体で最後のパイプコマンドのerrlvlが返されますが、ここでは意味がありません。これはより明確ですか?
vaab 14

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}ティーではなくgrepフィルタリングの場合
user1742529

12

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

# 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が実行されてstdout(ファイル記述子1)に通常の出力を出力し、それが完了すると、printfが実行されてicommand1の終了コードをstdoutに出力しますが、そのstdoutはファイル記述子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に直接書き込むこともあります。推測(プログラムがファイル記述子をチェックして開いているかどうかを確認し、開いている場合はそれを使用するか、開いていない場合はそれに応じて異なる動作をすることを想像できます)。したがって、後者を念頭に置き、汎用のケースで使用するのがおそらく最善です。


いい説明だ!
selurvedu

6

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


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

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


4

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

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

これは、シェルが想定されている忍者スクリプトなどで役立ちます/bin/sh


3

PIPESTATUS [@]は、pipeコマンドが戻った直後に配列にコピーする必要があります。 どれが PIPESTATUSの読み込み[@]の内容が消去されます。すべてのパイプコマンドのステータスを確認する場合は、別のアレイにコピーします。「$?」「$ {PIPESTATUS [@]}」の最後の要素と同じ値であり、それを読み取ると「$ {PIPESTATUS [@]}」が破壊されるようですが、これを完全に確認しているわけではありません。

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

パイプがサブシェルにある場合、これは機能しません。その問題の解決策については、バックティックコマンドのbash pipestatusを
参照してください


3

プレーンバッシュでこれを行う最も簡単な方法は、プロセス置換を使用することですは、パイプラインの代わりにです。いくつかの違いがありますが、ユースケースではおそらくそれほど重要ではありません。

  • パイプラインを実行すると、bashはすべてのプロセスが完了するまで待機します。
  • Ctrl-Cをbashに送信すると、メインのプロセスだけでなく、パイプラインのすべてのプロセスが強制終了されます。
  • 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

さっき言ったように、パイプ表現とは違います。パイプのクローズに影響されない限り、プロセスは実行を停止することはありません。特に、それはあなたのstdoutに物事を書き続けるかもしれません、それは混乱するかもしれません。


1

純粋なシェルソリューション:

% 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番目を次のようにcat置き換えfalseます:

% 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が閉じているためです。この例では、ログ内の失敗したコマンドの順序は正しいですが、それに依存しないでください。

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


1

@ 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

bashの詳細を掘り下げるよりも、外部コマンドを使用する方が簡単で明確な場合があります。パイプラインは、最小プロセススクリプト言語execlineから、2番目のコマンドの戻りコードで終了します*、shパイプラインと同じですが、とは異なり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番目のコマンドです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.