プロセス置換出力が順不同です


16

echo one; echo two > >(cat); echo three; 

コマンドは予期しない出力を提供します。

私はこれを読みました:プロセス置換はbashでどのように実装されていますか?インターネット上のプロセス置換に関する他の多くの記事が、なぜこのように動作するのか理解していない。

期待される出力:

one
two
three

実際の出力:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

また、この2つのコマンドは私の観点からは同等であるはずですが、そうではありません。

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

どうして同じだと思うの?なぜなら、どちらもseq出力をcat匿名パイプ(Wikipedia、プロセス置換)を介して入力に接続するからです。

質問:なぜこのように動作するのですか?私のエラーはどこにありますか?包括的な答えが望まれます(内部でどのように行われるかについての説明bash付き)。


2
それはそう一目でクリアしていない場合であっても、それは実際の重複だコマンドが無効である場合でも、プロセス置換での処理のためのbashの待機
ステファンChazelas

2
実際には、他の質問がこの質問と重複しているとマークされていると、この質問のほうが意味があります。それが私の答えをそこにコピーした理由です。
ステファンシャゼラス

回答:


21

はい、中bashにあるようなksh(機能はどこから来る)、プロセス置換内部のプロセスは(スクリプトの次のコマンドを実行する前に)を待っていません。

以下のために<(...)、通常は罰金のようです1:

cmd1 <(cmd2)

シェルは、パイプ上のファイルの終わりが置換されるまで読み取りcmd1cmd1行うため、通常待機しcmd2ます。また、ファイルの終わりは、通常、終了時に発生しcmd2ます。それは、いくつかのシェル(ではないが、同じ理由ですbash)を待っている気にしないでくださいcmd2の中でcmd2 | cmd1

以下のためには、cmd1 >(cmd2)それはより多くのだとして、しかし、それは、一般的ケースではありませんcmd2ため、通常は待機するcmd1ので、一般的に後が終了します。

で固定だとzshするためにその待機cmd2(ただし、あなたのようにそれを書く場合があるcmd1 > >(cmd2)cmd1、組み込みされていない使用{cmd1} > >(cmd2)の代わりとして文書化を)。

kshデフォルトでは待機しませんが、wait組み込みで待機することができます(でpidを使用できるようにしますが$!、実行した場合は役に立ちませんcmd1 >(cmd2) >(cmd3)

rccmd1 >{cmd2}構文を使用)、kshすべてのバックグラウンドプロセスのPIDを取得できることを除いて、と同じです$apids

es(また付きcmd1 >{cmd2}待ち)cmd2などでzshも、そして待つcmd2における<{cmd2}プロセスリダイレクト。

bashのpid cmd2(またはサブシェルcmd2の子プロセスで実行されるサブシェルの最後のコマンドであってもより正確に)をで使用可能にしますが$!、待機させることはできません。

を使用する必要がある場合bash、両方のコマンドを待機するコマンドを使用して問題を回避できます。

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

それは両方になりcmd1cmd2パイプへのFD 3が開いています。catもう一方の端にエンド・オブ・ファイルをお待ちしておりますので、通常時にのみ、両方終了するcmd1cmd2死んでいます。そして、シェルはそのcatコマンドを待ちます。あなたはすべてのバックグラウンドプロセスの終了をキャッチするネットとして見ることができます(&、coprocs、またはデーモンが通常行うようにすべてのファイル記述子を閉じない限り、バックグラウンドで実行されるコマンドなど、バックグラウンドで開始された他のものに使用できます) )。

その無駄なサブシェルプロセスのおかげでそれがあっても動作しますが、上記のことを注意cmd2そのFD 3閉じて(コマンドは通常はそれをしませんが、いくつかのようなsudoまたはsshください)。の将来のバージョンでbashは、最終的に他のシェルと同様にそこで最適化が行われる可能性があります。次に、次のようなものが必要になります。

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

そのsudoコマンドを待っているfd 3を開いたままのシェルプロセスがまだあることを確認します。

cat(プロセスはfd 3に書き込まないため)何も読み込まないことに注意してください。同期のためだけにあります。read()最後に何も返さないシステムコールを1つだけ実行します。

catコマンド置換を使用してパイプの同期を実行することで、実際に実行を回避できます。

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

