The reason your pipe-based script doesn't work isn't some peculiarity of zsh. It's due to the way shell command substitutions, shell redirections and pipes work. Here's the script without the superfluous parts.
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
The command substitution $(call_me_from_cmd_subst)
creates an anonymous pipe connecting the output of the subshell running the function to the original shell process. The original process reads from that pipe. The child process creates a grandchild process to run echo 'Hello Response!' > /tmp/foo.bar
. Both processes start out with the same open files, including the anonymous pipe. The grandchild performs the redirection > /tmp/foo.bar
. This blocks because nothing is reading from the named pipe /tmp/foo.bar
.
Redirection is a two-step process (in fact three-step but the third doesn't matter here), because when you open a file, you don't get to choose its file descriptor. The >
operator wants to redirect standard output, i.e. it wants to connect a specific file to file descriptor 1. This takes three system calls:
- Call
fd = open("/tmp/foo.bar", O_RDWR)
to open the file. The file will be opened on some file descriptor fd
that the process is not currently using. This is the step that blocks until something starts reading from the named pipe /tmp/foo.bar
: opening a named pipe blocks if nobody is listening.
- Call
dup2(fd, 1)
to open the file on the desired file descriptor in addition to the one the kernel chose. If there's anything open on the new descriptor (1), which there is (the anonymous pipe used for the command substitution), it is closed at this point.
- Call
close(fd)
, keeping the redirection target only on the desired file descriptor.
Meanwhile, the child prints Reponse Sent!
and terminates. The original shell process is still reading from the pipe. Since the pipe is still open for writing in the grandchild, the original shell process keeps waiting.
To fix this deadlock, ensure that the grandchild does not keep the pipe open any longer than it has to. For example:
call_me_from_cmd_subst() {
{ exec >&-; /bin/echo 'Hello Response!' > /tmp/foo.bar; } &
echo 'Response Sent!'
}
or
call_me_from_cmd_subst() {
{ echo 'Hello Response!' > /tmp/foo.bar; } >/dev/null &
echo 'Response Sent!'
}
or any number of variations on this theme.
You don't have this problem with a coprocess because it doesn't involve a named pipe, so one of the halves of the deadlock isn't blocked: >/tmp/foo.bar
blocks when it opens a named pipe, but >&p
doesn't block since it's just redirecting an already-open file descriptor.
Best Answer
To prevent
cat
from hanging in the absence of any writer (in which case it's the opening of the fifo, not reading from it, that hangs), you can do:The first redirection opens in read+write mode which on most systems doesn't block and instantiates the pipe even if there's no writer nor reader already. Then the second open (read-only this time) would not block because there is at least one writer now (itself).
The
0
is only needed in recent versions of ksh93 where the default fd for<>
changed from 0 to 1.Also, in
ksh93
, that would not work whencat
is the shell builtin, like whenksh93
is called when/opt/ast/bin
is ahead of/bin
in$PATH
or after a call tobuiltin cat
as upon the<"$my_named_pipe"
, (I guess) ksh93 saves the previous target of stdin on a separate file descriptor which would hold the pipe open. You can work around that by writing it instead:(which you might also argue conveys the intention more clearly)
Note that that
<>
on the pipe would also unlock other readers to the fifo.If there were some writers,
cat
would still have to read all their output and wait until they have closed their end of the pipe. You could open the pipe in non-blocking mode, like with GNUdd
's:Which would only read from the pipe as long as there's some data in it, and exit with a
error when there's no more, and not unlock other readers, but that means you could miss some of the writers output if they are slower to write to the pipe than you (
dd
) are to read it.Another approach could be to timeout when there's been no input in a while, for instance by using
socat
's-T
option:Which would exit if there's not been anything coming from the pipe in one second.