Shells open a pipe between the two processes linked by a pipe symbol|
. The shells (sh, ksz, zsh, bash) differ a bit in how exactly they implement that (for example: is one process the executing shell itself, and if yes, which one?); but the important thing to realize is that it is two processes communicating through a one-directional pipe. The writing process writes to standard out, which by default is file descriptor 1, and the other one reads from standard in, which by default has file descriptor 0. The operating system buffers data written by one and provides it to the reading one on the other end.
In the end, all higher-level language specific I/O routines (for example printf or fread from the C standard library) call the low-level I/O system calls. Low-level I/O is one of the functionalities that an operating system provides. The POSIX specific user-land system call read()
from unistd.h which you call from your program is a wrapper around a kernel system call. The kernel has knowledge about the actual transport mechanism underlying a file descriptor (a pipe, a file, a socket) and about that specific facility's state. It communicates with the hardware through device drivers which eventually read from actual electric lines, and report back up the chain of abstraction, until it reaches user accessible information like the read()
call and errno
. Higher-level functions like fread()
evaluate this information and transform it to their own specification (return EOF, return true for subsequent feof()
).
As Kamil pointed out, the specification of read()
specifies that the end-of-file condition is reported by returning 0 even though more than 0 bytes were requested. If end of file was, by contrast, not reached but instead the communication was just stalled for some reason — a seeking disk, a spotty WLAN —, the default read()
call would block until data was available. If it was called non-blocking, it would return with -1 and set errno to EAGAIN.
In your case, the condition for end-of-file for the pipeline between the bash loop and fzf
in
for i in $(seq 1 5); do echo $i; sleep 1 ; done | fzf
is that the left side closes the writing end of the pipeline. This is reported to the reading side by the kernel, and a pending user-land read()
will return empty, possibly after coming back with fewer bytes than requested first (but a short read alone is not a sign of end-of-file!).
When you redirect input to come from a file, things are different. Reaching the end of the file at a given moment, as in your example, is literally end-of-file and communicated as such. A program like tail -f
which wants to monitor changes to a file may need to monitor the file state or employ operating system specific mechanisms exceeding the smallest common denominator of the POSIX specification, for example inotify
.