I experiment with exec'ing the bash itself only to redirect the output. If I use redirection like
exec >bla.log
ls
exec 1>&2
it works as expected: the ls
output ends up in bla.log
and after the second exec
things are back to normal, mainly because handle 2 is still bound to the terminal.
Now I thought to send the output through a pipe instead of into a file, a trivial example being exec | cat >bla.log
. However, the command immediately returns. To figure out what is going on, I did this:
exec | bash -c 'echo $$; ls -l /proc/$$/fd /proc/23084/fd'
where 23084 is the bash currently running and got this:
24002
/proc/23084/fd:
total 0
lrwx------ 1 harald harald 64 Aug 14 20:17 0 -> /dev/pts/1
lrwx------ 1 harald harald 64 Aug 14 20:17 1 -> /dev/pts/1
lrwx------ 1 harald harald 64 Aug 14 20:17 2 -> /dev/pts/1
lrwx------ 1 harald harald 64 Aug 14 20:17 255 -> /dev/pts/1
/proc/24002/fd:
total 0
lr-x------ 1 harald harald 64 Aug 14 21:56 0 -> pipe:[58814]
lrwx------ 1 harald harald 64 Aug 14 21:56 1 -> /dev/pts/1
lrwx------ 1 harald harald 64 Aug 14 21:56 2 -> /dev/pts/1
As we can see, the sub-process 24002 is indeed listening to a pipe. But it certainly is not the parent process, 23084, which has this pipe open.
Any ideas what is going on here?
The proper way to implement something that might otherwise be written
exec | cat >bla.log
is
#!/bin/bash
# ^^^^ - IMPORTANT: not /bin/sh
exec > >(cat >bla.log)
This is because >()
is a process substitution; it's replaced with a filename (of the /dev/fd/NN
form if possible, or a temporary FIFO otherwise) which, when written to, will deliver to the stdin of the enclosed process. (<()
is similar, but in the other direction: being substituted with the name of a file-like object which will, when read, return the given process's stdout).
Thus, exec > >(cat >bla.log)
is roughly equivalent to the following (on an operating system that doesn't provide /dev/fd
, /proc/self/fds
, or similar):
mkfifo "tempfifo.$$" # implicit: FIFO creation
cat >bla.log <"tempfifo.$$" & # ...start the desired process within it...
exec >"tempfifo.$$" # explicit: redirect to the FIFO
rm "tempfifo.$$" # ...and can unlink it immediately.
When a command contains a pipeline, each subcommand is run in a subshell. So the shell first forks a subshell for each part of the pipeline, and then the subshell for the first part executes exec
with no arguments, which does nothing and exits.
exec
with redirection and no command is treated as a special case. From the documentation:
If command is not specified, any redirections take effect in the current shell, and the return status is 0.
It took me a while to figure out how to get the combination of redirections right for stdout and stderr, so this might be useful to others.
The following examples illustrate using redirections with tee as a "pipe" target while distinguishing stdout and stderr.
#!/bin/bash
echo "stdout"
echo "stderr" >&2
echo "stdout to out.log" | tee out.log
echo "stderr to err.log" 2>&1 >&2 | tee err.log >&2
exec 2> >(tee -a err.log >&2)
exec > >(tee -a out.log)
echo "exec stdout to out.log"
echo "exec stderr to err.log" >&2
Run this in the CLI with stdout redirected to /dev/null and you only see the stderr messages. Plus within each log file you only see the relevant messages.
Note that the order of the exec 2>
line and the exec >
lines is important. Essentially we first want to redirect stderr to an error file, then stdout to a log file. If these lines appear in the opposite order the results would not be correct.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With