コマンド置換時の coproc と名前付きパイプの動作

コマンド置換時の coproc と名前付きパイプの動作

同じコマンド置換の後続の呼び出しと状態を渡すコマンド置換を介して呼び出されるzshシェルスクリプトから関数を生成する必要があります。

C関数の静的変数に似ています(おおよその)。

この目的のために、私は補助プロセッサを使用する方法と名前付きパイプを使用する方法の2つのアプローチを試しました。名前付きパイプ方法を操作できません。セカンダリプロセッサに関連する唯一の問題を解決すると思ったので、残念です。つまり、端末で新しいzshシェルを入力すると、親のzshセッションのcoprocを見ることができるようです。

以下の問題を説明するために、単純化されたスクリプトを作成しました。私が何をしたいのか疑問に思うなら、置き換えられたスクリプトでbuild_prompt()関数を呼び出す弾丸列車zshテーマに新しいステートフルコンポーネントを追加することです。コマンドはここにあります: https://github.com/caiogondim/bullet-train.zsh/blob/d60f62c34b3d9253292eb8be81fb46fa65d8f048/bullet-train.zsh-theme#L692

スクリプト1 - 補助プロセッサ

#!/usr/bin/env zsh

coproc cat
disown
print 'Hello World!' >&p

call_me_from_cmd_subst() {
    read get_contents <&p
    print "Retrieved: $get_contents"
    print 'Hello Response!' >&p
    print 'Response Sent!'
}

# Run this first
call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
#print "$(call_me_from_cmd_subst)"

# Hello Response!
read finally <&p
echo $finally

スクリプト2 - 名前付きパイプ

#!/usr/bin/env zsh

rm -rf /tmp/foo.bar
mkfifo /tmp/foo.bar
print 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    get_contents=$(cat /tmp/foo.bar)
    print "Retrieved: $get_contents"
    print 'Hello Response!' > /tmp/foo.bar &!
    print 'Response Sent!'
}

# Run this first
call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
#print "$(call_me_from_cmd_subst)"

# Hello Response!
cat /tmp/foo.bar

初期フォーマットでは、どちらもまったく同じ出力を生成します。

$ ./named-pipe.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!

$ ./coproc.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!

これで、コマンドの置き換えを使用して呼び出されるようにcoprocスクリプトを切り替えても、何も変わりません。

# Run this first
#call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"

つまり、コマンド置換によって生成された子プロセスでコプロセスを読み書きすることは問題を引き起こしません。私はこの事実に少し驚きました。しかし、良いニュースです!

ただし、名前付きパイプの例で同じ変更を実行すると、スクリプトがブロックされ、出力はありません。私がなぜそれで実行したのかを調べるには、zsh -x次のようにします。

+named-pipe.zsh:3> rm -rf /tmp/foo.bar
+named-pipe.zsh:4> mkfifo /tmp/foo.bar
+named-pipe.zsh:15> call_me_from_cmd_subst
+call_me_from_cmd_subst:1> get_contents=+call_me_from_cmd_subst:1> cat /tmp/foo.bar
+named-pipe.zsh:5> print 'Hello World!'
+call_me_from_cmd_subst:1> get_contents='Hello World!'
+call_me_from_cmd_subst:2> print 'Retrieved: Hello World!'
+call_me_from_cmd_subst:4> print 'Response Sent!'

次の行がまだ終了していない間にコマンド置換によって生成されたサブプロセスは終了しないようです(私はusingを使用しており、結果&に変更はありません)。&!disown

print 'Hello Response!' > /tmp/foo.bar &!

これを実証するために、猫が応答を読み取るように手動でトリガーできます。

$ cat /tmp/foo.bar
Hello Response!

これでパイプに読み込む内容がないので、スクリプトは最後のcatコマンドを待ちます。


私の質問は次のとおりです

  1. コマンド置換があるときにコプロセスと同じように動作するように名前付きパイプを設定することは可能ですか?
  2. coprocessが明らかに子プロセスから読み書きできますが、zshコンソールで(入力を介して)手動でサブシェルを作成すると、子プロセスとは無関係にアクセスできなくなる理由を説明できますか?その親と終了し、継続して親を使用します! )。
  3. 1が可能であれば、名前付きパイプが特定のシェルプロセスに接続されていないため、名前付きパイプは2ほど複雑ではないと思いますか?

2と3の意味を説明してください。

