On Thu, 9 Oct 2025 at 02:19, Pourko via Bug reports for the GNU Bourne
Again SHell <[email protected]> wrote:

> Now, if only we had a way to say:  read -d "" --noblock
>

Yeah, and it would help if « read -t0 -n0 » worked consistently with other
timeout values too.

«read» treating zero timeout as meaning hopefully-non-destructive
look-ahead was added in the weekly snapshot on 2008-09-04
(commit:48ff544772dfa6b8fa3dda69361a5d64a9b0cbcd). Before that, a zero
timeout was always an error. In hindsight I think this was a suboptimal
design choice; I would have preferred it to be treated like O_NONBLOCK.

«read» treating zero size as meaning non-destructive (but less portable)
look-ahead was added in the weekly snapshot on 2017-06-30
(commit:9952f68c0817d518471ed88090c79dbc761b96de). In this case 0 returned
by the «read» syscall means “no error” rather than EOF, and the «read»
built-in usefully returns 0 in this case.
However this depends on how the «read» syscall treats a zero «count»
parameter. (Some kernels interpret it to mean return 0 if at least one byte
is already available, and otherwise either wait until a byte is available,
or return EAGAIN if opened with O_NONBLOCK. Other kernels return 0
immediately without checking for available input, presumably on the theory
that zero bytes are already available.)
Before this change, no attempt was made to check whether input was
available, and the return status was effectively random.


Currently, very short timeouts (less than about 30µs on my machine) always
result in timing out, even when data is already available. This makes them
largely indistinguishable from a zero timeout, at least on systems that can
report EAGAIN from a zero-sized non-blocking read.

I'm guessing this is because this corresponds to the (very) small delay
between setting an alarm timer, and then initiating a read.
Since this amounts to a race condition, fixing it can't break anything that
wasn't already broken.

The question is whether an actual zero timeout should be treated the same
as (but faster than) a very short timeout, and whether zero-sized read
should be forced to behave as a look-ahead even on kernels that don't
directly support it?

The current behaviour runs counter to reasonable expectations based on
time-limited I/O in other environments: absent very hard real-time
considerations, a time limit on an IO operation should be about the data in
transit to or from the host, rather than about the time it takes to
transfer data out of or into the process that's making the request. (And
when hard real-time limits apply, I wouldn't want the kernel copying large
blobs into the process's address space while the timer is still running.)
In particular, a zero timeout on read should simply mean “give me what you
already have, without waiting for any more to become available”; it should
not mean “give up before giving me stuff that's already available, based on
some implementation race condition”.

Before I go on, I acknowledge that there's now 17 years of compatibility
precedent, and therefore simply “fixing” the behaviour of «read -t0» could
cause problems to existing scripts.

Currently when «read» is given «-t0», both «-n» and «-d» are ignored, so
does anyone think any of the following proposed changes would break
existing working scripts?

1. Explicitly define the behaviour or «read -t0 -N0» as a non-destructive
lookahead, while deprecating «read -t0» without «-N0» for this purpose.
2. Alter «read -t0 -N$count» to place up «$count» bytes of input in
«REPLY», but only from what is already available. In particular, when
reading from a pipe, it consumes exactly the whole content of the kernel's
pipe buffer (½KiB for POSIX; 4KiB or one vmpage for Linux) provided that
«$count» is large enough).
3. Alter «read -t0 -d$'\n'» to place a line of input in «REPLY» if one is
already available. If only a partial line of input is available (missing a
newline), then:
  (3a) when reading from a tty that's already in ICANON (“cooked”) mode,
«REPLY» is left empty without consuming any input; and
  (3b) in all other cases, any input that is consumed is placed in «REPLY»,
but the consumption is unspecified and explicitly documented as “may
change”.
4. Alter «read -t0 -n0 -d$'\n'» to perform look-ahead while remaining in
ICANON mode.
5. The behaviour «read -t$non_zero -N$count» and «read -t$non_zero -d$'\n'»
be stabilized to be consistent with (1) through (4); in particular, this
means that any SIGALRM timer should not start until after a non-blocking
read has reported «EAGAIN».
6. The behaviour of «-d» with something other than $'\n' be made as
consistent as possible with (1) through (5) (As an implementation detail,
when reading from a tty, it would be nice to use «termios.c_cc[VEOL]» for
«-d» on systems that support it.)

If there's interest, I'll see if I can work up suitable patches.

Question: should timeouts be rounded up or down when using the tty timer
facilities («termios.c_cc[VTIME]»)?

Second question: if we're at EOF on a plain file, should using a non-zero
timer wait for more data to be added to the file?

-Martin

PS:
Tentative related proposals:

[a] if the stdin is open on a directory, use the «getdents» syscall instead
of «read».
Follow-up question: what format should be used for the returned data?
Should this be controlled by a variable such as «BASH_READDIR_FMT»?
(Perhaps as a format string similar to either find -printf or stat(1).)
Or should «read» just ignore IFS and put one field in each named variable
or array element in some fixed order?

[b] if the stdin is a socket, use the «recvmsg» syscall instead of «read»,
which honours the sender's packetisation, rather than relying on delimiter
bytes. Consider adding a shopt to cause «|» and «|&» to create AF_LOCAL
sockets rather than pipes.

Reply via email to