今回は、シェルの代わりにcat、パイプの読み取りが行われ、パイプのもう一方の端はfd 3 cmd1およびで開かれcmd2ます。変数の割り当てを使用しているため、終了ステータスはcmd1で利用可能です$?

または、手動でプロセスの置換を行うこともできますし、システムのsh標準的なシェル構文として使用することもできます。

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

ただし、前述のように、すべてのsh実装が終了cmd1後に待機するわけではないことに注意してくださいcmd2(ただし、他の方法よりも優れています)。その時間に$?は、の終了ステータスが含まれcmd2ます。しかしbashzshメイクcmd1で利用できるの終了ステータス${PIPESTATUS[0]}$pipestatus[1]それぞれ(も参照pipefailので、いくつかのシェルでオプションを$?最後以外のパイプ部品の失敗を報告することができます)

yashプロセスリダイレクト機能にも同様の問題があることに注意してください。そこcmd1 >(cmd2)に書かれcmd1 /dev/fd/3 3>(cmd2)ます。しかしcmd2、待つことはできずwait、待つこともできず、そのpidも$!変数で利用できません。と同じ回避策を使用しbashます。


まず、を試しecho one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;、次にそれを単純化して、echo one; echo two > >(cat) | cat; echo three;値を正しい順序で出力します。この記述子の操作3>&1 >&4 4>&-はすべて必要ですか?また、私はこれを取得しません-4番目のfdに>&4 4>&リダイレクトstdoutし、4番目のfdを閉じてから、再び使用4>&1します。なぜ必要で、どのように機能するのですか?このトピックに関する新しい質問を作成する必要がありますか?
MiniMax

1
@MiniMax、しかしそこに、あなたがの標準出力に影響を及ぼしているcmd1cmd2、ファイルディスクリプタと少し踊りを持つポイントは、元のものを復元することであり、のみ、余分なパイプを使用して待っている代わりに、また、コマンドの出力をチャネリング。
ステファンシャゼル

@MiniMax理解するまでに時間がかかりました。以前はこんなに低いレベルのパイプを取得できませんでした。右端4>&1は、外側の中括弧コマンドリスト用のファイル記述子(fd)4を作成し、外側の中括弧のstdoutと等しくします。内部ブレースには、外部ブレースに接続するためのstdin / stdout / stderrが自動的にセットアップされています。ただし、3>&1fd 3を外側の中括弧の標準入力に接続します。>&4内部ブレースのstdoutを外部ブレースfd 4(前に作成したもの)に接続します。4>&-内側ブレースからfd 4を閉じます(内側ブレースのstdoutはすでにアウトブレースのfd 4に接続されているため)。
ニコラス・ピピトーネ

@MiniMax紛らわしい部分は右から左の部分であり4>&1、他のリダイレクトの前に最初に実行されるため、「もう一度使用する4>&1」ことはありません。全体的に、内部ブレースはデータをその標準出力に送信し、指定されたfd 4で上書きされました。内側のブレースが与えられたfd 4は、外側のブレースのfd 4であり、これは外側のブレースの元のstdoutと同じです。
ニコラスピピトーネ

Bash 4>5は「4が5に行く」という意味に感じさせますが、実際には「fd 4はfd 5で上書きされます」。そして、実行前に、fd 0/1/2は(外部シェルのfdと一緒に)自動接続され、必要に応じて上書きできます。それは少なくともbashのドキュメントの私の解釈です。あなたは外の何かを理解している場合、この、LMK。
ニコラスピピトーネ

4

2番目のコマンドを別のコマンドにパイプしてcat、入力パイプが閉じるまで待機します。例:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

短くてシンプル。

==========

簡単そうに思えますが、多くのことが舞台裏で行われています。これがどのように機能するかに興味がない場合は、残りの答えを無視できます。

がある場合echo two > >(cat); echo three>(cat)は対話型シェルによって分岐され、から独立して実行されecho twoます。したがって、echo two終了してecho threeから実行されますが、>(cat)終了する前に。ときbashからのデータ取得>(cat)、それは(ミリ秒のカップルの後に)それを期待していなかったときに、それがプロンプトのようなあなたが戻って端末に取得するために改行をヒットする必要があり、状況(他のユーザーがいる場合と同じことをあなたを与えるmesg「あなたがED)。

