Skip to main content

Fixed: A Named Pipe's Reader Never Finishes in a Shell Script

· 4 min read
Jagdish Kumawat
Founder @ Dewiride

My script fed commands to a program through a named pipe. It closed the pipe when it was done, and waited for the program to finish, and that wait never ended. Another process had a copy of the pipe open, and I'd given it that copy without noticing.

What I Was Doing​

A script kept a mysql session open by writing to it through a named pipe, and started another program in between. Here it is in its smallest form, with cat as the reader and sleep as the other program:

pipe.zsh
mkfifo demo.pipe
cat demo.pipe > fifo.out &
READER=$!

exec 3> demo.pipe
print -u3 "first line"

sleep 8 &

exec 3>&-
wait $READER
  • exec 3> demo.pipe opens the pipe for writing, as file descriptor 3, and keeps it open.
  • print -u3 writes to it.
  • exec 3>&- closes it, and the reader should then see the end of the file.

What I Expected​

cat to finish right after exec 3>&-, and wait to return.

What I Got​

wait didn't return until sleep had ended. In my real script, the other program was a server that kept running, so it never returned at all. With a check added after a second:

Output
After 1 second, the reader is still running
COMMAND FD TYPE NAME
sleep 3w FIFO demo.pipe
Once sleep ended, the reader finished

The reader still running after a second, lsof showing sleep with descriptor 3w on demo.pipe, and the reader finishing only once sleep ended.

What I Checked​

Who has it open. lsof -p lists a process's open files. It's in the output above: sleep has descriptor 3, open for writing (3w), on the pipe.

What ends a read. POSIX's read says: "If no process has the pipe open for writing, read() shall return 0 to indicate end-of-file." No process at all. So while any writer exists, the reader keeps waiting.

Why It Happens​

A child process inherits its parent's open file descriptors. sleep started while descriptor 3 was open, so it got its own copy. exec 3>&- closed only the script's copy.

The Fix​

Close the descriptor for the child, as you start it:

pipe.zsh (excerpt)
sleep 8 3>&- &

Or start other programs before you open the pipe. Do this for every program started while it's open.

After the Fix​

Output
The reader finished
first line

The reader finished straight away, and fifo.out holding first line.

Common Mistakes​

MistakeWhat happensDo this instead
Closing it only in the scriptChildren keep their copiesAdd 3>&- to each child
Starting a server while it's openIt holds the pipe for as long as it runsStart it first, or close it for the child
Hiding the hang with a timeoutThe real holder stays unknownFind it with lsof

Troubleshooting​

SymptomLikely causeFix
exec 3> pipe itself hangsNo reader has opened the pipe yetStart the reader first
The reader gets no dataThe writer's output is bufferedFlush it, or write less often
The script ends and the reader staysA child outlived it, with the descriptorCheck with lsof

Conclusion​

A pipe's reader sees the end only when every writer has closed it, and child processes inherit a copy. Use 3>&- on anything you start while it's open.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.