web-dev-qa-db-ja.com

さまざまなシェルでcoprocコマンドをどのように使用しますか?

誰かが coproc の使用方法の例をいくつか提供できますか?

84
slm

コプロセスは、kshの機能です(ksh88には既に含まれています)。 zshには最初から機能があり(90年代前半)、4.0(2009)でbashに追加されたばかりです。

ただし、動作とインターフェイスは3つのシェル間で大きく異なります。

ただし、考え方は同じです。ジョブをバックグラウンドで開始し、名前付きパイプに頼ることなく、入力を送信してその出力を読み取ることができます。

これは、一部のシステムでは最近のバージョンのksh93のほとんどのシェルとソケットペアで名前なしパイプを使用して行われます。

a | cmd | bでは、aがデータをcmdにフィードし、bがその出力を読み取ります。 cmdをコプロセスとして実行すると、シェルをabの両方にすることができます。

kshコプロセス

kshでは、次のようにコプロセスを開始します。

cmd |&

次のようなことを行って、cmdにデータをフィードします。

echo test >&p

または

print -p test

そして、cmdの出力を次のように読みます。

read var <&p

または

read -p var

cmdはバックグラウンドジョブとして開始されます。fgbgkillを使用して、%job-numberまたは$!で参照できます。

cmdが読み取っているパイプの書き込み側を閉じるには、次のようにします。

exec 3>&p 3>&-

そして、もう一方のパイプ(cmdが書き込んでいるパイプ)の読み取り側を閉じるには:

exec 3<&p 3<&-

最初にパイプファイル記述子を他のfdsに保存しない限り、2番目のコプロセスを開始することはできません。例えば:

tr a b |&
exec 3>&p 4<&p
tr b c |&
echo aaa >&3
echo bbb >&p

zshコプロセス

zshでは、コプロセスはkshのコプロセスとほぼ同じです。実際の唯一の違いは、zshコプロセスがcoprocキーワードで開始されることです。

coproc cmd
echo test >&p
read var <&p
print -p test
read -p var

実行:

exec 3>&p

注:これは、coprocファイル記述子をfd 3に移動しません(kshのように)が、それを複製します。したがって、フィードパイプまたは読み取りパイプを閉じる明示的な方法はありません。その他の開始anothercoproc

たとえば、フィードエンドを閉じるには:

coproc tr a b
echo aaaa >&p # send some data

exec 4<&p     # preserve the reading end on fd 4
coproc :      # start a new short-lived coproc (runs the null command)

cat <&4       # read the output of the first coproc

パイプベースのコプロセスに加えて、zsh(2000年にリリースされた3.1.6-dev19以降)には、expectのような疑似ttyベースの構成があります。ほとんどのプログラムと対話するには、kshスタイルのコプロセスは機能しません。プログラムが出力をパイプするとバッファリングが開始されるためです。

下記は用例です。

コプロセスを開始x

zmodload zsh/zpty
zpty x cmd

(ここでは、cmdは単純なコマンドです。ただし、evalまたは関数を使用して、より洗練されたことができます。)

コプロセスデータをフィード:

zpty -w x some data

コプロセスデータを読み取ります(最も単純な場合):

zpty -r x var

expectと同様に、コプロセスからの特定のパターンに一致する出力を待つことができます。

bashコプロセス

Bash構文はかなり新しく、ksh93、bash、およびzshに最近追加された新機能の上に構築されています。 10を超える動的に割り当てられたファイル記述子の処理を可能にする構文を提供します。

bashbasiccoproc構文とextended構文を提供します。

基本的な構文

コプロセスを開始するための基本的な構文は、zsh 's:のようになります。

coproc cmd

kshまたはzshでは、コプロセスとの間のパイプは>&pおよび<&pを使用してアクセスされます。

