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
is0
,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.