Bashで長時間実行されているコマンドを実行し、両方の終了ステータスをキャプチャし、その出力をTシャツにしたいと考えています。
だから私はこれをします:
command | tee out.txt
ST=$?
問題は、変数STがtee
コマンドではなくコマンドの終了ステータスをキャプチャすることです。どうすればこれを解決できますか?
コマンドは実行時間が長く、後で表示するために出力をファイルにリダイレクトすることは、私にとって良い解決策ではないことに注意してください。
Bashで長時間実行されているコマンドを実行し、両方の終了ステータスをキャプチャし、その出力をTシャツにしたいと考えています。
だから私はこれをします:
command | tee out.txt
ST=$?
問題は、変数STがtee
コマンドではなくコマンドの終了ステータスをキャプチャすることです。どうすればこれを解決できますか?
コマンドは実行時間が長く、後で表示するために出力をファイルにリダイレクトすることは、私にとって良い解決策ではないことに注意してください。
回答:
と呼ばれる内部Bash変数があり$PIPESTATUS
ます。これは、コマンドの最後のフォアグラウンドパイプラインの各コマンドの終了ステータスを保持する配列です。
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
または、他のシェル(zshなど)でも動作する別の方法は、pipefailを有効にすることです。
set -o pipefail
...
最初のオプションはないではないと動作zsh
少し異なる構文に起因します。
exit ${PIPESTATUS[0]}
。
bashを使用するset -o pipefail
と便利です
pipefail:パイプラインの戻り値は、ゼロ以外のステータスで終了した最後のコマンドのステータス、またはゼロ以外のステータスで終了したコマンドがない場合はゼロ
( set -o pipefail; command | tee out.txt ); ST=$?
set -o pipefail
は、コマンドを実行してset +o pipefail
から実行し、その直後にオプションの設定を解除することができます。
-o pipefail
パイプが失敗した場合、彼は知っているだろうが、「コマンド」と「ティー」の両方が失敗した場合、彼は「ティー」からの終了コードを受け取ることになります。
ばかげた解決策:名前付きパイプ(mkfifo)を介してそれらを接続します。次に、コマンドを2番目に実行できます。
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
mkfifo
、mknod -p
私が正しく覚えている場合は代わりに必要になる可能性があることに注意してください。
mkfifo
または誰かに問題がある場合mknod -p
:私の場合、パイプファイルを作成する適切なコマンドがありましたmknod FILE_NAME p
。
パイプ内の各コマンドの終了ステータスを提供する配列があります。
$ 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
このソリューションは、bash固有の機能や一時ファイルを使用しなくても機能します。おまけ:最後に、終了ステータスは実際には終了ステータスであり、ファイル内の一部の文字列ではありません。
状況:
someprog | filter
からの終了ステータスとからsomeprog
の出力が必要filter
です。
これが私の解決策です:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
詳細な説明とサブシェルといくつかの警告のない代替案については、unix.stackexchange.comで同じ質問に対する私の回答を参照してください。
サブシェルでコマンド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
VALUE=$(might_fail | piping)
これはマスターシェルでPIPESTATUSを設定しませんが、エラーレベルを設定します。使用することで:VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})
欲しかった。
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}
ティーではなくgrepフィルタリングの場合
だから私はレスマナのような答えを提供したかったのですが、私はおそらく少し単純で少し有利な純粋なボーンシェルのソリューションだと思います:
# 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に直接書き込むこともあります。推測(プログラムがファイル記述子をチェックして開いているかどうかを確認し、開いている場合はそれを使用するか、開いていない場合はそれに応じて異なる動作をすることを想像できます)。したがって、後者を念頭に置き、汎用のケースで使用するのがおそらく最善です。
PIPESTATUS [@]は、pipeコマンドが戻った直後に配列にコピーする必要があります。 どれが PIPESTATUSの読み込み[@]の内容が消去されます。すべてのパイプコマンドのステータスを確認する場合は、別のアレイにコピーします。「$?」「$ {PIPESTATUS [@]}」の最後の要素と同じ値であり、それを読み取ると「$ {PIPESTATUS [@]}」が破壊されるようですが、これを完全に確認しているわけではありません。
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[@]}" )
パイプがサブシェルにある場合、これは機能しません。その問題の解決策については、バックティックコマンドのbash pipestatusを
参照してください。
プレーンバッシュでこれを行う最も簡単な方法は、プロセス置換を使用することですは、パイプラインの代わりにです。いくつかの違いがありますが、ユースケースではおそらくそれほど重要ではありません。
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に物事を書き続けるかもしれません、それは混乱するかもしれません。
純粋なシェルソリューション:
% 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の出力のように)ログファイルに削除したりできます。
@ 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
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番目のコマンドです。