Perhaps I should explain my background. Before I started using Bash, for about ten years my job was writing software that interacted with devices connected to serial ports. At that time, CPU speeds were such that I would only write such software in C, so I didn't really care what «read» in Bash did.
Now that machines are about 100× faster, I figure I should be able to write simple stuff as shell scripts without resorting to C, and indeed it works some of the time, but I've found that «read» has some “unexpected” corner cases,(*1) especially actions that are quite easy from a C program but which the «read» built-in flatly refuses to perform regardless of the options I give it. This is why I say that its design is “suboptimal”. I recognize that I'm in a small minority with my use cases, but (other than strict backwards compatibility) having options that provide simple ways to work with serial devices does not negatively affect “ordinary” users. On Sat, 18 Oct 2025 at 03:13, Chet Ramey <[email protected]> wrote: > On 10/13/25 11:08 PM, Martin D Kealey wrote: > > In hindsight I think this was a suboptimal design choice; I would have > preferred it to be treated like O_NONBLOCK. > > Hindsight is always perfect. > Agreed; we all try to do our best with what we have at the time. I wish I'd tried writing tty controller stuff as shell scripts back in 2011, then I could have reported this much sooner. > «read» treating zero size* […] *this depends on how the «read» syscall > treats a zero «count» > > parameter. > > POSIX specifies the behavior, but if you want to call read specifying zero > characters, bash will defer to the OS. > The «read» syscall always (eventually) returns zero unless the fd is closed or broken. It might or might not block if there's no input available; POSIX doesn't specify how long the call will take; I've seen both. That said, when choosing between allowed behaviours, I posit that it is more *useful* to provide more differentiated information, which in this case would be to report when a future non-zero read is likely to successfully return data (or report an error), even if that means using different syscalls to do so. So I'm suggesting that «read -n0» (or equivalently «read -N0») should simply use «select»/«pselect», and not use «read» at all on unseekable filedescriptors. Seekable filedescriptors are often not supported by «select», reporting EINVAL in that case, when that happens simply doing a non-blocking «read» should suffice. On the off chance that «select» *does* support seekable filedescriptors, it will give the right answer so we don't need to do anything special to cover that case. > 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. > > Bash doesn't use non-blocking reads. Indeed, it currently doesn't. But to implement the behaviour I'm describing, it likely would have to. > I'm guessing this is because this corresponds to the (very) small delay > > between setting an alarm timer, and then initiating a read. > > `read' timeouts dont use SIGALRM; it uses the timeout with select. However, > if the timer has already expired before bash calls select, it will return > an `expired' status. > It does on systems that lack both select and pselect. A lot of effort has been made in implementing the timer/alarm framework to make sure the p/select version has (what is to me) the same failure as the SIGALRM version: it can fail to return available input when the time limit is small but non-zero, depending on what the fd is connected to. > 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”. > > It means neither of those things. It means "tell me whether input is > available." > Yes, that's what it *does*; I'm using *should* in the context of describing what I would prefer. > Currently when «read» is given «-t0», both «-n» and «-d» are ignored, > > They're not. > Ermmm… am I mis-reading the following? Right after the internal_getopts loop, at lines 420~426 in builtins/read.def says: if (have_timeout && tmsec == 0 && tmusec == 0) { int ct; /* change terminal settings */ ct = (nflag || delim != '\n') && isatty (fd); return (check_read_input (fd, ct) ? EXECUTION_SUCCESS : EXECUTION_FAILURE); } OK, to be fair they slightly tweak the IO settings, but they make no difference to the quantity of data retrieved. (Arguably it has a false positive when there's a partial line of data available and the delimiter isn't NL.) Temporarily setting aside issues of backwards compatibility, it is my contention that this should look more like: if (have_timeout && tmsec == 0 && tmusec == 0) { if (nflag && nchars == 0) return check_read_input (fd, isatty (fd)) ? EXECUTION_SUCCESS : EXECUTION_FAILURE); if (compat_level < 54 && delim != '\n') return check_read_input (fd, isatty (fd)) ? EXECUTION_SUCCESS : EXECUTION_FAILURE); } (Further on it appears that when -n0/-N0 is given (and -t0 is not), -t and -d are ignored; however I haven't yet dredged through all the code so I won't absolutely guarantee that.) > 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. > > I think the current behavior of read -t0 -N1 is fine for this. > This is the crux of the aforementioned suboptimal design choice. I regard «read -t0 -N1» to be currently broken, because it fails to read (and return) a byte when one is already available: $ echo X | ( sleep 1 ; read -t0 -N1 ; echo "s=$? l=${#REPLY} r={$REPLY}" ) s=0 l=0 r={} However, that's not to say that the existing behaviour isn't useful. To ensure that it remains available, I'm suggesting that -t0 -N0 be defined as providing it. And to head off arguments about backwards compatibility, I'm not proposing to change read -t0 without either -d or -n/-N. > 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). > > The problem is that there is no portable way to determine `what is already > available'. This approach might work with pipes, but it doesn't extend to > other types of file descriptors. > I'd be happy with “whatever the kernel can provide from a non-blocking read”. > 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: > > You can't know this Can't know what? Whether a line is available? That's exactly why I specified both cases, when I continued with: > (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”. > > Are you saying the script/application would change the terminal modes > itself? > It's allowed to, which is why I said “already”; but it doesn't have to, since that's normally the default. Switching in and out of ICANON mode is already done by read, based on whether -n/-N is used, and I certainly don't plan to change that. The only addition would be using O_NONBLOCK, and/or c_cc[VTIME]=0 (when not in ICANON mode). > 4. Alter «read -t0 -n0 -d$'\n'» to perform look-ahead while remaining in > > ICANON mode. > This doesn't work: either you put the fd in non-blocking mode (or ~ICANON > mode) or select/pselect will return failure. That's where we started with > this issue. > Perhaps I wasn't making myself clear: look-ahead SHOULD return “nothing available” when reading from a tty in ICANON and a newline has not been received. Bytes in the kernel's line editing buffer will not necessarily be returned by read, as they may be expunged by typing the ERASE, WERASE, (line-) KILL, or FLUSH characters. By leaving the tty in ICANON mode, select/pselect will correctly indicate 0 when a delimiter has not been received (so nothing is yet available to read), and will indicate 1 when a whole line is available (or when the device has hung up, resulting in EOF on read). The crux of my proposed change is that when -n0 and -d are both used, «read» should not switch to ICANON mode, because the intention is merely to "peek", not to actually read any data. I'm leaving unspecified what happens when you use -d and -n with a number greater than zero. > 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». > > Timeouts don't use SIGALRM or non-blocking reads. > Point taken. Some effort seems to have been taken to emulate this. > 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.) > > kre brought this up earlier; I haven't looked at it because the existing > code path uses single-byte reads. > Those single-byte reads get really expensive in some cases; they can dominate the overall time of some scripts. -Martin (*1: “unexpected” based on my prior knowledge of the standard tty line discipline, as well as file-descriptor manipulation primitives.)
