たとえばecho
などのコマンドを呼び出すと、そのコマンドの結果をtee
を使用する他のいくつかのコマンドで使用できます。例:
echo "Hello world!" | tee >(command1) >(command2) >(command3)
Catを使用すると、いくつかのコマンドの結果を収集できます。例:
cat <(command1) <(command2) <(command3)
両方を同時に実行できるようにしたいので、tee
を使用して、他の何かの出力でこれらのコマンドを呼び出すことができます(たとえば、echo
)そして、cat
を使用して、すべての結果を単一の出力に収集します。
結果を順序どおりに保つことが重要です。これは、command1
、command2
、およびcommand3
の出力の行が絡み合わないようにする必要があることを意味します。 cat
)。
cat
とtee
より優れたオプションがあるかもしれませんが、それらは私がこれまでに知っているものです。
入力と出力のサイズが大きくなる可能性があるため、一時ファイルの使用を避けたいです。
どうすればこれができますか?
PD:別の問題は、これがループで発生するため、一時ファイルの処理が困難になることです。これは私が現在持っているコードで、小さなテストケースで機能しますが、理解できない方法でauxfileから読み書きすると無限ループを作成します。
somefunction()
{
if [ $1 -eq 1 ]
then
echo "Hello world!"
else
somefunction $(( $1 - 1 )) > auxfile
cat <(command1 < auxfile) \
<(command2 < auxfile) \
<(command3 < auxfile)
fi
}
Auxfileの読み取りと書き込みが重なっているように見え、すべてが爆発します。
GNU stdbufとpee
from moreutils の組み合わせを使用できます:
_echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
_
pee popen(3)
sこれらの3つのシェルコマンドライン、次にfread
s入力、fwrite
s入力3つすべて、最大1Mまでバッファリングされます。
アイデアは、少なくとも入力と同じ大きさのバッファを持つことです。この方法では、3つのコマンドが同時に開始されても、pee
pclose
sで3つのコマンドが順番に実行されるときにのみ、入力が表示されます。
pclose
が実行されるたびに、pee
はバッファをコマンドにフラッシュし、その終了を待ちます。これにより、これらのcmdx
コマンドが入力を受信する前に何も出力を開始しない限り(そして、親が戻った後も出力を継続する可能性のあるプロセスをフォークしない限り)、 3つのコマンドはインターリーブされません。
実際、これはメモリ内の一時ファイルを使用するのと少し似ていますが、3つのコマンドが同時に開始されるという欠点があります。
コマンドを同時に開始しないようにするには、pee
をシェル関数として記述します。
_pee() (
input=$(cat; echo .)
for i do
printf %s "${input%.}" | eval "$i"
done
)
echo "Hello world!" | pee cmd1 cmd2 cmd3 > out
_
ただし、zsh
以外のシェルは、NUL文字のバイナリ入力では失敗することに注意してください。
これは一時ファイルの使用を回避しますが、それは入力全体がメモリに保存されることを意味します。
いずれの場合も、メモリまたは一時ファイルのどこかに入力を保存する必要があります。
実際、これは非常に興味深い質問です。単一のタスクにいくつかの単純なツールを連携させるというUnixのアイデアの限界を示しているからです。
ここでは、タスクにいくつかのツールを連携させたいと思います。
echo
)tee
)cmd1
_、_cmd2
_、_cmd3
_)cat
)。それらがすべて同時に実行され、データが利用可能になり次第、処理することを意図しているデータに対してハードワークを行うことができれば、すばらしいでしょう。
1つのフィルターコマンドの場合、それは簡単です。
_src | tee | cmd1 | cat
_
すべてのコマンドは同時に実行され、_cmd1
_は、利用可能になるとすぐにsrc
からデータを収集し始めます。
これで、3つのフィルターコマンドを使用しても、同じことができます。同時に開始して、パイプで接続します。
_ ┏━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┏━━━┓
┃ ┃░░░░2░░░░░┃cmd1┃░░░░░5░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃░░░░1░░░░░┃tee┃░░░░3░░░░░┃cmd2┃░░░░░6░░░░┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁▁▁▁▁▁▁┏━━━━┓▁▁▁▁▁▁▁▁▁▁┃ ┃
┃ ┃░░░░4░░░░░┃cmd3┃░░░░░7░░░░┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
_
名前付きパイプで比較的簡単にできること:
_pee() (
mkfifo tee-cmd1 tee-cmd2 tee-cmd3 cmd1-cat cmd2-cat cmd3-cat
{ tee tee-cmd1 tee-cmd2 tee-cmd3 > /dev/null <&3 3<&- & } 3<&0
eval "$1 < tee-cmd1 1<> cmd1-cat &"
eval "$2 < tee-cmd2 1<> cmd2-cat &"
eval "$3 < tee-cmd3 1<> cmd3-cat &"
exec cat cmd1-cat cmd2-cat cmd3-cat
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
_
(_} 3<&0
_の上にあるのは、_&
_が_/dev/null
_からstdin
をリダイレクトするという事実を回避することであり、_<>
_を使用して、もう一方の端(cat
)も開くまでブロックする)
または、名前付きパイプを回避するために、zsh
coprocを使用してもう少し痛いです:
_pee() (
n=0 ci= co= is=() os=()
for cmd do
eval "coproc $cmd $ci $co"
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
echo abc | pee 'tr a A' 'tr b B' 'tr c C'
_
さて、問題は、すべてのプログラムが開始されて接続されると、データは流れますか?
2つの制約があります。
tee
はすべての出力を同じ速度でフィードするため、最も遅い出力パイプの速度でのみデータをディスパッチできます。cat
は、最初のパイプ(5)からすべてのデータが読み取られたときにのみ、2番目のパイプ(上の図のパイプ6)から読み取りを開始します。つまり、データは_cmd1
_が終了するまでパイプ6を流れません。また、上記の_tr b B
_の場合と同様に、データがパイプ3にも流れないことを意味する場合があります。つまり、tee
以降、パイプ2、3、または4のいずれにもデータが流れません。すべての中で最も遅い速度で給餌する3。
実際には、これらのパイプはnull以外のサイズであるため、一部のデータはなんとか通過し、少なくとも私のシステムでは、次のように機能させることができます。
_yes abc | head -c $((2 * 65536 + 8192)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c -c
_
それ以上に、
_yes abc | head -c $((2 * 65536 + 8192 + 1)) | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
_
デッドロックがあり、次のような状況にあります。
_ ┏━━━┓▁▁▁▁2▁▁▁▁▁┏━━━━┓▁▁▁▁▁5▁▁▁▁┏━━━┓
┃ ┃░░░░░░░░░░┃cmd1┃░░░░░░░░░░┃ ┃
┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃
┏━━━┓▁▁▁▁1▁▁▁▁▁┃ ┃▁▁▁▁3▁▁▁▁▁┏━━━━┓▁▁▁▁▁6▁▁▁▁┃ ┃▁▁▁▁▁▁▁▁▁┏━━━┓
┃src┃██████████┃tee┃██████████┃cmd2┃██████████┃cat┃░░░░░░░░░┃out┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┃ ┃▔▔▔▔▔▔▔▔▔┗━━━┛
┃ ┃▁▁▁▁4▁▁▁▁▁┏━━━━┓▁▁▁▁▁7▁▁▁▁┃ ┃
┃ ┃██████████┃cmd3┃██████████┃ ┃
┗━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━━┛▔▔▔▔▔▔▔▔▔▔┗━━━┛
_
パイプ3と6(それぞれ64kiB)を充填しました。 tee
はその余分なバイトを読み取り、それを_cmd1
_に送りましたが、
cmd2
_が空になるのを待っているため、パイプ3への書き込みがブロックされましたcmd2
_は、パイプ6での書き込みがブロックされているため空にできません。cat
が空にするのを待っていますcat
はパイプ5に入力がなくなるまで待機しているため、空にすることはできません。cmd1
_はcat
からの入力を待っているため、これ以上入力がないことをtee
に通知できません。tee
は_cmd1
_にそれをブロックすることができるので、これ以上入力がないことを伝えることはできません...など。依存関係ループがあるため、デッドロックが発生します。
さて、解決策は何ですか?より大きなパイプ3と4(src
のすべての出力を含めるのに十分な大きさ)がそれを実行します。たとえば、tee
と_pv -qB 1G
_の間に_cmd2/3
_を挿入して、pv
が_cmd2
_と_cmd3
_を読んでください。ただし、次の2つのことを意味します。
cmd2
_は実際にはcmd1が終了したときにのみデータの処理を開始するため、3つのコマンドすべてを連携させることはできません。2番目の問題の解決策は、パイプ6と7も大きくすることです。 _cmd2
_および_cmd3
_が消費するのと同じ量の出力を生成すると仮定すると、メモリを消費しません。
(最初の問題で)データの重複を回避する唯一の方法は、ディスパッチャ自体にデータの保持を実装することです。つまり、tee
のバリエーションを実装し、最速の出力レートでデータをフィードできます。 (自分のペースで遅いものに供給するためのデータを保持します)。ささいなことではありません。
したがって、結局、プログラミングなしで合理的に得ることができる最高のものはおそらく(Zsh構文)のようなものです:
_max_hold=1G
pee() (
n=0 ci= co= is=() os=()
for cmd do
if ((n)); then
eval "coproc pv -qB $max_hold $ci $co | $cmd $ci $co | pv -qB $max_hold $ci $co"
else
eval "coproc $cmd $ci $co"
fi
exec {i}<&p {o}>&p
is+=($i) os+=($o)
eval i$n=$i o$n=$o
ci+=" {i$n}<&-" co+=" {o$n}>&-"
((n++))
done
coproc :
read -p
eval tee /dev/fd/$^os $ci "> /dev/null &" exec cat /dev/fd/$^is $co
)
yes abc | head -n 1000000 | pee 'tr a A' 'tr b B' 'tr c C' | uniq -c
_
あなたが提案することは、既存のコマンドでは簡単に行うことができず、とにかくあまり意味がありません。パイプの全体的な考え方(Unix/Linuxでは|
)は、cmd1 | cmd2
ではcmd1
がメモリバッファーがいっぱいになるまで(最大で)出力を書き込み、次にcmd2
を実行するというものです。バッファが空になるまで(最大で)バッファからデータを読み取ります。つまり、cmd1
とcmd2
は同時に実行されるため、それらの間で「処理中」のデータが限られた量を超える必要はありません。複数の入力を単一の出力に接続する場合、リーダーの1つが他のリーダーよりも遅れている場合は、他のリーダーを停止するか(並列実行のポイントは何ですか?)、または遅延がまだ読んでいない出力を隠します。 (中間ファイルがないことの意味は何ですか?)さらに、同期全体がlotより複雑になります。
Unixでの30年近くの経験の中で、このような複数出力のパイプにとって本当に有益だった状況を覚えていません。
今日、複数の出力を1つのストリームに組み合わせることができますが、インターリーブの方法ではありません(cmd1
とcmd2
の出力をどのようにインターリーブしますか?1行ずつ交互に入れ替えますか?交互に10バイトを書き込みますか?代替 "段落"どういうわけか定義されていますか?そして、長い間何も書かない場合は?これはすべて処理が複雑です)。それは、例えばによって行われます。 (cmd1; cmd2; cmd3) | cmd4
、プログラムcmd1
、cmd2
、およびcmd3
が次々に実行され、出力がcmd4
への入力として送信されます。
Linuxで(そしてbash
またはzsh
では__ksh93
_ではなく)重複する問題については、次のように実行できます。
_somefunction()
(
if [ "$1" -eq 1 ]
then
echo "Hello world!"
else
exec 3> auxfile
rm -f auxfile
somefunction "$(($1 - 1))" >&3 auxfile 3>&-
exec cat <(command1 < /dev/fd/3) \
<(command2 < /dev/fd/3) \
<(command3 < /dev/fd/3)
fi
)
_
_(...)
_の代わりに_{...}
_を使用して各反復で新しいプロセスを取得し、新しいauxfile
を指す新しいfd 3を取得できることに注意してください。 _< /dev/fd/3
_は、削除されたファイルにアクセスするためのトリックです。 Linux以外のシステムでは、_< /dev/fd/3
_がdup2(3, 0)
のように機能しないため、fd 0は書き込み専用モードで開かれ、ファイルの末尾にカーソルが置かれます。
ネストされたsomefunctionのフォークを回避するには、次のように記述します。
_somefunction()
{
if [ "$1" -eq 1 ]
then
echo "Hello world!"
else
{
rm -f auxfile
somefunction "$(($1 - 1))" >&3 auxfile 3>&-
exec cat <(command1 < /dev/fd/3) \
<(command2 < /dev/fd/3) \
<(command3 < /dev/fd/3)
} 3> auxfile
fi
}
_
シェルは、各反復でバックアップ fd 3を処理します。ただし、ファイル記述子が不足することになります。
次のようにするとより効率的です。
_somefunction() {
if [ "$1" -eq 1 ]; then
echo "Hello world!" > auxfile
else
somefunction "$(($1 - 1))"
{ rm -f auxfile
cat <(command1 < /dev/fd/3) \
<(command2 < /dev/fd/3) \
<(command3 < /dev/fd/3) > auxfile
} 3< auxfile
fi
}
somefunction 12; cat auxfile
_
つまり、リダイレクトをネストしないでください。