Cat hangs when attempting to read empty STDIN

2

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.

StarCrashr

Posted 2018-09-27T14:40:04.043

Reputation: 123

I would be surprised if a chroot without at least /dev/null was usable in production. There's a long way between "/dev mounted" and "/dev manually created with the bare minimum", anyway... – user1686 – 2018-09-27T14:45:54.763

No solution but cat - or even a simple cat would do the same as your cat <&0. – xenoid – 2018-09-27T18:07:21.593

@xenoid yes, I know. I tried all three, and they all produce identical results. <&0 is just leftover from playing around to see if I can make cat error out rather than wait. – StarCrashr – 2018-09-27T18:24:58.560

By the way, I continued to play with it while waiting for an answer and discovered that it wasn't just hanging. It was looking for input from the console. This lead to to experimenting with heredocs to attempt to terminate cat if it has no input, but got an answer before getting far with that. – StarCrashr – 2018-09-27T19:51:06.710

Answers

3

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 $'\0' stdin

Null character (which you cannot store in a Bash variable anyway) as a delimiter (-d $'\0') 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.

Kamil Maciorowski

Posted 2018-09-27T14:40:04.043

Reputation: 38 429

My input is indeed text, in case anyone is curious. It's a colon-delimited array of information about a user and/or host that may or may not contain a password, which is why optional stdin is important for my uses(no passwords permitted on command line). If there is no data on stdin, the script attempts passwordless authentication methods and looks for the rest of the information in the first argument instead. – StarCrashr – 2018-09-27T20:03:29.790