Bash – In the code “{ exec >/dev/null; } >/dev/null” what is happening under the hood

bashcommand linefile-descriptorsio-redirectionshell

When you redirect a command list that contains an exec redirection, the exec >/dev/null doesn't seem to still be applied afterwards, such as with:

{ exec >/dev/null; } >/dev/null; echo "Hi"

"Hi" is printed.

I was under the impression that {} command list is not considered a subshell unless it is part of a pipeline, so the exec >/dev/null should still be applied within the current shell environment in my mind.

Now if you change it to:

{ exec >/dev/null; } 2>/dev/null; echo "Hi"

there is no output as expected; file descriptor 1 remains pointed at /dev/null for future commands as well. This is shown by rerunning:

{ exec >/dev/null; } >/dev/null; echo "Hi"

which will give no output.

I tried making a script and stracing it, but I am still unsure exactly what is happening here.

At each point in this script what is happening to the STDOUT file descriptor?

EDIT:
Adding my strace output:

read(255, "#!/usr/bin/env bash\n{ exec 1>/de"..., 65) = 65
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
close(10)                               = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, TCGETS, 0x7ffee027ef90)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hi\n", 3)                     = 3

Best Answer

Let's follow

{ exec >/dev/null; } >/dev/null; echo "Hi"

step by step.

  1. There are two commands:

    a. { exec >/dev/null; } >/dev/null, followed by

    b. echo "Hi"

    The shell executes first the command (a) and then the command (b).

  2. 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.)

  3. Once the first command is done the shell executes echo "Hi". Standard output is wherever it was before the first command.