パイプで接続された2つのプロセスfoo
とbar
があります。
$ foo | bar
bar
は常に0で終了します。 foo
の終了コードに興味があります。それを得る方法はありますか?
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_bash
とretval_zsh
のどちらか一方のみが設定されます。もう一方は空白になります。これにより、関数はreturn $retval_bash $retval_zsh
で終了することができます(引用符がないことに注意してください)。
これを行うには、3つの一般的な方法があります。
最初の方法は、pipefail
オプション(ksh
、zsh
またはbash
)を設定することです。これは最も単純で、基本的には終了ステータス$?
を最後のプログラムの終了コードに設定して、ゼロ以外(またはすべてが正常に終了した場合はゼロ)で終了します。
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Bashには、最後のパイプライン内のすべてのプログラムの終了ステータスを含む$PIPESTATUS
(zsh
内の$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
このソリューションは、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
下から上へ:
#part3
)と右側(#part2
)のコマンドが実行されます。 exit $xs
は、パイプの最後のコマンドでもあります。つまり、stdinからの文字列が構成全体の終了ステータスになります。#part2
になり、構成全体の終了ステータスになります。#part5
および#part6
)と右側のコマンド(filter >&4
)が実行されます。 filter
の出力はファイル記述子4にリダイレクトされます。#part1
では、ファイル記述子4がstdoutにリダイレクトされました。これは、filter
の出力が構成全体のstdoutであることを意味します。#part6
の終了ステータスがファイル記述子3に出力されます。#part3
では、ファイル記述子3が#part2
にリダイレクトされました。つまり、#part6
からの終了ステータスは、構成全体の最終的な終了ステータスになります。someprog
が実行されます。終了ステータスは#part5
で取得されます。 stdoutは#part4
のパイプによって取得され、filter
に転送されます。 filter
からの出力は、#part4
で説明されているように、次にstdoutに到達します。あなたが尋ねたとおりではありませんが、あなたは使うことができます
#!/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
可能な場合は、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
を区別できません。
だから私はレスマナのような答えを提供したかったのですが、私はおそらく少しシンプルで少し有利な純粋なボーンシェルソリューションだと思います:
# 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に直接書き込む場合もあります。推測(開いているかどうかを確認するためにファイル記述子をチェックし、開いている場合はそれを使用するプログラム、または開いていない場合はそれに応じて異なる動作をするプログラムを想像できます)。したがって、後者を念頭に置き、汎用のケースで使用するのがおそらく最善です。
moreutils パッケージがインストールされている場合は、mispipeユーティリティを使用して、要求どおりの処理を行うことができます。
上記のlesmanaのソリューションは、代わりに{ .. }
を使用してネストされたサブプロセスを開始するオーバーヘッドなしで実行することもできます(この形式のグループ化されたコマンドは常にセミコロンで終了する必要があることを思い出してください)。このようなもの:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
このコンストラクトをダッシュバージョン0.5.5およびbashバージョン3.2.25および4.2.42で確認したので、一部のシェルが{ .. }
グループ化をサポートしていない場合でも、POSIXに準拠しています。
以下は、一般的な解決策の1つを使用できない場合に備えて、@ Patrikの回答のアドオンを意味します。
この回答は次のことを前提としています。
$PIPESTATUS
もset -o pipefail
も知らないシェルがあります追加の仮定。あなたはすべてを取り除くことができますが、これはレシピを過度に壊しているので、ここではカバーされていません:
- 知りたいのは、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
にファイルを作成しますwait
は、ksh
に必要です。これは、ksh
が他のすべてのパイプコマンドの終了を待機しないためです。ただし、バックグラウンドタスクが存在する場合は不要な副作用があるため、デフォルトでコメントアウトしていることに注意してください。待ち時間が問題にならない場合は、コメントで記入できます。read
はfalse
を返すため、true
はエラーを示しますこれは、単一のコマンドのプラグインの置き換えとして使用でき、次のものが必要です。
/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 sh
、bash
、ksh
、ash
、sash
、およびcsh
でテストされました
これは移植可能です。つまり、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))
少し注意して、これはうまくいくはずです:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
次の「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
ほどエレガントではありませんが、この機能が利用できない場合の正当な代替手段です。
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
$
(少なくともbashで)set -e
と組み合わせると、サブシェルを使用してpipefailを明示的にエミュレートし、パイプエラーで終了することができます
set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program
そのため、何らかの理由でfoo
が失敗した場合、残りのプログラムは実行されず、スクリプトは対応するエラーコードで終了します。 (これは、foo
が独自のエラーを出力することを前提としています。これは、失敗の理由を理解するのに十分です)