Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash exec sending output to a pipe, how?

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?

like image 392
Harald Avatar asked Aug 14 '14 20:08

Harald


3 Answers

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.
like image 128
Charles Duffy Avatar answered Nov 11 '22 09:11

Charles Duffy


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.

like image 8
Barmar Avatar answered Nov 11 '22 10:11

Barmar


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.

like image 2
JinnKo Avatar answered Nov 11 '22 10:11

JinnKo