我需要在 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)
答案1
基于管道的脚本不起作用的原因并不是 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
不会阻塞,因为它只是重定向已经打开的文件描述符。
答案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 上产生某种输出?