2

Usually paste prints two named (or equivalent) files in adjacent columns like this:

paste <(printf '%s\n' a b) <(seq 2)

Output:

a   1
b   2

But when the two files are /dev/stdin and /dev/stderr, it doesn't seem to work the same way.

Suppose we have blackbbox program which outputs two lines on standard output and two lines on standard error. For illustration purposes, this can be simulated with a function:

bb() { seq 2 | tee >(sed 's/^/e/' > /dev/stderr) ; }

Now run annotate-output, (in the devscripts package on Debian/Ubuntu/etc.), to show that it works:

annotate-output bash -c 'bb() { seq 2 | tee >(sed 's/^/e/' > /dev/stderr) ; }; bb'
22:06:17 I: Started bash -c bb() { seq 2 | tee >(sed s/^/e/ > /dev/stderr) ; }; bb
22:06:17 O: 1
22:06:17 E: e1
22:06:17 O: 2
22:06:17 E: e2
22:06:17 I: Finished with exitcode 0

So it works. Feed bb to paste:

bb | paste /dev/stdin /dev/stderr

Output:

1   e1
e2
^C

It hangs -- ^C means pressing Control-C to quit.

Changing the | to a ; also doesn't work:

bb ; paste /dev/stdin /dev/stderr

Output:

1
2
e1
e2
^C

Also hangs -- ^C means pressing Control-C to quit.

Desired output:

1    e1
2    e2

Can it be done using paste? If not, why not?