しかし、bashでは、コプロセスからのパイプとコプロセスへのパイプのファイル記述子が$COPROC配列に返されます(それぞれ${COPROC[0]}${COPROC[1]}。それで…

コプロセスへのデータのフィード:

echo xxx >&"${COPROC[1]}"

コプロセスからデータを読み取ります:

read var <&"${COPROC[0]}"

基本的な構文では、一度に開始できるコプロセスは1つだけです。

拡張構文

拡張構文では、次のことができますnameコプロセス(zsh zptyコプロセスのように):

coproc mycoproc { cmd; }

コマンドhasは複合コマンドになります。 (上の例がfunction f { ...; }を連想させることに注意してください。)

今回は、ファイル記述子は${mycoproc[0]}および${mycoproc[1]}にあります。

一度に複数のコプロセスを開始できますが、doコプロセスがまだ実行されているときに(非インタラクティブモードであっても)コプロセスを開始すると警告が表示されます。

拡張構文を使用する場合は、ファイル記述子を閉じることができます。

coproc tr { tr a b; }
echo aaa >&"${tr[1]}"

exec {tr[1]}>&-

cat <&"${tr[0]}"

その方法を閉じることは、4.3より前のバージョンのbashでは機能せず、代わりに記述する必要があることに注意してください。

fd=${tr[1]}
exec {fd}>&-

kshおよびzshと同様に、これらのパイプファイル記述子は、exec-onとしてマークされます。

しかし、bashでは、それらを実行されたコマンドに渡す唯一の方法は、それらをfds 01、または2に複製することです。これにより、1つのコマンドで操作できるコプロセスの数が制限されます。 (例については、以下を参照してください。)

yashプロセスとパイプラインリダイレクト

yash 自体にはコプロセス機能はありませんが、同じ概念をパイプラインおよびプロセスリダイレクト機能で実装できます。 yashpipe()システムコールへのインターフェイスを備えているため、この種のことは手動で比較的簡単に行うことができます。

あなたはコプロセスを始めるでしょう:

exec 5>>|4 3>(cmd >&5 4<&- 5>&-) 5>&-

これは最初にpipe(4,5)(5書き込み側、4読み取り側)を作成し、次にfd 3をパイプにリダイレクトして、そのstdinをもう一方の端で実行するプロセスにリダイレクトし、作成されたパイプにstdoutを送信します。ついさっき。次に、必要のない親でそのパイプの書き込み側を閉じます。したがって、シェルでは、fd 3をcmdのstdinに接続し、fd 4をcmdのstdoutにパイプで接続しています。

これらのファイル記述子には、close-on-execフラグが設定されていないことに注意してください。

データをフィードするには:

echo data >&3 4<&-

データを読み取るには:

read var <&4 3>&-

そして、あなたはいつものようにfdsを閉じることができます:

exec 3>&- 4<&-

さて、なぜそれほど人気が​​ないのか

名前付きパイプを使用するよりもメリットはほとんどありません

コプロセスは、標準の名前付きパイプで簡単に実装できます。正確に名前が付けられたパイプがいつ導入されたかはわかりませんが、kshがコプロセスを考案した後だった可能性があります(おそらく80年代半ばに、ksh88が88に「リリース」されましたが、kshがAT&Tで内部的に使用されていたと思いますその数年前に)理由を説明します。

cmd |&
echo data >&p
read var <&p

で書くことができます:

mkfifo in out

cmd <in >out &
exec 3> in 4< out
echo data >&3
read var <&4

これらの操作は、特に複数のコプロセスを実行する必要がある場合は特に簡単です。 (以下の例を参照してください。)

coprocを使用する唯一の利点は、使用後にこれらの名前付きパイプをクリーンアップする必要がないことです。

デッドロックが発生しやすい

シェルはいくつかの構成でパイプを使用します:

  • シェルパイプ:cmd1 | cmd2
  • コマンド置換:$(cmd)
  • およびプロセス置換:<(cmd)>(cmd)

これらの場合、データは異なるプロセス間で1方向にのみ流れます。

ただし、コプロセスと名前付きパイプを使用すると、デッドロックが発生しやすくなります。 1つが開いたままでプロセスが生きていることを防ぐために、どのコマンドがどのファイル記述子を開いているかを追跡する必要があります。デッドロックは非決定的に発生する可能性があるため、調査が難しい場合があります。たとえば、1つのパイプをいっぱいにするだけの量のデータが送信された場合のみです。

設計された目的でexpectよりも動作が悪い

コプロセスの主な目的は、コマンドを操作する方法をシェルに提供することでした。ただし、うまく機能しません。

上記の最も単純なデッドロックの形式は次のとおりです。

tr a b |&
echo a >&p
read var<&p

出力は端末に送信されないため、trは出力をバッファリングします。したがって、stdinでファイルの終わりを検出するか、出力するためにバッファーでいっぱいのデータを蓄積するまで、何も出力しません。したがって、上記では、シェルがa\n(2バイトのみ)を出力した後、readがシェルからデータを送信するのを待機しているため、trは無期限にブロックされます。

つまり、パイプはコマンドとのやり取りには適していません。コプロセスは、出力をバッファリングしないコマンドと対話するためにのみ使用できますor出力をバッファリングしないように指示できるコマンド。たとえば、最近のGNUまたはFreeBSDシステムでは、一部のコマンドでstdbufを使用します。

そのため、expectまたはzptyは代わりに疑似ターミナルを使用します。 expectは、コマンドを操作するために設計されたツールで、うまく機能します。

ファイル記述子の処理は面倒で、正しく行うのが難しい

コプロセスを使用すると、単純なシェルパイプが許可するよりも複雑な配管を行うことができます。

他のUnix.SEの回答 には、coprocの使用例があります。

これは簡単な例です:コマンドの出力のコピーを他の3つのコマンドにフィードし、それらの3つのコマンドの出力を持つ関数が必要だと想像してください。連結されます。

すべてパイプを使用しています。

たとえば、printf '%s\n' foo barの出力をtr a bsed 's/./&&/g'cut -b2-にフィードして、次のようなものを取得します。

foo
bbr
ffoooo
bbaarr
oo
ar

最初に、それは必ずしも明白ではありませんが、そこでデッドロックが発生する可能性があり、数キロバイトのデータの後にのみ発生し始めます。

次に、シェルに応じて、さまざまな対処が必要なさまざまな問題を実行します。

たとえば、zshでは、次のようにします。

f() (
  coproc tr a b
  exec {o1}<&p {i1}>&p
  coproc sed 's/./&&/g' {i1}>&- {o1}<&-
  exec {o2}<&p {i2}>&p
  coproc cut -c2- {i1}>&- {o1}<&- {i2}>&- {o2}<&-
  tee /dev/fd/$i1 /dev/fd/$i2 >&p {o1}<&- {o2}<&- &
  exec cat /dev/fd/$o1 /dev/fd/$o2 - <&p {i1}>&- {i2}>&-
)
printf '%s\n' foo bar | f

上記では、コプロセスfdsにはclose-on-execフラグが設定されていますが、notそれらから複製されたものです({o1}<&pの場合など)。したがって、デッドロックを回避するには、デッドロックを必要としないプロセスで確実にクローズされるようにする必要があります。

同様に、サブシェルを使用し、最後にexec catを使用して、パイプを開いたままにするシェルプロセスが存在しないようにする必要があります。

ksh(ここではksh93)では、次のようにする必要があります。

f() (
  tr a b |&
  exec {o1}<&p {i1}>&p
  sed 's/./&&/g' |&
  exec {o2}<&p {i2}>&p
  cut -c2- |&
  exec {o3}<&p {i3}>&p
  eval 'tee "/dev/fd/$i1" "/dev/fd/$i2"' >&"$i3" {i1}>&"$i1" {i2}>&"$i2" &
  eval 'exec cat "/dev/fd/$o1" "/dev/fd/$o2" -' <&"$o3" {o1}<&"$o1" {o2}<&"$o2"
)
printf '%s\n' foo bar | f

注:kshsocketpairsの代わりにpipesを使用し、Linuxのように/dev/fd/nが機能するシステムでは機能しません。)

