A descriptor is a slot you can rebind
◈ 10 cardsTell apart the two unrelated jobs the exec command does — replacing the shell process image, and rewiring the shell own descriptor table — and read two files in lockstep from one script.
One word, two unrelated jobs
exec is really two commands sharing a name, and telling them apart is this lesson.
- With a command,
execreplaces the shell. - With only redirections and no command,
execrewires the shell.
They have nothing to do with each other. Almost every mistake made with exec is a case of expecting one of them and getting the other.
Role one: overlay this process
Normally, running date from the shell means: fork a child, exec date into the child, wait for it to finish, print a prompt. With exec date the fork does not happen. The kernel overlays the shell’s own process image with date. Same process id, same descriptors, different program. The shell is gone — not suspended, gone — and when date finishes there is nothing left to return to.
Run exec date from a login shell and you see the date and then the login prompt, because the process that just exited was the one the system was waiting on for that session.
Two consequences the exam builds questions from:
Nothing after exec cmd in a script ever runs. Not the next line, not the cleanup, not the echo done you put at the bottom. The only exception is failure: if cmd cannot be found, there is nothing to overlay with, so the shell reports the error and (being non-interactive) exits.
Parenthesise it and only the subshell dies. ( exec date ) starts a subshell, overlays that with date, and when date exits the subshell is finished — which is what a subshell does anyway. Your shell is untouched and the next line runs normally.
Why anyone wants role one: a wrapper. A startup script that sets an environment, changes a directory, drops a privilege and then ends with exec "$@" hands the process over completely, leaving no extra shell sitting in the process table holding memory and intercepting the signals meant for the real program.
Role two: rebind this shell’s descriptors
Every process has a table of open file descriptors — small numbered slots, each pointing at something the process can read or write. Slot 0 is standard input, slot 1 standard output, slot 2 standard error. Slots 3 and up are free for you.
A redirection on an ordinary command line changes the child’s table, for that one command: sort < in > out rebinds slots 0 and 1 in the process running sort, and your shell is unaffected. exec with no command changes the shell’s own table, and the change lasts for the rest of the script.
exec > report.out
echo this line lands in the file
exec > /dev/tty
echo and this one is back on screen
The forms:
exec < file— the rest of the script reads its standard input fromfileexec > fileandexec >> file— standard output tofile, truncating or appendingexec n< file— openfilefor reading on descriptornexec n> file— openfilefor writing on descriptornexec n>&m— makena duplicate ofmexec n<&-andexec n>&-— close descriptorn
The number may be omitted, and then it defaults to the descriptor the operator implies: < reads, so exec <&- closes descriptor 0, standard input; > writes, so exec >&- closes descriptor 1, standard output. Some editions of the textbook’s descriptor table label both of them as closing standard output. That cannot be right — it would leave no spelling at all for closing standard input.
Worked example — two files in lockstep
Here is the thing you cannot do with ordinary redirection. A loop written while read line; do ...; done < f1 owns exactly one input, so it can walk one file. To compare two files line by line you need two descriptors:
#!/bin/sh
exec 3< "$1"
exec 4< "$2"
n=0
while read left 0<&3 && read right 0<&4
do
n=$((n + 1))
[ "$left" = "$right" ] || echo "line $n differs"
done
exec 3<&- 4<&-
Step by step. exec 3< "$1" opens the first file for reading and parks it in slot 3 of the shell’s own table; exec 4< "$2" does the same for the second file in slot 4. Slots 0, 1 and 2 are untouched, so the script can still write to the terminal.
Inside the loop, read left 0<&3 runs the read built-in with descriptor 0 temporarily duplicated from descriptor 3 — so read takes its line from the first file, and the file’s read offset advances. read right 0<&4 does the same for the second. The && means the loop ends as soon as either file runs out.
exec 3<&- 4<&- closes both borrowed slots. Two closes in one command is fine; the shell applies the redirections left to right.
Extending it to a third file is now a mechanical change: exec 5< "$3", a third read, a third comparison, and 5<&- on the close.
Getting your terminal back
Role two has a sharp edge. Type exec > log at an interactive prompt and the session appears to die: the prompt, your commands’ output, everything is going into log. The shell is fine; you have simply disconnected yourself from it.
The cure is /dev/tty, which always names the calling process’s own controlling terminal — so it works without you knowing which terminal you are on:
exec > /dev/tty— standard output backexec 2> /dev/tty— standard error backexec < /dev/tty— standard input back
Note what does not work: exec > /dev/stdout. On Linux that path is a link to descriptor 1 of the current process, which is the log file you are trying to escape from, so it reopens the log.