agc
  • 7,045
  • 3
  • 23
  • 53
  • Similar: [tee + cat: use an output several times and then concatenate results](//unix.stackexchange.com/q/66853) – Stéphane Chazelas Oct 09 '18 at 12:22
  • Possible duplicate of [Split an input for different command and combine the result](//unix.stackexchange.com/q/183356) – Stéphane Chazelas Oct 09 '18 at 12:24
  • 3
    You're never redirecting stderr, so it will probably be the terminal device if you run that from a terminal. Reading from a terminal device (like your `paste /dev/stderr` does) reads what you type on the keyboard, writing to it (like your `tee /dev/stderr` does) sends it for display by the terminal emulator. – Stéphane Chazelas Oct 09 '18 at 12:34
  • Could you tell us exactly what you want to do? You can go with `for i in $(seq 2); do echo -n "$i " > /dev/stdout; echo e$i > /dev/stderr; done` and you will get the same effect – mrc02_kr Oct 09 '18 at 12:39
  • 1
    @Goro, Not a duplicate of that, since [Split an existing prompt command line in a few lines](https://unix.stackexchange.com/questions/79696/split-an-existing-prompt-command-line-in-a-few-lines) has nothing to do with stderr. Please retract. – agc Oct 09 '18 at 13:29
  • @mrc02_kr, I don't want the same effect. The question is: is it possible to do with `paste` or not possible? A negative answer would be just as useful. – agc Oct 09 '18 at 13:36
  • 1
    @StéphaneChazelas, Thanks, [Split an input for different command and combine the result](https://unix.stackexchange.com/q/183356) is certainly related, but it's really not about *stderr* as such. – agc Oct 09 '18 at 13:43
  • @StéphaneChazelas, Re *"You're never redirecting stderr..."*: did you mean that in the general sense, (it's impossible), or specific to this code, (*i.e.* the OP code doesn't do it, but perhaps should)? – agc Oct 09 '18 at 13:52
  • It would need a long answer to clear your confusions, but unfortunately I don't have time now. You can look at the linked Q&A for how to do it. – Stéphane Chazelas Oct 09 '18 at 13:56
  • 1
    See also: [Why can't I read /dev/stdout with a text editor](https://unix.stackexchange.com/questions/202263/why-cant-i-read-dev-stdout-with-a-text-editor) – agc Oct 09 '18 at 14:17
  • in or out? (did you make an error in the question?) – ctrl-alt-delor Oct 10 '18 at 08:49
  • @mosvy, Please post [your last comment](https://unix.stackexchange.com/questions/474232/how-to-paste-stdin-next-to-stderr?noredirect=1#comment867027_474232) as an answer. – agc Oct 10 '18 at 22:15
  • I don't see this as duplicate of mentioned questions. –  Oct 11 '18 at 00:40

3 Answers3

4

Why you can't use /dev/stderr as a pipeline

The problem isn't with paste, and neither is it with /dev/stdin. It's with /dev/stderr.

All commands are created with one open input descriptor (0: standard input) and two outputs (1: standard output and 2: standard error). Those can typically be accessed with the names /dev/stdin, /dev/stdout and /dev/stderr respectively, but see How portable are /dev/stdin, /dev/stdout and /dev/stderr?. Many commands, including paste, will also interpret the filename - to mean STDIN.

When you run bb on its own, both STDOUT and STDERR are the console, where command output usually appears. The lines go through different descriptors (as shown by your annotate-output) but ultimately end up in the same place.

When you add a | and a second command, making a pipeline...

bb | paste /dev/stdin /dev/stderr

the | tells the shell to connect the output of bb to the input of paste. paste first tries to read from /dev/stdin, which (via some symlinks) resolves to its own standard input descriptor (which the shell just connected up) so the line 1 comes through.

But the shell/pipeline does nothing to STDERR. bb still sends that (e1 e2 etc.) to the console. Meanwhile, paste attempts to read from the same console, which hangs (until you type something).

Your link Why can't I read /dev/stdout with a text editor? is still relevant here because those same restrictions apply to /dev/stderr.

How to make a second pipeline

You have a command that produces both standard output and standard error, and you want to paste those two lines next to each other. That means two concurrent pipes, one for each column. The shell pipeline ... | ... provides one of those, and you're going to need to create the second yourself, and redirect STDERR into that using 2>filename.

mkfifo RHS
bb 2>RHS | paste /dev/stdin RHS

If this is for use in a script, you may prefer to make that FIFO in a temporary directory, and remove it after use.

JigglyNaga
  • 7,706
  • 1
  • 21
  • 47
  • Good answer. Please note that I'm specifically interested in `/dev/stdin`. Maybe my `seq 2` example is vague and confusing, since this is the 2nd time somebody's suggested merely *simulating* the desired `paste` output. ... See revised question text for possible disambiguation. – agc Oct 10 '18 at 02:23
  • @agc the revision doesn't make any difference. You still need to send bb's stderr somewhere that `paste` can read from, and `/dev/stderr` is not that place. In practice, you can send bb's stderr to a pipe/FIFO file, and have `paste` read from that in addition to stdin. – muru Oct 10 '18 at 02:48
  • @muru, You are correct and we agree that functionally `bb()` makes no difference and is in that sense unnecessary, but it is hoped that for presentation purposes `bb()` may help some readers focus more on the general problem rather that any superficial aspects. – agc Oct 10 '18 at 03:07
  • @agc You say you're specifically interested in `/dev/stdin`, but that's not the problem here. `bb`'s standard output is connected to `paste`'s standard input, and those lines (the raw numbers) are going through exactly as you intend. The command hangs because of what you're trying to do with `/dev/stderr`. The modified numbers (`e1`, `e2` etc.) go straight to the console, and `paste` hangs while trying to read from its own standard error. `paste /dev/stderr` will have the same effect. – JigglyNaga Oct 10 '18 at 07:37
  • Updated to use the OP's `bb` function. That should make the explanation simpler because there are no other pipelines and redirections to distract. Neither this, nor my earlier revision, were "simulating" the `paste` output; it's using `paste` directly. – JigglyNaga Oct 10 '18 at 10:27
3

annotate-output is able to do that because it's doing something special (namely it redirects the stderr of a command to a fifo), something that paste has absolutely no way of doing -- simply because paste is not running itself the command(s) it gets its input from, and it has no way of redirecting their input or their output.

But you could write a wrapper that use exactly the same trick annotate-output is using:

pasteout(){
  f=$(mktemp -u) || return
  mkfifo -m 600 -- "$f" || return
  "$@" 2>"$f" | paste -- - "$f"
  rm -f -- "$f"
}
pasteout bb

Note however that it's prone to dead-locks. If for instance bb produces more standard output than can fit in the pipe plus the extra amount initially read by paste but doesn't produce any error output, paste will be blocked waiting for input on the fifo and will not empty the pipe bb is feeding its stdout to, causing bb's write()s to the pipe to hang as well.

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
  • This is a nice tidy wrapper function equivalent of [*JigglyNaga's* FIFO-based answer](https://unix.stackexchange.com/a/474308/165517). – agc Oct 11 '18 at 00:58
1

There are a couple of problems with the whole line that we need to analyze, that is:

seq 2 | tee >(sed 's/^/e/' > /dev/stderr) | paste /dev/stdin /dev/stderr

stderr

First, the last command. Only stdout can pass through a pipe:

$ seq2 | paste -
1
2

$ seq2 | paste - -
1 2

There is nothing to read from stderr:

$ seq 2 | paste - /dev/stderr 
1   ^C

You need to ^C it because it blocks, there is nothing to read from stderr.
Even if you create some output to stderr it doesn't travel through a pipe:

$ { seq 2; seq 3 4 >/dev/stderr; } | paste - /dev/stderr
1   3
4

Exactly as before, the 1 gets printed and the paste blocks waiting for stderr.
The other 2 numbers went directly to the console and got (independently) printed.

You could give some input to stderr in the last command of the pipe:

$ { seq 2; seq 3 4 >/dev/stderr; } | paste - /dev/stderr 2</dev/null
1
2
3
4

Which is exactly the same as 2>/dev/null by the way, to avoid blocking the second file descriptor used in the paste command. But the values printed come directly from the seq 3 4 redirected to the console, not from paste. This does the same:

$ { seq 2; seq 3 4 >/dev/tty; } | paste - /dev/stderr 2</dev/null
1   
2   
3
4

And this doesn't block:

$ seq 2 | tee >(sed 's/^/e/' > /dev/stderr) | 
  paste /dev/stdin /dev/stderr 2</dev/null
1   
2   
e1
e2

order

Second, the output of tee doesn't have to be "in order". `tee` and `bash` process substitution order

And, in fact: The output of a process substitution doesn't have to be "in order": The process substitution output is out of the order

$ echo one; echo two > >(cat); echo three;
one
three
two

In fact, on some examples, if you try several times, you could get different orders. non-deterministic output from independent processes run concurrently by process substitution

$ printf '%s\n' {0..1000} | tee >(head -n2) >(sort -grk1,1 | head -n3) >/dev/null
1000
999
998
0
1

So, no, it could not be done with process substitution and paste.
You need to give some order to the execution:

$ seq 2 | { while read a; do printf "%s %s\n" "$a" "e$a" ; done; }
1 e1
2 e2

bb

So, your bb function, which (basically) contains:

| tee >(sed 's/^/e/')

Which could be tested with :

$ printf '%s\n' {0..1000} | tee >(sort -grk1,1 | head -n3 >&2) | head -n 2
0
1
291
290
289

Should print 0, 1, 1000, 999, 998, In that order, but many times it doesn't.
That is: It is intrinsically in-stable.

Stable real Solution.

The only safe solution for bb is to avoid any process substitution.
And, taking advantage that the {…} capture both stdout and stderr, example:

$ bash -c '{ echo test-str >/dev/stderr; }' 2>/dev/null

No output, remove the 2 to confirm.

This will work for bb:

$ bb() { seq 5 | tee /dev/stderr | sed 's/^/e/'; }

And use a fifo for paste:

$ mkfifo out2
$ bb 2>out2  | paste out2 -
1   e1
2   e2
3   e3
4   e4
5   e5

You will need to set a trap to remove the fifo file and test if the fifo file exist before creating it.

Seems to work portably on all shells (compatible with Almquist syntax) I tested. Not fully tested, ask for confirmation from other users, there may be some yet unknown surprises.

  • Wow, on my system that `printf '%s\n' {0..1000} | tee >(sort -grk1,1 | head -n3 >&2) | head -n 2` is practically a random number generator. – agc Oct 11 '18 at 06:29
  • `| tee /dev/stdout |`? ITYM `| tee /dev/stderr |`. – JigglyNaga Oct 11 '18 at 11:29