kshでは、2の上のfdsは、コマンドラインで明示的に渡されない限り、close-on-execフラグでマークされます。そのため、zshのように未使用のファイル記述子を閉じる必要はありませんが、{i1}>&$i1を実行し、$i1の新しい値に対してevalを使用して、teeおよびcat…に渡す必要があるのもこのためです。

bashでは、exec-onフラグを回避できないため、これを行うことはできません。

上記では、単純な外部コマンドのみを使用しているため、比較的単純です。シェル構造を代わりに使用したい場合、さらに複雑になり、シェルのバグに遭遇し始めます。

上記を、名前付きパイプを使用して同じものと比較します。

f() {
  mkfifo p{i,o}{1,2,3}
  tr a b < pi1 > po1 &
  sed 's/./&&/g' < pi2 > po2 &
  cut -c2- < pi3 > po3 &

  tee pi{1,2} > pi3 &
  cat po{1,2,3}
  rm -f p{i,o}{1,2,3}
}
printf '%s\n' foo bar | f

結論

コマンドを操作する場合は、expectzshzpty、または名前付きパイプを使用します。

パイプを使っておしゃれな配管をしたい場合は、名前付きパイプを使用してください。

コプロセスは上記の一部を実行できますが、重要なものを真剣にスクラッチする準備をしてください。

