bash exec sending output to a pipe, how?

Harald picture Harald · Aug 14, 2014 · Viewed 8.2k times · Source

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?

Answer

Charles Duffy picture Charles Duffy · Aug 15, 2014

What

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)

Why

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.