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


回答:


118

コプロセスはksh機能です(既にksh88)。zshそれだけだけに追加されているが、開始(90年代初頭)から特徴を持っていたbash中で4.0(2009年)。

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

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

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

a | cmd | baデータをフィードしcmdbその出力を読み取ります。実行中の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>&-

そして、他のパイプの読み込み側を閉じます(1つ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)が、それを複製します。だから、給餌や読書パイプをクローズする明示的な方法はありません、他の起動、別の coproc

たとえば、供給側を閉じるには:

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以降)のような擬似ttyベースのコンストラクトがありexpectます。ほとんどのプログラムと対話するには、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を超える動的に割り当てられたファイル記述子を処理できる構文を提供します。

bash基本的な coproc構文と拡張された構文を提供します。

基本的な構文

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

coproc cmd

ksh又はzsh、および同時プロセスからパイプを用いてアクセスされる>&p<&p

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

コプロセスにデータをフィードします。

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

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

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

基本的な構文では、一度に1つのコプロセスのみを開始できます。

拡張構文

拡張構文では、コプロセスに名前を付けることができます(zshzptyコプロセスのように):

coproc mycoproc { cmd; }

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

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

あなたはに複数の共同プロセスを開始することができ、時間ができますが、ない 1はまだ(さえ非対話型モードで)実行しているときは、共同プロセスを起動したときに警告が表示されます。

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

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、それらのパイプのファイル記述子がクローズ(close-on-exec)としてマークされています。

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

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

yash本質的にコプロセス機能はありませんが、パイプラインおよびプロセスリダイレクト機能を使用して同じ概念を実装できます。システムコールyashへのインターフェースを持っているpipe()ので、この種のことはそこで手で比較的簡単に行うことができます。

あなたはコプロセスを開始します:

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

最初にpipe(4,5)(5の書き込み側、4の読み取り側)を作成し、次にfd 3をパイプにリダイレクトし、もう一方の端でstdinを使用して実行されるプロセスにリダイレクトします。次に、必要のない親のパイプの書き込み側を閉じます。そのため、シェルで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つのパイプを埋めるだけのデータが送信される場合にのみ。

expectそれが設計されているものよりも悪い動作

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

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

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

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

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

そのためexpectzpty代わりに擬似端末を使用します。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フラグが設定されていますが、それらから複製されたものはそうではありません(のように{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

注意:システム上で動作しないことをksh使用するsocketpairs代わりにpipes、どこで/dev/fd/nLinuxのように動作します。)

ではksh2コマンドラインで明示的に渡されない限り、上記のfds はclose-on-execフラグでマークされます。私たちが持つような未使用のファイルディスクリプタをクローズする必要はありません理由ですzsh-ブタたちがしなければならない理由、それはまた、だ{i1}>&$i1と使用evalその新しい値のため$i1に渡される、teecat...

ではbashあなたはclose-on-execフラグを避けることができないため、この、実行することはできません。

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

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

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

結論

あなたは、コマンド、使用と対話したい場合expect、またはzsh年代zpty、または名前付きパイプ。

パイプを使用して派手な配管を行いたい場合は、名前付きパイプを使用します。

コプロセスは上記のいくつかを実行できますが、些細ではないものについては深刻な頭をひっかくように準備してください。


確かに素晴らしい答えです。具体的にいつ修正されたかはわかりませんが、少なくともの時点で、補助を必要とせずにcoprocファイル記述子を直接閉じるbash 4.3.11ことができます。変数; あなたの答えの例の観点でexec {tr[1]}<&- 、今(coprocの標準入力を閉じるために、あなたのコードは、(間接的に)閉じようとすることに注意して動作します{tr[1]}使用して>&-、しかし{tr[1]}coprocのある標準入力、及びで閉じる必要があります<&-)。修正4.2.25は、問題がまだ発生している、と発生し4.3.11ていないの間にあるはずです。
mklement0

1
@ mklement0、ありがとう。exec {tr[1]}>&-確かに新しいバージョンで動作するようで、CWRU / changelogエントリで参照されています{array [ind]}のような単語が有効なリダイレクトを許可する... 2012-09-01)。exec {tr[1]}<&-(または、より適切な>&-同等のものはclose()、両方を呼び出すだけなので違いはありません)、coprocのstdinを閉じませんが、そのcoprocへのパイプの書き込み側を閉じます。
ステファンシャゼル

1
@ mklement0、良い点、私はそれを更新して追加しましたyash
ステファンシャゼル

1
利点の1つmkfifoは、競合状態やパイプアクセスのセキュリティについて心配する必要がないことです。fifoのデッドロックを心配する必要があります。
オテウス

1
デッドロックについて:stdbufコマンドは、少なくともそれらのいくつかを防ぐのに役立ちます。Linuxおよびbashで使用しました。とにかく、@StéphaneChazelasが結論に正しいと信じています。名前付きパイプに戻ったときだけ、「頭の引っ掻き」フェーズが終了しました。
ハブ

7

コプロセスは、シェルを使用したシェルスクリプト言語で最初に導入されksh88(1988年)、その後zsh1993年以前のある時点で導入されました。

kshでコプロセスを起動する構文はcommand |&です。そこから開始して、でcommand標準入力に書き込み、print -pその標準出力を読み取ることができます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

-1

「coproc」とは何ですか?

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

zshでcoprocを開始します

coproc command

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

この記事を読んで、ここでそれは、execとcoproc間のケーススタディを提供


回答に記事の一部を追加できますか?このトピックはU&Lでカバーされているように見えました。ご回答有難うございます!また、タグをzshではなくBashに設定していることに注意してください。
slm

@slmあなたはすでにBashハッカーを指摘しました。十分な例を見ました。あなたの意図がこの質問に注意を向けることであるなら、あなたは成功しました:>
バレンティンバジラミ

それらは特別な種類のパイプではなく、で使用されるものと同じパイプ|です。(つまり、ほとんどのシェルでパイプを使用し、ksh93でソケットペアを使用します)。パイプとソケットペアは先入れ先出しで、すべてFIFOです。mkfifo名前付きパイプを作成し、コプロセスは名前付きパイプを使用しません。
ステファンシャゼル

@slm zshでごめんなさい...実際に私はzshで働いています。私は時々フローでそれをする傾向があります。それは...あまりにもbashで正常に動作します
MunaiダスUdasin

@ステファンChazelas私は...私はそれがI / Oだとどこかでそれを読んでは、FIFOと呼ばれるパイプの特殊な種類の接続されていることをかなり確信している
MunaiダスUdasin

-1

もう1つの良い(そして機能する)例があります-BASHで書かれたシンプルなサーバーです。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)
$
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.