124

コプロセスは、最初にksh88シェル(1988)を含むシェルスクリプト言語で導入され、1993年より前のある時点でzshで導入されました。

Kshでコプロセスを起動する構文はcommand |&です。そこから始めて、print -pを使用してcommand標準入力に書き込み、read -pを使用してその標準出力を読み取ることができます。

数十年後、この機能が欠けていたbashが4.0リリースでついに導入されました。残念ながら、互換性がなく、より複雑な構文が選択されました。

Bash 4.0以降では、coprocコマンドを使用してコプロセスを起動できます。例:

$ coproc awk '{print $2;fflush();}'

次に、その方法でコマンドstdinに何かを渡すことができます。

$ echo one two three >&${COPROC[1]}

そしてawk出力を次のように読みます:

$ read -ru ${COPROC[0]} foo
$ echo $foo
two

Kshでは、それは次のようになります。

$ awk '{print $2;fflush();}' |&
$ print -p "one two three"
$ read -p foo
$ echo $foo
two
7
jlliagre

「coproc」とは何ですか?

これは「コプロセス」の略で、シェルと連携する2番目のプロセスを意味します。これは、コマンドの最後に「&」で始まるバックグラウンドジョブと非常に似ていますが、親シェルと同じ標準入出力を共有する代わりに、標準I/Oが特別な方法で親シェルに接続されている点が異なります。 FIFOと呼ばれるパイプの一種です。参考までに ここをクリック

1つはzshでcoprocを開始します:

coproc command

コマンドは、stdinからの読み取りやstdoutへの書き込み、あるいはその両方を行う準備ができている必要があります。そうでない場合、coprocとしてはあまり役に立ちません。

この記事を読む ここ execとcoprocの間のケーススタディを提供します

0

次に、BASHで記述された単純なサーバーであるもう1つの良い(そして機能する)例を示します。 OpenBSDのnetcatが必要になることに注意してください。古典的なものは機能しません。もちろん、unixソケットの代わりにinetソケットを使用することもできます。

server.sh

#!/usr/bin/env bash

SOCKET=server.sock
PIDFILE=server.pid

(
    exec </dev/null
    exec >/dev/null
    exec 2>/dev/null
    coproc SERVER {
        exec nc -l -k -U $SOCKET
    }
    echo $SERVER_PID > $PIDFILE
    {
        while read ; do
            echo "pong $REPLY"
        done
    } <&${SERVER[0]} >&${SERVER[1]}
    rm -f $PIDFILE
    rm -f $SOCKET
) &
disown $!

client.sh

#!/usr/bin/env bash

SOCKET=server.sock

coproc CLIENT {
    exec nc -U $SOCKET
}

{
    echo "$@"
    read
} <&${CLIENT[0]} >&${CLIENT[1]}

echo $REPLY

使用法:

$ ./server.sh
$ ./client.sh ping
pong ping
$ ./client.sh 12345
pong 12345
$ kill $(cat server.pid)
$
0