少量のデータのためのリーンソリューション

少量のデータのためのリーンソリューション

Bashコードから

command1 | tee >(command2) | command3

command2inの出力とvar2inの出力をキャプチャしたいと思います。command3var3

command1他のコマンドはI / O制約のため高価ですが、command1完了する前に作業を開始できます。

command2合計の出力順序command3は固定されていません。だから私はファイル記述子を使ってみました。

read -r var2 <<< var3=(command1 | tee >(command2 >&3) | command3) 3>&1

または

{read -u 3 -r var2; read -r var3} <<< command1 | tee >(command2 >&3) | command3

しかし、成功しませんでした。

これら3つのコマンドを並列に実行して一時ファイルを作成せずに結果を別の変数に保存する方法はありますか?

答え1

それでは、cmd1の出力をcmd2とcmd3にパイプし、cmd2とcmd3の出力を別の変数にインポートしますか?

したがって、シェルには2つのパイプが必要なようです。 1つはcmd2の出力に、もう1つはcmd3の出力に接続され、シェルは両方のパイプでselect()/を使用します。poll()

bashそうしません。などのより高いレベルのシェルが必要ですzshzshネイティブインタフェースはありませんが、pipe()Linuxでは通常のパイプの名前付きパイプとして機能するという事実を活用し、/dev/fd/x以下のようなものを使用できます。シェルリダイレクトを使用して同じファイル記述子を読み書きする

#! /bin/zsh -

cmd1() seq 20
cmd2() sed 's/1/<&>/g'
cmd3() tr 0-9 A-J

zmodload zsh/zselect
zmodload zsh/system
typeset -A done out
{
  cmd1 > >(cmd2 >&3 3>&-) > >(cmd3 >&5 5>&-) 3>&- 5>&- &
  exec 4< /dev/fd/3 6< /dev/fd/5 3>&- 5>&-
  while ((! (done[4] && done[6]))) && zselect -A ready 4 6; do
    for fd (${(k)ready[(R)*r*]}) {
      sysread -i $fd && out[$fd]+=$REPLY || done[$fd]=1
    }
  done
} 3> >(:) 5> >(:)

printf '%s output: <%s>\n' cmd2 "$out[4]" cmd3 "$out[6]"

答え2

すべての要件を理解している場合は、bash各コマンドに対して名前のないパイプを作成し、各コマンドの出力をその名前のないパイプにリダイレクトし、最後にRetrieveの各出力を別々の変数にパイピングしてこれを達成します。できます。

したがって、解決策は次のとおりです。

: {pipe2}<> <(:)
: {pipe3}<> <(:)

command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2=$(while read -ru ${pipe2} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)
var3=$(while read -ru ${pipe3} line ; do [ "${line}" = EOF ] && break ; echo "${line}" ; done)

exec {pipe2}<&- {pipe3}<&-

ここで特別な注意が必要なのは次のとおりです。

  • この構造の使用法<(:)は、「名前なし」パイプを開く文書化されていないBashトリックです。
  • 出力がなくなったことをループにecho EOF知らせるには、簡単な方法を使用します。whileこれは、名前のないパイプ(通常はすべてのループが終了するwhile read)を閉じるだけでは役に立たないので必要です。これは、書き込みと読み取りの両方の双方向であるためです。読み取り用と書き込み用に1つずつ、一般的ないくつかのファイル記述子に開くか変換する方法がわかりません。

この例では、純粋なbashアプローチを使用しました(各変数3をtee除くsedwhilevar2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"

少量のデータのためのリーンソリューション

: {pipe2}<> <(:)
: {pipe3}<> <(:)

command1 | tee >({ command2 ; echo EOF ; } >&${pipe2}) >({ command3 ; echo EOF ; } >&${pipe3}) > /dev/null &
var2="$(sed -ne '/^EOF$/q;p' <&${pipe2})"
var3="$(sed -ne '/^EOF$/q;p' <&${pipe3})"

exec {pipe2}<&- {pipe3}<&-

ターゲット変数を表示するには、次のようにIFSをクリアしてトークン化を無効にする必要があります。

IFS=
echo "${var2}"
echo "${var3}"

それ以外の場合、出力で改行文字が失われます。

上記は非常にきれいな解決策のようです。残念ながら、高すぎない出力にのみ使用でき、マイレージは異なる場合があります。テストでは約530kの出力に問題がありました。 4k制限内であれば(非常に保守的に)大丈夫でしょう。

この制限の理由は、これらのタスクが2つあるという事実にあります。コマンドの置き換え構文は同期操作です。言い換えれば、tee2つの命令を同時に提供し、命令が受信バッファを満たすときにすべての命令をブロックするのとは対照的に、第1の割り当てが完了した後にのみ第2の割り当てが実行される。二重ロック。

この問題を解決するには、両方のバッファを同時に消去するには、少し複雑なスクリプトが必要です。これを行うには、while両方のパイプを繰り返します。会議便利に来てください。

すべてのデータボリュームに対するより標準的なソリューション

より標準的なBashismは次のとおりです。

declare -a var2 var3
while read -r line ; do
   case "${line}" in
   cmd2:*) var2+=("${line#cmd2:}") ;;
   cmd3:*) var3+=("${line#cmd3:}") ;;
   esac
