File descriptors and exec redirections

Bash · Expert · 12 min

Advanced logging, locking and two-way communication all rely on managing descriptors yourself.

Redirections on exec without a command apply to the current shell for the rest of the script:

  • exec 3> file opens fd 3 for writing, exec 3< file for reading, exec 3<> file both,
  • exec 3>&- closes it,
  • exec {fd}> file lets bash choose a free number (10 or above) and stores it in fd; use >&"$fd".

Duplication n>&m makes fd n a copy of fd m at that moment, which explains classic tricks. cmd 2>&1 >/dev/null sends only stderr into a pipe and discards stdout. 3>&1 1>&2 2>&3 3>&- swaps stdout and stderr through a spare descriptor.

exec > >(tee -a run.log) 2>&1 near the top of a script copies all following output to a log file while still showing it. Because the tee runs asynchronously, its last lines can appear after the script exits.

Descriptors also power locking: exec 9> /var/lock/job.lock; flock -n 9 || exit 1 ensures only one instance runs, and the lock is released automatically when the process exits.

coproc NAME { cmd; } starts a background process with two pipes; write to ${NAME[1]} and read from ${NAME[0]}. Watch for buffering: many programs only flush output when their input closes. Inspect a process's descriptors in /proc/$$/fd.

Example

cd "$(mktemp -d)"
exec 3> log.txt                # fd 3 stays open for the rest of the script
echo "to the log" >&3
exec 3>&-                      # close it
cat log.txt                    # to the log
exec {fd}< /etc/passwd         # bash picks a free fd number
read -r first <&"$fd"; exec {fd}<&-
echo "${first%%:*}"            # root
{ echo out; echo err >&2; } 2>&1 >/dev/null | tr a-z A-Z   # ERR
{ echo out; echo err >&2; } 3>&1 1>&2 2>&3 3>&- | cat   # err via the pipe, out on stderr
exec 9> job.lock
flock -n 9 && echo "got the lock"   # got the lock
coproc UP { tr a-z A-Z; }
echo "hello coproc" >&"${UP[1]}"; exec {UP[1]}>&-
read -r line <&"${UP[0]}"; echo "$line"   # HELLO COPROC
ls /proc/$$/fd                 # 0 1 2 9 255 ...  (open descriptors)

Remember

  • exec with only redirections changes the current shell's descriptors
  • n>&m copies m at that moment; order explains 2>&1 >/dev/null
  • flock on an fd gives easy single-instance locking

Watch out for

  • Expecting cmd >/dev/null 2>&1 and cmd 2>&1 >/dev/null to be the same.

Check yourself

  1. What reaches the pipe in `cmd 2>&1 >/dev/null | less`?
  2. What does `exec 3>&-` do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.gnu.org/software/bash/manual/bash.html#Redirections