命令替換下的 coproc 和命名管道行為
我需要在 zsh shell 腳本中創建一個函式,該函式由命令替換呼叫,與後續呼叫相同的命令替換通信狀態。
類似於 C 函式中的靜態變數(非常粗略地說)。
為此,我嘗試了兩種方法——一種使用協處理器,一種使用命名管道。命名管道方法,我無法開始工作——這很令人沮喪,因為我認為它可以解決協處理器的唯一問題——也就是說,如果我從終端進入一個新的 zsh shell,我似乎沒有能夠看到父 zsh 會話的 coproc。
我已經創建了簡化的腳本來說明下面的問題 - 如果你對我正在嘗試做的事情感到好奇 - 它正在向子彈列車 zsh 主題添加一個新的有狀態組件,它將由替換命令 build_prompt( ) 功能在這裡: 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 命令,因為管道中沒有任何內容可供讀取。
我的問題是:
- 在存在命令替換的情況下,是否可以構造命名管道以使其行為與協同程序完全一樣?
- 您能解釋一下為什麼可以從子程序中明顯地讀取和寫入協程序,但是如果我
zsh
在控制台中手動創建一個子shell(通過鍵入 ),我將無法再訪問它(實際上我可以創建一個新的協程序來執行獨立於其父級並退出,並繼續使用父級!)。- 如果 1 是可能的,我假設命名管道不會像 2 那樣複雜,因為命名管道沒有綁定到特定的 shell 程序?
為了解釋我在 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)
您的基於管道的腳本不起作用的原因不是 zsh 的某些特性。這是由於 shell 命令替換、shell 重定向和管道的工作方式。這是沒有多餘部分的腳本。
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)
會創建一個匿名管道,將執行函式的子 shell 的輸出連接到原始 shell 程序。原始程序從該管道讀取。子程序創建一個孫子程序來執行echo 'Hello Response!' > /tmp/foo.bar
。兩個程序都從相同的打開文件開始,包括匿名管道。孫子執行重定向> /tmp/foo.bar
。這會阻塞,因為沒有從命名管道讀取任何內容/tmp/foo.bar
。重定向是一個兩步過程(實際上是三步,但這裡第三步無關緊要),因為當你打開一個文件時,你無法選擇它的文件描述符。
>
操作員想要重定向標準輸出,即將特定文件連接到文件描述符 1。這需要三個系統呼叫:
- 呼叫
fd = open("/tmp/foo.bar", O_RDWR)
打開文件。fd
該文件將在程序目前未使用的某個文件描述符上打開。這是阻塞直到開始從命名管道讀取內容的步驟/tmp/foo.bar
:如果沒有人在聽,則打開命名管道阻塞。- 除了核心選擇的文件描述符之外,呼叫
dup2(fd, 1)
以打開所需文件描述符上的文件。如果新描述符 (1) 上有任何打開的內容(用於命令替換的匿名管道),則此時將關閉它。- 呼叫
close(fd)
,僅將重定向目標保留在所需的文件描述符上。同時,孩子列印
Reponse Sent!
並終止。原始的 shell 程序仍在從管道中讀取。由於管道在孫子程序中仍然是開放的,所以原來的 shell 程序一直在等待。要解決此僵局,請確保孫子不會使管道保持打開的時間超過它必須的時間。例如:
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
不會阻塞,因為它只是重定向一個已經打開的文件描述符。