Appendix F. A Detailed Introduction to I/O and I/O Redirection

written by Stéphane Chazelas, and revised by the document author

A command expects the first three file descriptors to be available. The first, fd 0 (standard input, stdin), is for reading. The other two (fd 1, stdout and fd 2, stderr) are for writing.

There is a stdin, stdout, and a stderr associated with each command. ls 2>&1 means temporarily connecting the stderr of the ls command to the same "resource" as the shell's stdout.

By convention, a command reads its input from fd 0 (stdin), prints normal output to fd 1 (stdout), and error ouput to fd 2 (stderr). If one of those three fd's is not open, you may encounter problems:

 bash$ cat /etc/passwd >&-
 cat: standard output: Bad file descriptor
       

For example, when xterm runs, it first initializes itself. Before running the user's shell, xterm opens the terminal device (/dev/pts/<n> or something similar) three times.

At this point, Bash inherits these three file descriptors, and each command (child process) run by Bash inherits them in turn, except when you redirect the command. Redirection means reassigning one of the file descriptors to another file (or a pipe, or anything permissible). File descriptors may be reassigned locally (for a command, a command group, a subshell, a while or if or case or for loop...), or globally, for the remainder of the shell (using exec).

ls > /dev/null means running ls with its fd 1 connected to /dev/null.

 bash$ lsof -a -p $$ -d0,1,2
 COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    363 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    363 bozo        2u   CHR  136,1         3 /dev/pts/1
 
 
 bash$ exec 2> /dev/null
 bash$ lsof -a -p $$ -d0,1,2
 COMMAND PID     USER   FD   TYPE DEVICE SIZE NODE NAME
 bash    371 bozo        0u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        1u   CHR  136,1         3 /dev/pts/1
 bash    371 bozo        2w   CHR    1,3       120 /dev/null
 
 
 bash$ bash -c 'lsof -a -p $$ -d0,1,2' | cat
 COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    379 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    379 root    1w  FIFO    0,0      7118 pipe
 lsof    379 root    2u   CHR  136,1         3 /dev/pts/1
 
 
 bash$ echo "$(bash -c 'lsof -a -p $$ -d0,1,2' 2>&1)"
 COMMAND PID USER   FD   TYPE DEVICE SIZE NODE NAME
 lsof    426 root    0u   CHR  136,1         3 /dev/pts/1
 lsof    426 root    1w  FIFO    0,0      7520 pipe
 lsof    426 root    2w  FIFO    0,0      7520 pipe

This works for different types of redirection.

Exercise: Analyze the following script.
   1 #! /usr/bin/env bash
   2 
   3 mkfifo /tmp/fifo1 /tmp/fifo2
   4 while read a; do echo "FIFO1: $a"; done < /tmp/fifo1 & exec 7> /tmp/fifo1
   5 exec 8> >(while read a; do echo "FD8: $a, to fd7"; done >&7)
   6 
   7 exec 3>&1
   8 (
   9  (
  10   (
  11    while read a; do echo "FIFO2: $a"; done < /tmp/fifo2 | tee /dev/stderr \
  12    | tee /dev/fd/4 | tee /dev/fd/5 | tee /dev/fd/6 >&7 & exec 3> /tmp/fifo2
  13 
  14    echo 1st, to stdout
  15    sleep 1
  16    echo 2nd, to stderr >&2
  17    sleep 1
  18    echo 3rd, to fd 3 >&3
  19    sleep 1
  20    echo 4th, to fd 4 >&4
  21    sleep 1
  22    echo 5th, to fd 5 >&5
  23    sleep 1
  24    echo 6th, through a pipe | sed 's/.*/PIPE: &, to fd 5/' >&5
  25    sleep 1
  26    echo 7th, to fd 6 >&6
  27    sleep 1
  28    echo 8th, to fd 7 >&7
  29    sleep 1
  30    echo 9th, to fd 8 >&8
  31 
  32   ) 4>&1 >&3 3>&- | while read a; do echo "FD4: $a"; done 1>&3 5>&- 6>&-
  33  ) 5>&1 >&3 | while read a; do echo "FD5: $a"; done 1>&3 6>&-
  34 ) 6>&1 >&3 | while read a; do echo "FD6: $a"; done 3>&-
  35 
  36 rm -f /tmp/fifo1 /tmp/fifo2
  37 
  38 
  39 # For each command and subshell, figure out which fd points to what.
  40 # Good luck!
  41 
  42 exit 0