tee + cat:出力を数回使用し、結果を連結します


18

たとえば、あるコマンドを呼び出すとecho、そのコマンドの結果を他のいくつかのコマンドで使用できますtee。例:

echo "Hello world!" | tee >(command1) >(command2) >(command3)

catを使用すると、いくつかのコマンドの結果を収集できます。例:

cat <(command1) <(command2) <(command3)

私は両方のことを同時に行うことができるようにしたいのでtee、何か他の出力(たとえばecho私が書いたもの)でそれらのコマンドを呼び出してから、単一の出力ですべての結果を収集することができますcat

それは、その結果を順に維持することが重要です。この手段の出力のラインcommand1command2およびcommand3絡み合っが、(それがで起こるようにコマンドがあるとして注文するべきではありませんcat)。

そこよりも良い選択肢かもしれcatteeが、それらは私がこれまで知っているものです。

入力と出力のサイズが大きくなる可能性があるため、一時ファイルの使用を避けたい。

どうすればこれができますか?

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の読み取りと書き込みは重複しているようで、すべてが爆発します。


2
私たちはどのくらい話しているのですか?要件により、すべてがメモリに保持されます。結果を順番に保持することは、command2とcommand3が処理を開始する前に、最初にcommand1が完了する必要があることを意味します(したがって、おそらく入力全体を読み取って出力全体を印刷します)(最初にメモリに出力を収集する場合を除きます)。
frostschutz

あなたは正しいです、command2とcommand3の入力と出力は大きすぎてメモリに保持できません。一時ファイルを使用するよりも、スワップを使用する方が適切に機能すると予想していました。私が抱えている別の問題は、これがループで発生し、ファイルの処理がさらに難しくなることです。私は単一のファイルを使用していますが、現時点では何らかの理由で、ファイルの読み取りと書き込みに重複があり、それが無限に大きくなります。あまりにも多くの詳細であなたを退屈させることなく、質問を更新しようとします。
-Trylks

4
一時ファイルを使用する必要があります。入力echo HelloWorld > file; (command1<file;command2<file;command3<file)または出力のいずれかecho | tee cmd1 cmd2 cmd3; cat cmd1-output cmd2-output cmd3-output。それはちょうどそれがどのように機能するかです-teeはすべてのコマンドが並行して機能し処理する場合にのみ入力を分岐できます。もし1つのコマンドスリープ(あなたがインターリーブたくないので)それは単にそう入力してメモリを充填防止するために、すべてのコマンドをブロックします...
frostschutz

回答:


27

あなたはGNU stdbufのと組み合わせて使用することができますpeeからmoreutilsを

echo "Hello world!" | stdbuf -o 1M pee cmd1 cmd2 cmd3 > output

おしっこはpopen(3)これらの3つのシェルコマンドラインをfread入力し、次に入力を入力し、fwrite3つすべてを入力します。これらは最大1Mまでバッファーされます。

アイデアは、少なくとも入力と同じ大きさのバッファを持つことです。この方法では、3つのコマンドが同時に開始されますがpee pclose、3つのコマンドが順番に入力されたときにのみ入力が表示されます。

それぞれpclosepee、バッファをコマンドにフラッシュし、その終了を待ちます。これにより、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
  • いくつかのフィルタコマンド(cmd1cmd2cmd3
  • および集約コマンド(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

または、名前付きパイプを避けるために、zshcoprocを使用してもう少し痛いです:

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に入力がなくなるまで待機しているため、空にできません。
  • cmd1catからの入力を待機しているため、これ以上入力がないことはわかりませんtee
  • ブロックされているため、これ以上入力teeがないとは言えませんcmd1...など。

依存関係ループがあるため、デッドロックが発生しています。

さて、解決策は何ですか?より大きなパイプ3および4(すべてsrcの出力を含むのに十分な大きさ)がそれを行います。たとえば、最大1Gのデータを保存できる場所と場所を挿入してpv -qB 1G、それらを読み取って読み取ることで、これを実行できます。ただし、次の2つのことを意味します。teecmd2/3pvcmd2cmd3

  1. それは潜在的に多くのメモリを使用しており、さらにそれを複製しています
  2. cmd2実際にはcmd1が終了したときにのみデータの処理を開始するため、3つのコマンドすべてを連携させることはできません。

2番目の問題の解決策は、パイプ6と7も大きくすることです。それを仮定してcmd2cmd3消費するのと同じくらいの出力を生成すると、それはより多くのメモリを消費しません。

(第一の問題で)データの重複を回避するための唯一の方法は、上のバリエーションを実装していること、ディスパッチャ自体のデータの保持を実装することであろう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

デッドロックは、一時ファイルの使用を避けるためにこれまでに発見した最大の問題です。これらのファイルはかなり高速に見えますが、どこかにキャッシュされているかどうかはわかりません。ディスクへのアクセス時間は怖かったのですが、今のところは妥当と思われます。
-Trylks

6
+1 素敵なアスキーアートの追加:
カートPfeifle

3

提案することは、既存のコマンドでは簡単に実行できず、とにかくあまり意味がありません。パイプの全体的な考え方(|Unix / Linuxの場合)は、メモリバッファがいっぱいになるまで(最大)書き込み出力でcmd1 | cmd2、空になるまで(最大)バッファからデータの読み取りcmd1cmd2実行するというものです。すなわち、cmd1そしてcmd2同時に実行、それらの間の「飛行中の」データの限られた量よりも多く持つことが必要とされることはありません。複数の入力を単一の出力に接続する場合、リーダーの1つが他のリーダーよりも遅れている場合は、他のリーダーを停止するか(並列実行のポイントは何ですか?) (中間ファイルを持たないことのポイントは何ですか?)。 より複雑です。

Unixでの30年近くの経験では、このような複数出力パイプにとって本当に有益な状況を覚えていません。

あなただけではない任意のインターリーブされた方法(方法の出力をすべきで、今日1つのストリームに複数の出力を組み合わせることができますcmd1し、をcmd2インターリーブすること?順番に1行?何とか定義された10バイト?別の「段落」を書いてターンを取る?一つだけのdoesn」の場合長い間何も書いていないのですか?これはすべて処理が複雑です)。これは、例えばによって行われ(cmd1; cmd2; cmd3) | cmd4、プログラムcmd1cmd2cmd3次々に実行され、出力は入力として送信されますcmd4


3

あなたの重複の問題は、Linux(ととの上bashzshではなく付き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
)

各反復で新しいプロセスを取得する(...)代わりにの使用に注意してください。これ{...}により、新しいを指す新しいfd 3を持つことができますauxfile< /dev/fd/3削除されたファイルにアクセスするためのトリックです。これは、Linux以外のシステムでは動作しません< /dev/fd/3のようなものですdup2(3, 0)ので、0は、ファイルの末尾にカーソルが書き込み専用モードでオープンになりfdが。

ネストされた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

つまり、リダイレクトをネストしないでください。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.