Bash - Check if the standard input contains anything
Asked Answered
P

2

7

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?

Phaeton answered 3/12, 2023 at 16:36 Comment(3)
The operator -t doesn't check if there is input; it checks whether the file handle is connected to a tty (i.e. generally a terminal). If there is a terminal, input could arrive at any time, so in some sense it's the opposite of what you seem to want.Aldarcie
@KamilCuk Yes, I made a mistake, I misunderstood the meaning.Phaeton
See How to check if a pipe is empty and run a command on the data if it isn't?. Note the comments saying that read -t0 is broken in Bash.Vizzone
B
5

Why does it behave this way

-t checks if file descriptor is connected to a TTY.

how can I solve the problem?

Read 1 byte. If you did read it, it's not empty, otherwise it's empty.

... | {
   if IFS= read -d '' -n 1; then
       echo 'Not empty!' >&2
       # forward data along if you want to
       if [[ -n "$REPLY" ]]; then
           printf "%s" "$REPLY"
       else
           # properly handle zero byte in input
           printf "\x00"
       fi
       cat
   else
       echo 'empty!' >&2
   fi
} | ...

Note that, because it is not possible to hold a zero byte in Bash, the above code check the [[ -n "$REPLY" ]] to handle zero byte. You could also do something with dd and xxd or some custom program would to handle it zero bytes.

Biffin answered 3/12, 2023 at 16:46 Comment(5)
Where did you get the "$REPLY" variable from?Phaeton
REPLY is set by read. See read --help.Biffin
There is a trick to enable read to handle zero/NUL bytes in input. See BashFAQ/058 (Can bash handle binary data?). It contains code to "simulate cat with just bash builtins, binary safe".Vizzone
Sure, so you could do if [[ -n "$REPLY" ]]; then printf "%s" "$REPLY"; else printf "\x00"; fi. I usually don't care about zero bytes / assume they won't be there when checking if empty. Added, nice! I wonder, would printf "${REPLY:+%s}${REPLY:-\x00}" "$REPLY" be better?Biffin
I think you need to use IFS= read -d '' -n 1 to handle input that starts with whitespace (including newline) correctly.Vizzone
L
4

You are confusing:

  • [ -t 0 ] or test -t 0, which tests if file descriptor 0 (stdin) is connected to a terminal, as others have already explained
  • with read -t 0, which tests if there is something to read on stdin (or on the file descriptor specified with the -u option), without actually reading it

Tests

(read also the caveat section below)

$ echo 'Hello world' | read -t 0
$ echo $?
0
$ read -t 0
$ echo $?
1
$ : | read -t 0
$ echo $?
1

Relevant part of the manual

read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

Read a line from the standard input and split it into fields.

(...)

-t timeout

Time out and return failure if a complete line of input is not read within TIMEOUT seconds. The value of the TMOUT variable is the default timeout. TIMEOUT may be a fractional number. 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. The exit status is greater than 128 if the timeout is exceeded

Caveat

There is an inherent race condition with a construct such as echo something | read -t 0 because, by default, each command in a pipeline is executed as a separate process and you have no control over which process is started first. Moreover, even if echo is started first, it may be interrupted before it has written anything to stdout.

So, sometimes, read -t0 will return 0 as you may expect, but sometimes read will be executed before anything is written to the pipe and will return 1.

As suggested by @F.Hauri in comment, you may want to add some delay before testing whether there is something to read, but there is no guarantee this will work every time. Anyway, if you use a non-blocking read, then I assume you expect to retry your read later.

Lontson answered 3/12, 2023 at 17:45 Comment(22)
Yes, -t switch of read command is for timeout, but with 0 as timeout, read return immediately without reading anything, but return 0 (true) if data are ready to read and return 1 (false) if no data are present.Engraving
But use pseudo variable _ as variable name for read command: read -t 0 _ as readcommand require a variable name or use $REPLY by default. Using $_ prevent this test command to modify anything in current environment.Engraving
Use sample: echo 'Hello world!' | if read -t 0 _ ; then read line; echo "$line"; fiEngraving
For me, echo Hello world | read -t 0 always exits with 1. echo 'Hello world' | read -t 0.0001 exits sometimes with 142 and sometimes with 0.Biffin
@Biffin You'r right, I noticed this just now! A kind or race condition, I repeated this again and again and sometime...Engraving
@Biffin I have added the relevant part of the read help to my answerLontson
@Biffin Try this: { sleep .001; if read -t 0 _ ;then read line;echo "$line";fi } < <(echo 'Hello world!') or this echo Hello world | { sleep .0001; read -t 0 _;echo $?;}Engraving
@F.Hauri-GiveUpGitHub Yes, there is an inherent race condition due to the way Bash spawns its two child processes. This is something I already covered in the past on SO or U.SE, I'll try to find my answer.Lontson
@Lontson hmm correct, but as bash is currently running his own process, they have to spawn only 1 other process... (so you'd better written: ...the way bash spawn it's second process... just sementic ;-)Engraving
@F.Hauri-GiveUpGitHub « bash is currently running his own process, they have to spawn only 1 other process » Not by default, only with shopt -s lastpipeLontson
@Lontson Unfortunately I also get the echo 'Hello world' | read -t 0; echo $? always returns 1. Ubuntu 22.04.Phaeton
@MarioPalumbo read previious commnents and try: echo Hello world | { sleep .0001; read -t 0 _;echo $?;}Engraving
@MarioPalumbo I have added a caveat section to my answerLontson
sleep 0 also works but not other commands, why?Phaeton
@MarioPalumbo Anything that adds some delay and that may force the kernel to run the other process (read) will work most of the time. But it may not work under heavy load. That's what I wrote in my caveat: there is no absolute guarantee. Of course, if you sleep 1h, that will work 99.99999999% of the time, but that defeats the purpose of a non-lbocking read IMHOLontson
Yes, unfortunately I had guessed it.Phaeton
(too many comments to cope with :-) @F.Hauri-GiveUpGitHub « read command require a variable name or use $REPLY » read -t0 does not read anything and thus does not change the value of REPLY. Moreover I consider $_ as a reserved variable and I would not use it either, as it may cause confusion to the inadvertent reader of my codeLontson
@Biffin I have added a caveat section to address echo | read -t 0 returning sometimes 0 and sometimes 1. As to why read -t 0.001 returns 142, this is covered by the manual I quoted in my answer: read was interrupted by a SIGALRM signal before the read was successfulLontson
Unfortunately also : | { sleep .0001; read -t 0 _;echo $?;} returns 0.Phaeton
Let us continue this discussion in chat.Lontson
This situation is more difficult than it appears. ifne in moreutils package is not flexible and forces me to do an if then, instead I need it to give me a simple exit code so I can do an if then else. The fact that this triviality is not easy to solve is worth mentioning as a feature request for bash as well as the "pipe if success" symbolism &|.Phaeton
@MarioPalumbo, ..."pipe if success" doesn't make sense. If it doesn't start both sides in parallel (meaning before success/failure of the left is known), it's not a shell pipe. ifne at least can get out of the way after a single byte is written; "pipe if success" (assuming that it checks the left-hand side's exit status) would need to buffer all the way to end of execution.Hers

© 2022 - 2025 — McMap. All rights reserved.