1

I am trying to write a simple script that takes standard output and standard err and puts the word STDERR: at the beginning of each line of standard err. For testing I have a simple script that outputs a couple lines alternating between standard out and standard err:

#!/usr/bin/perl
print "OUT 1\n";
print STDERR "ERR 1\n";
print "OUT 2\n";
print STDERR "ERR 2\n";

When I run it:

lorkenpeist$ ./testscript.pl
OUT 1
ERR 1
OUT 2
ERR 2

And here is my script stderr.awk to add STDERR:

#!/bin/awk -f
{print "STDERR: " $0}

If I run ./testscript.pl | ./stderr.awk (which is obviously wrong because I'm piping standard out instead of standard err):

lorkenpeist$ ./testscript.pl | ./stderr.awk
ERR 1
ERR 2
STDERR: OUT 1
STDERR: OUT 2

I see that standard err is output immediately, while standard output is delayed because of the pipe. The original order of the print statements is not preserved.

I can also redirect standard err to standard output:

lorkenpeist$ ./testscript.pl 2>&1 | ./stderr.awk
STDERR: ERR 1
STDERR: ERR 2
STDERR: OUT 1
STDERR: OUT 2

Not only is everything processed by stderr.awk instead of just standard err, but again the order of the print statements is not preserved. Is there any way to send just the standard err to stderr.awk, and also preserve the order of the print statements? What I'd really like to see is:

OUT 1
STDERR: ERR 1
OUT 2
STDERR: ERR 2

I'm beginning to suspect that IO redirection simply isn't the answer, but I'm at a loss for alternatives.

EDIT: Given that standard output is buffered and standard err is not, It looks like I don't have complete control over the order in which the print statements appear on the terminal. That being said, I would prefer if order was at least somewhat preserved, instead of all of standard err being printed before any of standard output. Alternatively, is there a way to make standard output and/or pipes unbuffered?

Lorkenpeist
  • 1,475
  • 2
  • 13
  • 26
  • 1
    On most o/s'es stderr is immediately flushed when a line is written to it (not buffered); whereas stdout is buffered by the o/s (i.e. it buffers up multiple lines of text before it outputs it to the terminal). Therefore everything that goes to stderr is outputted straight away, whereas stuff that is sent to stdout is not outputted until the o/s decides that it is time to do so. This text written to stderr later may appear on the terminal earlier than lines written to stdout later. – tbsalling Jun 20 '13 at 21:14

2 Answers2

5

Like Andy Lutomirski, I think that How to pipe stderr and not stdout can help.

Here's a command sequence that filters standard error without changing standard output; ./genouterr.sh generates information on both standard output and standard error.

( ./genouterr.sh 2>&1 1>&3 | sed 's/^/STDERR: /' >&2) 3>&1

What that does is:

  • Run a subshell (...) and sends it output from file descriptor 3 to standard output (3>&1).
  • In the subshell, it runs the command ./genouterr.sh, piping its standard output to sed.
  • But, before running the shell script, it arranges for error output to go to standard output (2>&1), and for standard output to go to file descriptor 3 (1>&3). Thus, anything written to standard output by the script goes to file descriptor 3 in fact, while anything written to standard error by the script goes down the pipe.
  • The RHS of the pipeline is sed, inserting STDERR: at the start of each line, and then redirecting its standard output to standard error (1>&2).

The net result is that what ./genouterr.sh writes on standard output appears on standard output; what ./genouterr.sh writes on standard error is prefixed with STDERR: and appears on standard error.

Note that because of buffering and so on, the output from the commands may interleave standard output and standard error differently from the way they'd appear on the terminal screen without any I/O redirection. This is pretty close to unavoidable. If you are going to avoid it, you get involved in using pseudo-ttys (ptys), which is a complex area, and I'd not want to guarantee the behaviour even then if you need to filter something through sed.

The script genouterr.sh simply generates both standard output and standard error lines:

#!/bin/bash
for i in {01..50}
do
  echo "stdout $i"
  echo "stderr $i" >&2
done

And the original test I did (I wrote this a month or two ago, as an exercise extending the other question) was looking for standard error lines containing a number ending in 0 (using grep); changing that to a sed script is a work of moments.

#!/bin/bash
set -x
rm -f out.[123]
./genouterr.sh 1>/dev/null
./genouterr.sh 2>/dev/null
( ./genouterr.sh 2>&1 1>&3 | grep '[0-9]0' >&2) 3>out.3 2>out.2 1>out.1
ls -l out.[123]
( ./genouterr.sh 2>&1 1>&3 | grep '[0-9]0' >&2) 3>&1

When you run this script, you find that the file out.1 is empty, out.3 contains what genouterr.sh wrote to standard output, and out.2 contains the filtered version of what genouterr.sh wrote to standard error.

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • This is a little better than my answer (it avoids the asynchronicity issues) as long as you're okay with trashing fd 3. – Andy Lutomirski Jun 22 '13 at 00:16
  • If you have a specific use for fd 3, you can change the number to any other unused fd (4..255, at least in principle; multi-digit numbers would need to be checked), but few systems run with fd 3 in use for any purpose (indeed, usually not with anything other than fds 0-2). – Jonathan Leffler Jun 22 '13 at 00:27
2

The answers from How to pipe stderr, and not stdout? may be useful. Your best bet is probably something like:

(echo 'out'; echo 'err' >&2; echo 'out2'; echo 'err2' >&2) 2> >(sed -e 's/^/STDERR: /' >&2)

This has synchronicity issues, though -- the last couple STDERR lines will be written after bash thinks the command is done.

To improve on it, you could kick off the sed command as a separate asynchronous process and then explicitly wait for it, but this will quickly get messy.

You'll need to be careful, though: libc won't buffer writes to stderr. For more deterministic behavior, you can make sure that stdout is a tty by creating a pseuto-tty (see the manpage for pty for details -- there's little in the way of documentation for this). For that fancy stuff, though, you'll probably have to use a real programming language (e.g. C or Python).

Community
  • 1
  • 1
Andy Lutomirski
  • 1,343
  • 12
  • 15