ただし、が与えられるとecho two > >(cat) | cat; echo three、2つのサブシェルが生成されます(|シンボルのドキュメントに従って)。

Aという名前の1つのサブシェルはfor echo two > >(cat)であり、Bという名前の1つのサブシェルはfor catです。Aは自動的にBに接続されます(Aの標準出力はBの標準入力です)。次に、実行echo two>(cat)開始します。>(cat)の標準出力はAの標準出力に設定され、これはBの標準出力に等しくなります。終了後echo two、Aは終了し、その標準出力を閉じます。ただし、>(cat)Bの標準入力への参照はまだ保持されています。2番目catのの標準入力はBの標準入力を保持catしており、EOFが見つかるまで終了しません。EOFは、誰もファイルを書き込みモードで開いていない場合にのみ与えられるため、>(cat)stdoutは2番目のファイルをブロックしcatます。Bはその秒を待っていcatます。以来echo two終了し、>(cat)最終的にはそう、EOFを取得>(cat)バッファをフラッシュして終了します。B's / second保持している人はcatもう標準入力なので、2番目catはEOFを読み取ります(Bは標準入力をまったく読み取りませんが、気にしません)。このEOFにより、2番目のcatユーザーはバッファーをフラッシュし、stdoutを閉じてcat終了し、Bが終了してBが待機していたため終了しcatます。

この警告は、bashがサブシェルを生成すること>(cat)です!このため、あなたはそれを見るでしょう

echo two > >(sleep 5) | cat; echo three

Bの標準入力を保持しecho threeていsleep 5ない場合でも、実行する前に5秒待機します。これは、生成された隠しサブシェルC >(sleep 5)がを待ってsleepおり、CがBの標準入力を保持しているためです。あなたはどのように見ることができます

echo two > >(exec sleep 5) | cat; echo three

ただし、sleepBのstdinを保持していないため、待機しません。また、Bのstdinを保持しているゴーストサブシェルCはありません(execはCをフォークしてCを待機させるのではなく、強制的にスリープをCに置き換えsleepます)。この警告に関係なく、

echo two > >(exec cat) | cat; echo three

前述のように、関数は順番に適切に実行されます。


私の回答へのコメントで@MiniMaxを使用した変換で述べたように、これはコマンドの標準出力に影響するという欠点があり、出力の読み取りと書き込みが余分に必要になることを意味します。
ステファンシャゼラス

説明は正確ではありません。はで発生するのをA待っていません。私の答えで述べたように、その理由cat>(cat)echo two > >(sleep 5 &>/dev/null) | cat; echo threethree 5秒後に出力さは、現在のバージョンが待機bashする余分なシェルプロセスを無駄に>(sleep 5)sleep、そのプロセスがまだpipe2番目のcat終了を防ぐstdoutを持っているためです。echo two > >(exec sleep 5 &>/dev/null) | cat; echo three余分なプロセスを削除するためにそれを置き換えると、すぐに戻ることがわかります。
ステファンシャゼラス

ネストされたサブシェルを作成しますか?私はそれを把握するためにbashの実装を調べようとしてきましたがecho two > >(sleep 5 &>/dev/null)、最低限でも独自のサブシェルを取得できると確信しています。sleep 5独自のサブシェルも取得する原因となる文書化されていない実装の詳細ですか?それが文書化されている場合、それはより少ない文字でそれを成し遂げる正当な方法です(タイトループがない限り、誰もがサブシェルまたは猫のパフォーマンスの問題に気付かないと思います) `。それが文書化されていない場合、リッピングはいいハックですが、将来のバージョンでは動作しません。
ニコラスピピトーン

$(...)<(...)確かにサブシェルを含みますが、ksh93またはzshは同じプロセスでそのサブシェルの最後のコマンドを実行しbashますが、パイプを開いsleepていないまま実行している間にパイプを開いている別のプロセスがまだあるのはそうではありません。の将来のバージョンでbashは、同様の最適化が実装される可能性があります。
ステファンシャゼラス

1
@StéphaneChazelas私は私の答えを更新し、短いバージョンの現在の説明は正しいと思いますが、シェルの実装の詳細を知っているので、確認できます。ただし、ファイル記述子のダンスとは対照的に、このソリューションを使用する必要があります。exec、期待どおりに機能するためです。
ニコラスピピトーネ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.