done < <(
   command1 | tee >(command2 | stdbuf -oL sed -re 's/^/cmd2:/') >(command3 | stdbuf -oL sed -re 's/^/cmd3:/') > /dev/null
)

ここでは、2つのコマンドの行を単一の標準の「stdout」ファイル記述子に多重化し、出力を各対応する変数にマージして逆多重化します。

注意:

  • インデックス配列をターゲット変数として使用する:これは、通常の変数に追加するだけで出力量が多くなると速度が非常に遅くなるためです。
  • コマンドを使用してsed各出力行の前に文字列「cmd2:」または「cmd3:」(それぞれ)を追加すると、スクリプトは各行がどの変数に属するかを知ることができます。
  • コマンドを出力するためのラインバッファリングの必要な使用stdbuf -oL:これは、2つのコマンドが同じ出力ファイル記述子を共有するためです。したがって、ストリーミングしている場合は、最も一般的な競合状態で同時に互いの出力を簡単に上書きできます。出力データラインバッファ出力はこれを防ぐのに役立ちます。
  • また、各チェーンの最後のコマンドのみをstdbuf、つまり共有ファイル記述子に直接出力するコマンド(この場合は、各commandXの出力の前に一意のプレフィックスを追加するsedコマンド)を使用する必要があります。

これらのインデックス配列を正しく表示する安全な方法は次のとおりです。

for ((i = 0; i < ${#var2[*]}; i++)) ; do
   echo "${var2[$i]}"
done

もちろん直接使用することもできます"${var2[*]}"

echo "${var2[*]}"

しかし、ラインが多ければ効率はそれほど高くありません。

答え3

うまく動作するようなものを見つけました。

exec 3<> <(:)
var3=$(command1 | tee >(command2 >&3) | command3)
var2=$(while IFS= read -t .01 -r -u 3 line; do printf '%s\n' "$line"; done)

<(:)ファイル記述子3の匿名パイプを設定し、command2その出力をパイプする方法で動作します。ファイル記述子3の出力をキャプチャvar3し、0.01秒間新しいデータが受信されなくなるまで最後の行を読み取ります。command3

command2匿名パイプによってバッファリングされているように見える最大65536バイトの出力でのみ機能します。

ソリューションの最後の行が気に入らない。 0.01秒待ってからバッファがビザすぐに停止するよりも、一度にすべての内容を読むほうが良いです。しかし、より良い方法はわかりません。

答え4

4.0からは可能です。 bashはシェル予約語coprocを追加します。バックグラウンドでコマンドに従うために子プロセスに分岐しますが(通常は変数渡しは許可されていません)、配列を作成します(デフォルトはCOPROCですが名前を指定できます)。 ${COPROC[0]} は、サブプロセス ${COPROC の標準入力に接続されます。1これらのプロセスはジョブとして機能し、非同期なので、teeを使用して2つのコルーチンにパイプし、別々のファイルに出力してから、「$ {コルーチンを最初に呼び出すことができます。1} ${コルーチン秒1これはパイプの中にある必要はないのでとても良いです。

bash coproc command_1 { command1 arg1 arg2 arg3 >> command_1_output.txt } coproc command_2 { command2 arg1 arg2 arg3 >> command_2_output.txt } othercommand tee>^${command_11}" >&"${command_21}"読み取り-r結果1 <&"${command_1[0]"読み取り-r resluts2 <&"${command_2[0]" echo "$result1 $result2" | command3 >>結果マージ.txt

述べたように、この解決策はまだ完成していないのでショックを受けましたが、原則は妥当であり、上記の答えと似ています。しかし、トピックに関する良い記事を案内し、作業が可能なときにこの回答に戻ります。

coprocessの変数設定例

コルーチンと名前付きパイプの詳細

用途と罠

関連情報