web-dev-qa-db-ja.com

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

たとえば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の読み取りと書き込みが重なっているように見え、すべてが爆発します。

18
Trylks

GNU stdbufとpee from moreutils の組み合わせを使用できます:

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

pee popen(3) sこれらの3つのシェルコマンドライン、次にfreads入力、fwrites入力3つすべて、最大1Mまでバッファリングされます。

アイデアは、少なくとも入力と同じ大きさのバッファを持つことです。この方法では、3つのコマンドが同時に開始されても、peepclosesで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つのことを意味します。

  1. それは潜在的に大量のメモリを使用し、さらにそれを複製しています
  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
_
27

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

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

今日、複数の出力を1つのストリームに組み合わせることができますが、インターリーブの方法ではありません(cmd1cmd2の出力をどのようにインターリーブしますか?1行ずつ交互に入れ替えますか?交互に10バイトを書き込みますか?代替 "段落"どういうわけか定義されていますか?そして、長い間何も書かない場合は?これはすべて処理が複雑です)。それは、例えばによって行われます。 (cmd1; cmd2; cmd3) | cmd4、プログラムcmd1cmd2、およびcmd3が次々に実行され、出力がcmd4への入力として送信されます。

3
vonbrand

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
_

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

3