6

My script (should) acts differently, depending on the presence of the data in the input stream. So I can invoke it like this:

$ my-script.sh

or:

$ my-script.sh <<-MARK
    Data comes...
    ...data goes.
MARK

or:

$ some-command | my-script.sh

where two last cases should read the data, while first case should notice the data is missing and act accordingly.

The crucial part (excerpt) of the script is:

#!/bin/bash
local myData;
read -d '' -t 0 myData;

[ -z "${myData}" ] && {
    # Notice the lack of the data.
} || {
    # Process the data.
}

I use read to read input data, then option -d '' to read multiple lines, as this is expected, and the -t 0 to set timeout to zero. Why the timeout? According to help read (typing left unchanged; bold is mine):

-t timeout time out and return failure if a complete line of input is not read withint TIMEOUT seconds. The value of the TMOUT variable is the default timeout. TIMEOUT may be a fractional number. If TIMEOUT is 0, read returns success only if input is available on the specified file descriptor. The exit status is greater than 128 if the timeout is exceeded

So I in case 2 and 3 it should read the data immediately, as I understand it. Unfortunately it doesn't. As -t can take fractional values (according to above man page), changing read line to:

read -d '' -t 0.01 myData;

actually reads the data when data is present and skips it (after 10ms timeout) if it is not. But it should also work when TIMEOUT is set to real 0.

Why it actually doesn't? How can this be fixed? And is there, perhaps, alternative solution to the problem of "act differently depending on the presence of the data"?

UPDATE

Thanks to @Isaac I found a misleading discrepancy between quoted on-line version and my local one (normally I do not have locale set to en_US, so help read gave me translation which I couldn't paste here, and looking up for on-line translation was faster than setting new env---but that caused the whole problem).

So for 4.4.12 version of Bash it says:

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.

This gives a little bit different impression than "If TIMEOUT is 0, read returns success only if input is available on the specified file descriptor"---for me it implied actually an attempt to read the data.

So finally I tested this and it worked perfectly:

read -t 0 && read -d '' myData;

The meaning: see if there's anything to read and if it succeed, just read it.

So as to base question, the correct answer was provided by Isaac. And as to alternative solution I prefer the above "read && read" method.

9
  • Hmm... what happens if you sleep 0.01 before the read -t0? Jan 19, 2019 at 19:44
  • Doesn't help :)
    – peterph
    Jan 19, 2019 at 19:49
  • @steeldriver Didn't check, but it seems more like a hack than real solution. Actually -t 0.01 is shorter... Still it's a hack. ;-)
    – Cromax
    Jan 19, 2019 at 20:07
  • 1
    I don't know if shell read timeouts work with non-ttys, but a workaround might be if [ -t 0 ]; then echo not file or pipe, so no data; else read -d '' myData; ... ; fi Jan 19, 2019 at 20:13
  • @MarkPlotnick Indeed, it worked, so +1. Although I do also wonder how does it work on non-tttys.
    – Cromax
    Jan 19, 2019 at 20:27

2 Answers 2

3

No, read -t 0 will not read any data.

You are reading the wrong manual. The man read will give the manual of a program in PATH called read. That is not the manual for the builtin bash read.

To read bash man page use man bash and search for read [-ers] or simply use:

help read

Which contains this (on version 4.4):

If timeout is 0, read returns immediately, without trying to read any data.

So, no, no data will be read with -t 0.


  1. Q1

    Why it actually doesn't?

Because that is the documented way it works.

  1. Q2

    How can this be fixed?

Only if that is accepted as a bug (I doubt it will) and the bash source code is changed.

  1. Q3

    And is there, perhaps, alternative solution to the problem of "act differently depending on the presence of the data"?

Yes, actually, there is a solution. The next sentence of help read after what I quoted above reads:

returning success only if input is available on the specified file descriptor.

Which means that even if it doesn't read any data, it could be used to trigger an actual read of available data:

read -t 0 && read -d '' myData

[ "$myData" ] && echo "got input" || echo "no input was available"

That will have no delay.

2
  • 1
    It turned out that indeed I made a mistake by typing in the description man read, but the quote and text under provided URL actually refers to help read. The funny thing is, that when I changed locale to en_US and ran help read I get slightly different text that the one quoted. And here it indeed says: 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. So it gave me a clue to do this: read -t 0 && read -d '' myData and... it worked! But +1 for pointing that out.
    – Cromax
    Jan 20, 2019 at 2:43
  • This solution does not work, if you read the data from terminal: read -t 0 does not return zero unless you press enter. Note also that read -d '' myDatadoes not exit unless you interrupt it by signal (e.g. by typing Ctrl+C) or type NUL character (by ctrl+@) when the input comes from terminal. (Bash 5.0.17)
    – jarno
    Jul 17, 2022 at 9:24
3

Disclosure: not a rigorous answer, lots of handwaving

There are several things at play here. First, bash handles zero timeout internally totally differently from non-zero timeouts. (If you decide to look at the code, be prepared for spending some time on it - it is not as straightforward as one might expect; it goes rather deep.)

Second, there's an inherent race condition when working with pipes - the processes are created "from right to left", i.e. the last process on the command line is created first. However it is not guaranteed the shell will continue executing, spawning further process from the command line, before the ones already created get scheduled themselves - thus it may well happen that the read is executed before the input is ready. Depending on the particular system, if you play a bit with the timeout value, you may be able to reproduce the -t 0 behaviour with something like -t 0.00001.1)

On top of the above, consider that most systems these days are parallel (either multi-core or multi-processor) - you will get different results depending on how the kernel schedules all the processes in the pipeline (one thing that influences that will be the current system load, btw). This problem may be alleviated, by restricting the process group to a single particular CPU (e.g. with taskset), but the problem with scheduling of child processes mentioned above is not going to go away.


1) I actually managed to get to a point, where read only received part of the input (1 or 2 characters out of 3).

1
  • I like this explanation a lot. Sometimes I forget, how complex the internals are. And how hard is to create consistent and working shell. So this all gives me much better insight into the "problem", which actually, as for now, seems to be pretty natural, to occur. And because it actually answers the question, give +1 to it. If there will be no other answers, I will accept it. Thanks!
    – Cromax
    Jan 19, 2019 at 21:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .