I have a requirement to make a function in a zsh shell script, that is called by command substitution, communicate state with subsequent calls to the same command substitution.
Something like C's static variables in functions (very crudely speaking).
To do this I tried 2 approaches - one using coprocessors, and one use named pipes. The named pipes approach, I can't get to work - which is frustrating because I think it will solve the only problem I have with coprocessors - that is, if I enter into a new zsh shell from the terminal, I don't seem to be able to see the coproc of the parent zsh session.
I've create simplified scripts to illustrate the issue below - if you're curious about what I'm trying to do - it's adding a new stateful component to the bullet-train zsh theme, that will be called by the command substituted build_prompt() function here: https://github.com/caiogondim/bullet-train.zsh/blob/d60f62c34b3d9253292eb8be81fb46fa65d8f048/bullet-train.zsh-theme#L692
Script 1 - Coprocessors
#!/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
Script 2 - Named Pipes
#!/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
In their initial forms they both produce exactly the same output:
$ ./named-pipe.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!
$ ./coproc.zsh
Retrieved: Hello World!
Response Sent!
Hello Response!
Now if I switch the coproc script to call using the command substitution nothing changes:
# Run this first
#call_me_from_cmd_subst
# Then comment out the above call
# And run this instead
print "$(call_me_from_cmd_subst)"
That is reading and writing to the coprocess from the subprocess created by command substituion causes no issue. I was a little suprised by this - but it's good news!
But if I make the same change in the named piped examples the script blocks - with no output. To try to guage why I ran it with zsh -x
, giving:
+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!'
It looks to me like the subprocess created by the command substitution won't terminate whilst the following line hasn't terminated (I've played with using &
, &!
, and disown
here with no change in result).
print 'Hello Response!' > /tmp/foo.bar &!
To demonstrate this I can manually fire-in a cat to read the response:
$ cat /tmp/foo.bar
Hello Response!
The script now waits at the final cat command as there is nothing in the pipe to read.
My questions are:
- Is it possible to construct the named pipe to behave exactly like the coprocess in the presence of a command substitution?
- Can you explain why a coprocess can demonstrably be read and written to from a subprocess, but if I manually create a subshell (by typing
zsh
) into the console, I can no longer access it (in fact I can create a new coproc that will operate independantly of its parent and exit, and continue using the parent's!). - If 1 is possible, I assume named pipes will have no such complicates as in 2 because the named pipe is not tied to a particular shell process?
To explain what I mean in 2 and 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
I can't see the coprocess from inside the child zsh, yet I can see it from inside a command substitution subprocess?
Finally I'm using Ubuntu 18.04:
$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)
cat
tries to read to the end, and blocks if there is no end. have you considered actually just reading one line at a time?cat
will only block if there's nothing to read - and I'm pretty sure from the zsh logging that the it's not blocking oncat
, it's the write blocking on termination of the command substitution's subprocess. When we remove the command substitution nothing blocks so the problem isn't the use ofcat
alone - it's something to do with the substitution. I think I could rephrase this as "how do I make the Hello Responseprint
not block the command substitution exiting?"cat
blocks on it when it reaches the end of what's there (unless there's enough unread to fill a buffer)cat
you think is blocking in which script? I'm not disputing that this is fact - but I see no blocking without the command substitution, and with the exception of the lastcat
blocking on the named pipe after manual intervention (which is expected) the debug logs are not showingcat
blocking (or I'm missing the point).