14

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
  • That's strange; I can't reproduce the close(10). Can you also post your entire script contents that you ran strace on? – DepressedDaniel Dec 21 '16 at 0:35
  • @DepressedDaniel Here is the full script and strace: script strace – Joey Pabalinas Dec 21 '16 at 3:37
  • You have a stray ; after }, which changes the meaning of > /dev/null to not apply to the compound list {} after all. – DepressedDaniel Dec 21 '16 at 4:12
  • @DepressedDaniel Ah, you are completely correct! Now the output is what I expect; thank you for your answers! – Joey Pabalinas Dec 21 '16 at 5:13
18

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.

  • Is there some reason behind why 2a is executed before 2b? (right-to-left) – Joey Pabalinas Dec 21 '16 at 0:07
  • 5
    Redirections must be executed before the command to which they apply, no? How could they work otherwise? – AlexP Dec 21 '16 at 0:09
  • Aha, never thought of it that way! First two are borh great answers; giving it a little bit before I decide on one, but I appreciate both explanations! – Joey Pabalinas Dec 21 '16 at 0:13
  • Unfortunately I can only pick one answer, so I am going with this one since it is a bit less technical and thus I think it would be able to help even the less tech-savvy users. However @DepressedDaniel had an equally great answer here that offers a more in depth explanation. – Joey Pabalinas Dec 21 '16 at 5:20
13

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

  • Since FD 1 is overwritten by FD 3 on line 137, why does line 141 not point 10 to /dev/null? – Joey Pabalinas Dec 21 '16 at 0:04
  • @JoeyPabalinas Line 141 is duplicating FD 1 (i.e., stdout) to the next available descriptor after 10, which turns out to be 11, as you can see in the return value from that system call. 10 is just hard-coded into bash so that bash's descriptor saving won't interfere with single-digit descriptors you might manipulate in your script via exec. – DepressedDaniel Dec 21 '16 at 0:12
  • So then fcntl(1, F_DUPFD, 10) will always refer to STDOUT no matter where FD 1 is currently pointing? – Joey Pabalinas Dec 21 '16 at 0:17
  • @JoeyPabalinas Not sure what your question is. FD 1 IS STDOUT. They are the same thing. – DepressedDaniel Dec 21 '16 at 0:22
  • Added full strace output to my original post. – Joey Pabalinas Dec 21 '16 at 0:29
8

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.

  • +1 for explaining the difference in easy way. AlexP's answer lacks of this explanation. – Kamil Maciorowski Dec 21 '16 at 13:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.