How it works
The explanation, with examples you can run.
See a process's descriptors
/proc/self/fd lists the descriptors of whichever process reads it, here ls:
ls -l /proc/self/fdls -l /proc/self/fd | catls -l /proc/self/fd 2>/dev/nulllrwx------ 1 root root 64 Oct 16 10:23 0 -> /dev/pts/0lrwx------ 1 root root 64 Oct 16 10:23 1 -> /dev/pts/0lrwx------ 1 root root 64 Oct 16 10:23 2 -> /dev/pts/0lr-x------ 1 root root 64 Oct 16 10:23 3 -> /proc/3557/fd lrwx------ 1 root root 64 Oct 16 10:23 0 -> /dev/pts/0l-wx------ 1 root root 64 Oct 16 10:23 1 -> pipe:[17960]lrwx------ 1 root root 64 Oct 16 10:23 2 -> /dev/pts/0lr-x------ 1 root root 64 Oct 16 10:23 3 -> /proc/3558/fd lrwx------ 1 root root 64 Oct 16 10:23 0 -> /dev/pts/0lrwx------ 1 root root 64 Oct 16 10:23 1 -> /dev/pts/0l-wx------ 1 root root 64 Oct 16 10:23 2 -> /dev/nulllr-x------ 1 root root 64 Oct 16 10:23 3 -> /proc/3560/fd(The total 0 lines are left out and blank lines added between runs.) At a terminal all three point at the terminal device. Piping into cat replaces only 1 with a pipe; 2>/dev/null replaces only 2. Descriptor 3 is the directory ls opened to read the listing. ls itself has no idea any of this happened.
Redirection, left to right
ls /etc/hostname /nope > out.txt 2> err.txt; echo "exit=$?"cat out.txt err.txtls /etc/hostname /nope > all.txt 2>&1; cat all.txtls /etc/hostname /nope 2>&1 > only-out.txtexit=2/etc/hostnamels: cannot access '/nope': No such file or directoryls: cannot access '/nope': No such file or directory/etc/hostnamels: cannot access '/nope': No such file or directory2>&1 means "make descriptor 2 a copy of whatever 1 is right now" (the shell calls dup2). In the last line 2 copies the terminal before 1 is pointed at the file, so the error still reaches the screen and only the listing goes to only-out.txt. >> appends instead of truncating, < feeds a file to stdin, and &> is bash shorthand for "both".
Pipes
a | b asks the kernel for a pipe, a buffer with a write end and a read end, then forks both processes with a's descriptor 1 on the write end and b's descriptor 0 on the read end. Both run at the same time. On Linux a pipe holds 64 KiB by default; when it is full the writer blocks until the reader catches up, which keeps a fast producer from flooding memory. If the reader exits, the next write gets SIGPIPE (or the error EPIPE), which is why yes | head -1 ends straight away instead of running forever. A pipeline's exit status is the last command's; set -o pipefail makes a failure anywhere count.
Leaks and limits
Every open descriptor counts against a per-process limit (ulimit -n, covered with resource limits). A server that opens files or sockets and forgets to close them eventually fails with EMFILE: Too many open files. ls /proc/<pid>/fd | wc -l or lsof -p <pid> shows how many it holds.
On macOS
Descriptors, redirection and pipes work the same way in zsh and bash on macOS. There is no /proc; use lsof -p <pid> to list a process's open files, or ls /dev/fd to see the current process's descriptors.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.