Cat hangs when attempting to read empty STDIN

Posted on

Problem :

My script attempts to gather information that may or may not be present in STDIN at execution time, but cat hangs if the pipe is empty. How can I ensure that my script skips this step if this there is nothing in STDIN?

stdin=$(cat <&0)

Note that I am specifically not looking for any solutions that refer to /dev/, as I intend this to be usable in a chroot whether or not /dev/ has been mounted if possible.

Solution :

Usually while working with pipes and stdin, a depleted pipe has no special meaning. New data may still appear until there is an eof that closes the pipe. Your cat terminates at eof as expected. If there was no data before eof, only then you can say the stdin was truly empty.

Consider sender | receiver. It’s not uncommon the sender is (much) slower than the receiver; in such case the receiver‘s stdin is almost always depleted, but you hardly ever want to kill the entire pipe because of it. Therefore tools that exit on “empty” (depleted but not yet terminated) stdin are exceptions rather than standard.

In Bash there is read -t 0 (-t is not required by POSIX). From help read:

If TIMEOUT is 0, read returns immediately, without trying to read any data, returning success only if input is available on the specified file descriptor.

By default read reads from stdin, so the exit status of read -t 0 will tell you if the stdin is “empty”. But beware! A command like

echo 1 | read -t 0

may exit successfully or not, because echo and read run simultaneously, not sequentially. To avoid this, your script should sleep for a while before read -t 0. Depending on where the stdin comes from, “a while” may be relatively long. Do something like this:

sleep 1
if read -t 0; then# process stdin here, you know it's non-empty

You populate a variable with data taken from stdin. Since storing binary data in a variable is not a good idea (read this), maybe your data is just text. If so, use read -t like this:

read -r -t 5 -d $'' stdin

Null character (which you cannot store in a Bash variable anyway) as a delimiter (-d $'') will allow you to read any text (e.g. with newlines) to the stdin variable. After at most 5 seconds (-t 5) the command terminates, allowing your script to continue.

Another approach is with timeout. A basic example from my Debian:

timeout --foreground 5 cat | wc -c

(Replace wc -c with your code that parses stdin; it’s just an example).

This should handle binary data just fine. If cat doesn’t get eof then after 5 seconds it will be killed, so wc will get eof anyway and the line doesn’t stall. The problem is cat will be killed regardless if it’s processing any data at the moment. I imagine you want to get all the data, if only there is some, even if it takes more than 5 seconds. Improved version:

{ timeout --foreground 5 dd bs=1 count=1 2>/dev/null && cat; } | wc -c

If the first byte appears within 5 seconds, cat will be triggered. It then will process any further input until eof, no matter how long it takes. Everything including the first byte (if any) will go to wc. If there is neither a byte nor eof in 5 seconds, wc will receive just eof; the line doesn’t stall.

Leave a Reply

Your email address will not be published. Required fields are marked *