Shell-Script

命令替換下的 coproc 和命名管道行為

  • March 9, 2021

我需要在 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 命令,因為管道中沒有任何內容可供讀取。


我的問題是:

  1. 在存在命令替換的情況下,是否可以構造命名管道以使其行為與協同程序完全一樣?
  2. 您能解釋一下為什麼可以從子程序中明顯地讀取和寫入協程序,但是如果我zsh在控制台中手動創建一個子shell(通過鍵入 ),我將無法再訪問它(實際上我可以創建一個新的協程序來執行獨立於其父級並退出,並繼續使用父級!)。
  3. 如果 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。這需要三個系統呼叫:

  1. 呼叫fd = open("/tmp/foo.bar", O_RDWR)打開文件。fd該文件將在程序目前未使用的某個文件描述符上打開。這是阻塞直到開始從命名管道讀取內容的步驟/tmp/foo.bar:如果沒有人在聽,則打開命名管道阻塞。
  2. 除了核心選擇的文件描述符之外,呼叫dup2(fd, 1)以打開所需文件描述符上的文件。如果新描述符 (1) 上有任何打開的內容(用於命令替換的匿名管道),則此時將關閉它。
  3. 呼叫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不會阻塞,因為它只是重定向一個已經打開的文件描述符。

引用自:https://unix.stackexchange.com/questions/638211