$ coproc cat
[1] 24516
$ print -p test
$ read -ep
test
$ print -p test_parent
$ zsh
$ print -p test_child
print: -p: no coprocess
$ coproc cat
[1] 28424
$ disown
$ print -p test_child
$ read -ep
test_child
$ exit
$ read -ep
test_parent

子zshの内部ではコプロセスを見ることはできませんが、コマンド置換を使用して子プロセスの内部で見ることはできますか?

最後に、Ubuntu 18.04を使用しました。

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)

答え1

パイプベースのスクリプトが機能しない理由はzshの一部の機能ではありません。これは、シェルコマンドの置換、シェルリダイレクト、およびパイプの動作方法によるものです。重複する部分がないスクリプトです。

mkfifo /tmp/foo.bar
echo 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    echo 'Hello Response!' > /tmp/foo.bar &
    echo 'Response Sent!'
}

echo "$(call_me_from_cmd_subst)"
cat /tmp/foo.bar

コマンド置換は、$(call_me_from_cmd_subst)関数を実行するサブシェルの出力を元のシェルプロセスに接続する匿名パイプを生成します。元のプロセスはこのパイプからデータを読み取ります。子プロセスは実行する孫プロセスを作成しますecho 'Hello Response!' > /tmp/foo.bar。どちらのプロセスも、匿名パイプを含む同じオープンファイルで始まります。 Sun Tzuはリダイレクトを実行します> /tmp/foo.bar。名前付きパイプから何も読み取れないためブロックされます/tmp/foo.bar

リダイレクトは、ファイルを開くときにそのファイル記述子を選択できないため、2段階のプロセスです(実際には3段階ですが、ここでは3番目のステップは重要ではありません)。オペレータは>標準出力をリダイレクトしようとします。つまり、特定のファイルをファイル記述子1にリンクしようとします。これには3つのシステムコールが必要です。

  1. fd = open("/tmp/foo.bar", O_RDWR)ファイルを開くために呼び出されます。ファイルは、fd現在のプロセスで使用されていない一部のファイル記述子で開きます。これは、名前付きパイプから何かを読み始めるまでブロックするステップです/tmp/foo.bar。誰も聞いていない場合は、名前付きパイプを開くことがブロックされます。
  2. dup2(fd, 1)カーネルは、選択されたファイル記述子に加えて、必要なファイル記述子からファイルを開くために呼び出されます。新しい記述子(1)(コマンド置換のための匿名パイプ)に開いている項目がある場合は、この時点で閉じます。
  3. close(fd)必須ファイル記述子のみにリダイレクト先を保持するために呼び出されます。

同時にサブプロセスが印刷されReponse Sent!、終了します。元のシェルプロセスはまだパイプからデータを読み取っています。孫に書き込むためにパイプがまだ開いているので、元のシェルプロセスは待機し続けます。

このデッドロックを解決するには、孫が必要以上にパイプを開いたままにしないようにする必要があります。たとえば、

call_me_from_cmd_subst() {
    { exec >&-; /bin/echo 'Hello Response!' > /tmp/foo.bar; } &
    echo 'Response Sent!'
}

または

call_me_from_cmd_subst() {
    { echo 'Hello Response!' > /tmp/foo.bar; } >/dev/null &
    echo 'Response Sent!'
}

または、そのトピックにさまざまなバリエーションがあります。

コプロセスには名前付きパイプは含まれていないため、デッドロックの半分はブロックされません。>/tmp/foo.bar名前付きパイプを開くとブロックされますが、すでに>&p開いているファイルをリダイレクトするため、ブロックされません。記述子。

答え2

コードを少し変更すると、期待どおりに機能します。

#!/usr/bin/env zsh

rm -rf /tmp/foo.bar
mkfifo /tmp/foo.bar
print 'Hello World!' > /tmp/foo.bar &

call_me_from_cmd_subst() {
    get_contents=$(cat /tmp/foo.bar)
    print "Retrieved: $get_contents"
    (print 'Hello Response!' > /tmp/foo.bar &!) >/dev/null
    print 'Response Sent!'
}

# Run this first
#call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"

# Hello Response!
cat /tmp/foo.bar

このコマンドを実行すると、予想される結果が生成されます。

Retrieved: Hello World!
Response Sent!
Hello Response!

なぜそうなのかよく分からない。 zshがfifoで印刷すると、stdoutで何らかの出力が生成されると思うようです。

関連情報