Filter both stderr and stdout, but keep them on stderr and stdout in a /bin/sh compatible way

60 Views Asked by At

This question resembles Pipe only STDERR through a filter, but I want both STDOUT and STDERR and I do not want output merged.

I want:

STDOUT ── [ filter ] ────> STDOUT
                       
STDERR ── [ filter ] ────> STDERR

And I want to keep $?. If myprogram is a function, I want it to be able to change the environment.

In Bash I can do:

myprogram() {
    echo stdout;
    echo stderr >&2;
    myvar=this_value
    (exit 3)
}

myfilter() {
    perl -pe "s/^/$@ /"
    echo filter "$@" stderr >&2
}

{ myprogram || foo=$?.$myvar; } 2> >(myfilter errfilter >&2) > >(myfilter outfilter)
wait
echo error=$foo
echo Done

This works on a read-only file system.

I want to do the same in /bin/sh. But there is no >(cmd) construct.

In /bin/sh I can do:

{ myprogram || foo=$?.$myvar; } >/tmp/tmpout 2>/tmp/tmperr
myfilter errfilter </tmp/tmperr >&2
myfilter outfilter </tmp/tmpout >&1
echo error=$foo
echo Done

(This will fail if stdout/stderr from myprogram is bigger than the disk, and will not work on a read-only file system).

Or:

mkfifo /tmp/fifoout
mkfifo /tmp/fifoerr
myfilter errfilter </tmp/fifoerr >&2 &
myfilter outfilter </tmp/fifoout >&1 &
{ myprogram || foo=$?.$myvar; } >/tmp/fifoout 2>/tmp/fifoerr
wait
echo error=$foo
echo Done

(This will fail if /tmp is read-only).

Can it be done in /bin/sh without using temporary files or named pipes?

I am thinking whether this can be done with some exec 2>&- 3>&1 magic.

This does not work, but I have the feeling it could work given the correct exec magic:

exec 3>&1 4>&2
myfilter outfilter <&3 >&1 &
myfilter errfilter <&4 >&2 &
{ myprogram || foo=$?.$myvar; } 1>&3 2>&4
wait
echo error=$foo
echo Done
2

There are 2 best solutions below

1
Ole Tange On

Not tested in detail, but it seems to work:

{
    exec 3>&1 
    myprogram 11 2>&1 >&3 | myfilter ferr >&2
    exec 3>&-
} | myfilter fout

However, it does not keep $? from myprogram.

5
ikegami On

Working on Ole Tange's attempt at a solution,

exec 3>&1                    # fd 3 = Original fd 1.
status="$(             # )"  # Syntax highlighting hack.
   exec 4>&1                 # fd 4 = Pipe to backticks (and thus to `$status`).
   {
      exec 5>&1              # fd 5 = Pipe to `filter out`.
      {
         program 2>&1 >&5    # Send to `filter out`.
         printf %s "$?" >&4  # Send to backticks (and thus to `$status`).
      } | filter err >&2
      exec 5>&-
   } | filter out >&3        # Send to original fd 1.
   exec 4>&-
)"                     # "   # Syntax highlighting hack.
exec 3>&-
( exit "$status" )           # Set `$?`.

Sample program and filter for testing:

program() { perl -e'print "a\nb\n"; warn "c\nd\n"; exit 42'; }

filter() { perl -spe'$_="$f $_"' -- -f="$1"; }