命令替换下的 coproc 和命名管道行为

Phi*_*hil 5 zsh pipe shell-script fifo coprocesses

我需要在 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
Run Code Online (Sandbox Code Playgroud)

脚本 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
Run Code Online (Sandbox Code Playgroud)

在它们的初始形式中,它们都产生完全相同的输出:

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

$ ./coproc.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!
Run Code Online (Sandbox Code Playgroud)

现在,如果我将 coproc 脚本切换为使用命令替换进行调用,则没有任何变化:

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

$ ./coproc.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!
Run Code Online (Sandbox Code Playgroud)

也就是说,从命令替换创建的子进程读取和写入协进程不会导致问题。我对此感到有点惊讶 - 但这是个好消息!

但是,如果我在命名的管道示例中进行相同的更改,脚本会阻塞 - 没有输出。为了尝试判断我为什么用 运行它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!'
Run Code Online (Sandbox Code Playgroud)

它看起来对我来说,由命令替换创建将不会终止,而下面的线还没有终止子(我打得使用&&!disown这里的结果没有变化)。

# Run this first
#call_me_from_cmd_subst

# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"
Run Code Online (Sandbox Code Playgroud)

为了证明这一点,我可以手动触发一只猫来读取响应:

$ cat /tmp/foo.bar
Hello Response!
Run Code Online (Sandbox Code Playgroud)

脚本现在等待最后的 cat 命令,因为管道中没有任何内容可供读取。


我的问题是:

  1. 在存在命令替换的情况下,是否可以将命名管道构造为与协进程完全相同?
  2. 你能解释一下为什么一个协进程可以明显地从一个子进程中读取和写入,但是如果我手动创建一个子shell(通过键入zsh)到控制台,我就不能再访问它(事实上我可以创建一个新的协程,它将运行独立于其父级并退出,并继续使用父级的!)。
  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
Run Code Online (Sandbox Code Playgroud)

我无法从子 zsh 内部看到协进程,但我可以从命令替换子进程内部看到它?

最后我使用的是 Ubuntu 18.04:

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)
Run Code Online (Sandbox Code Playgroud)

Gil*_*il' 5

基于管道的脚本不起作用的原因不是 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
Run Code Online (Sandbox Code Playgroud)

命令替换$(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!'
}
Run Code Online (Sandbox Code Playgroud)

或者

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

或此主题的任意数量的变体。

协进程没有这个问题,因为它不涉及命名管道,所以死锁的一半没有被阻塞:>/tmp/foo.bar当它打开命名管道时阻塞,但>&p不会阻塞,因为它只是重定向一个已经打开的文件描述符。