In the code "{ exec >/dev/null; } >/dev/null" what is happening under the hood?
Let's follow
{ exec >/dev/null; } >/dev/null; echo "Hi"
step by step.
There are two commands:
a.
{ exec >/dev/null; } >/dev/null
, followed byb.
echo "Hi"
The shell executes first the command (a) and then the command (b).
The execution of
{ exec >/dev/null; } >/dev/null
proceeds as follows:a. First, the shell perform the redirection
>/dev/null
and remembers to undo it when the command ends.b. Then, the shell executes
{ exec >/dev/null; }
.c. Finally, the shell switches standard output back to where is was. (This is the same mechanism as in
ls -lR /usr/share/fonts >~/FontList.txt
-- redirections are made only for the duration of the command to which they belong.)Once the first command is done the shell executes
echo "Hi"
. Standard output is wherever it was before the first command.
In order to not use a sub-shell or sub-process, when the output of a compound list {}
is piped >
, the shell saves the STDOUT descriptor before running the compound list and restores it after. Thus the exec >
in the compound list does not carry its effect past the point where the old descriptor is reinstated as STDOUT.
Let's take a look at the relevant part of strace bash -c '{ exec >/dev/null; } >/dev/null; echo hi' 2>&1 | cat -n
:
132 open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
133 fcntl(1, F_GETFD) = 0
134 fcntl(1, F_DUPFD, 10) = 10
135 fcntl(1, F_GETFD) = 0
136 fcntl(10, F_SETFD, FD_CLOEXEC) = 0
137 dup2(3, 1) = 1
138 close(3) = 0
139 open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
140 fcntl(1, F_GETFD) = 0
141 fcntl(1, F_DUPFD, 10) = 11
142 fcntl(1, F_GETFD) = 0
143 fcntl(11, F_SETFD, FD_CLOEXEC) = 0
144 dup2(3, 1) = 1
145 close(3) = 0
146 close(11) = 0
147 dup2(10, 1) = 1
148 fcntl(10, F_GETFD) = 0x1 (flags FD_CLOEXEC)
149 close(10) = 0
You can see how, on line 134, descriptor 1
(STDOUT
) is copied onto another descriptor with index at least 10
(that's what F_DUPFD
does; it returns the lowest available descriptor starting at the given number after duplicating onto that descriptor). Also see how, on line 137, the result of open("/dev/null")
(descriptor 3
) is copied onto descriptor 1
(STDOUT
). Finally, on line 147
, the old STDOUT
saved on descriptor 10
is copied back onto descriptor 1
(STDOUT
). The net effect is to insulate the change to STDOUT
on line 144
(which corresponds to the inner exec >/dev/null
).
The difference between { exec >/dev/null; } >/dev/null; echo "Hi"
and { exec >/dev/null; }; echo "Hi"
is that the double redirection does dup2(10, 1);
before closing fd 10 which is the copy of the original stdout
, before running the next command (echo
).
It happens that way because the outer redirect is actually overlaying the inner redirect. That's why it copies back the original stdout
fd once it completes.