あなたはGNU stdbufのと組み合わせて使用することができますpee
からmoreutilsを:
echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output
おしっこはpopen(3)
これらの3つのシェルコマンドラインをfread
入力し、次に入力を入力し、fwrite
3つすべてを入力します。これらは最大1Mまでバッファーされます。
アイデアは、少なくとも入力と同じ大きさのバッファを持つことです。この方法では、3つのコマンドが同時に開始されますがpee
pclose
、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
するという事実を回避することであり、もう一方の端()が開くまでブロックするパイプの開口を避けるために使用します)&
stdin
/dev/null
<>
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にも流れないことを意味する場合があります。つまり、3 tee
の中で最も遅い速度でフィードされるため、パイプ2、3、または4のいずれにもデータは流れません。
実際には、これらのパイプのサイズはヌルではないので、一部のデータはなんとか通り抜けることができます。少なくとも私のシステムでは、次のように処理できます。
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
の出力を含むのに十分な大きさ)がそれを行います。たとえば、最大1Gのデータを保存できる場所と場所を挿入してpv -qB 1G
、それらを読み取って読み取ることで、これを実行できます。ただし、次の2つのことを意味します。tee
cmd2/3
pv
cmd2
cmd3
- それは潜在的に多くのメモリを使用しており、さらにそれを複製しています
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