0

I know this question has been asked several times but it seems ineffective in my case.

[[ ! -t 0 ]] = Does standard input contains anything?

This command:

echo 'Hello world' | [[ ! -t 0 ]]
echo $?

gives the correct output: 0, i.e. the standard input contains anything.
This command:

[[ ! -t 0 ]]
echo $?

gives the correct output: 1, i.e. the standard input is empty.
Instead this command:

: | [[ ! -t 0 ]]
echo $?

gives unexpected output: 0, when instead the standard input is empty: 1.

Why does it behave this way and how can I solve the problem?

2

2 Answers 2

6

[[ ! -t 0 ]] = Does standard input contains anything?

Your premise is wrong. This tests whether stdin is connected to a terminal/tty, not whether stdin contains anything. Because of the negation the command returns 0 (true) if stdin is not connected to a terminal.

See man test:

-t FD file descriptor FD is opened on a terminal

and man bash:

-t fd True if file descriptor fd is open and refers to a terminal.

With this in mind, re-evaluate your examples and you'll see they do indeed return the expected exit status.

The correct way to check if there's anything pending on stdin would be to use select(2), but that's not directly available in a shell such as bash. The closest I can suggest is to use read with an immediate timeout:

sleep 2
if read -t 0 _
then
    echo "Ready to receive data"
    IFS= read -r data
    echo "Received: $data"
fi

Run this, and then during the initial sleep, (a) try preloading stdin with some text, (b) do nothing.

Note that this will still hang (on the read -t 0 _) if you're reading from a pipe and there is no writer present. It will work as I've described if there is a writer present, even if that writer has not written anything to read.

8
  • Chris, what do you mean by "preloading"? Commented Sep 15 at 14:47
  • 1
    @decision-making-mike just type some stuff, so you can compare results per the question title Commented Sep 15 at 16:45
  • Actually, when I do sleep 3 ; read -t0 ; echo $?, and type something during those 3 seconds, there's 1 echoed. The same if I type nothing. I mean something like aaa. Only if I type either CTRL-D or ENTER after that aaa, there's 0 echoed. Is this the expected behavior? Commented Sep 15 at 16:55
  • bash's read -t0 does actually do a select(). In zsh, you'd use the zselect builtin instead (zselect -r0 -r0). Commented Sep 15 at 18:52
  • I mean bash's read -t0 does a select() to check whether there's any input, but, like mksh's, and unlike ksh93's or zsh's does not read() it regardless of whether select() says there is or not, so it can be used here. In the case of bash, it's documented, not in as many words in the case of mksh, so it may be unintentional there. Commented Sep 15 at 19:02
1

The select() or poll() (or pselect()/ppoll() variants) when called with a 0 timeout can be used to check whether a read() system call would immediately succeed.

In bash, on systems that have a select() system call (all the POSIX ones at least) read -t0 does that: -t0 is a special case where it does a select(1, [0], NULL, [0], {tv_sec=0, tv_nsec=0}), checking for both read and error condition on fd 0 (stdin), and returns success if fd 0 is returned in the read set without doing any reading.

With zsh, you can do the same with its zselect builtin (in the zsh/zselect loadable module), which is a raw interface around the select() system call.

zmodload zsh/zselect
if zselect -t 0 -r 0; then
  print "A read() on fd 0 (stdin) would succeed immediately"
fi

A read() however can succeed without timeout because there's data to read, but also because end-of-file has been reached, in which case read() reads nothing but succeeds.

So in the general case, it's not the right approach to test whether there's anything to read now.

For that, you can use the FIONREAD ioctl() instead which returns the number of bytes that can be read on a file descriptor.

On Linux at least, that works when the fd is open at least on

  • a regular file
  • a tty device
  • a pipe (as used by | in most shells)
  • a Unix domain socket or socketpair (as used by ksh93's | for instance), and presumably other types of sockets though I've not tested it.

It doesn't work on some other device files, including /dev/{null,full,zero,random} and block devices.

I don't know of any shell that has a usable interface to that ioctl though¹, but you could resort to perl for instance:

if
  perl -le 'require "sys/ioctl.ph";
            ioctl(STDIN, &FIONREAD, $n) or die$!;
            exit(unpack("L", $n) == 0)'
then
  echo "There is something to read on fd 0 (stdin)"
fi

However, it's not because that returns true that a read builtin of your shell would not hang. First there's a TOCTOU race: something else which also has a fd open on the resource on stdin could read what's there by the time you call read.

Then the read builtin reads a full line (without -r even possibly multiple lines if they end in backslashes) for which it may call the read() system call several time (one byte at a time when the input is not seekable) until a newline character is found, and while the first read() will return something, the next ones may hang.

For instance, if stdin is the reading end of a pipe fed by (printf foo; sleep 3600; printf 'bar\n'), FIONREAD will return 3 (the 3 bytes in foo), but read var will hang for an hour to read the full foobar\n line.

Same if stdin is a terminal, and the user has typed foo then Ctrl+d.

The only thing that is guaranteed not to hang (if nothing else has already read the input in the interval) is a single read() system call or multiple read()s as long as the n bytes returned by FIONREAD have not been read yet.

Here, you could however use read -t:

if
  perl -le 'require "sys/ioctl.ph";
            ioctl(STDIN, &FIONREAD, $n) or die$!;
            exit(unpack("L", $n) == 0)'
then
  echo "There is something to read on fd 0 (stdin)"
  if IFS= read -rt 0.05 line; then
    printf 'A full line was read: "%s"\n' "$var"
  else
    printf 'However, not a full line yet. So far, I'\''ve read: "%s"\n' "$line"
  fi
else
  echo "Nothing to read from stdin at this point, or it's not of a type where I can tell"
fi

Note the 0.05 timeout, which is short but not much so as we need to give bash enough time to call select() and 1-byte² read()s multiple times until what's to be read has been read.


¹ bash's read -t0 does use FIONREAD where select() is unavailable, but you'll have a hard time finding a system that has FIONREAD but not select() these days.

² for non-seekable input such as ttys or pipes/sockets.

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.