-2

I want to run a command. I want to continue to pipe stdout and stderr to stdout/stderr of the console, but I also want to capture stderr in a variable.

To do this with stdout is trivial stdout=$(command | tee), but I can't see any way to do this for stderr.

Yair Halberstadt
  • 5,733
  • 28
  • 60
  • `I want to continue stdout and stderr to stdout/stderr of the console` I do not understand this. You want to output stdout and store stderr in a variable? – KamilCuk Mar 21 '23 at 14:47
  • @KamilCuk as in I want stdout and stderr to continue being piped to the console, as if nothing were redirected/piped to something else. – Yair Halberstadt Mar 21 '23 at 14:49
  • The `tee` in your example doesn't actually work to make a copy on the terminal; it writes only to stdout, and that content gets captured instead of written to the terminal. I assume you meant to write something like `tee /dev/tty` or `tee /dev/fd/2` or so forth. – Charles Duffy Mar 21 '23 at 14:50
  • @YairHalberstadt, note that when you handle stdout and stderr separately like this, you lose ordering guarantees between them (so the exact interleaving of ordering between lines written to stdout and stderr is lost); that's completely unavoidable while using standardized interfaces (the ways to evade it are OS-dependent and error-prone; I've written [an example elsewhere on this site](https://stackoverflow.com/questions/45760692/separately-redirecting-and-recombining-stderr-stdout-without-losing-ordering)). – Charles Duffy Mar 21 '23 at 15:00
  • BTW, using `yourcommand` instead of `command` in my answer is deliberate: `command` is the name of [a specific POSIX-standardized built-in command](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html#tag_20_22), so it's not a good choice to use as a stand-in in examples. – Charles Duffy Mar 21 '23 at 15:01
  • @CharlesDuffy thanks for the extensive answer! – Yair Halberstadt Mar 21 '23 at 15:04

1 Answers1

1

Just swap stdout and stderr before using the same practice you already know.

stderr=$(yourcommand 3>&1 >&2 2>&3 3>&- | tee /dev/fd/2)

Breaking that down:

  • 3>&1 makes a copy of original-stdout on file descriptor 3, which was previously empty.
  • 1>&2 makes a copy of original-stderr on file descriptor 1 (new-stdout).
  • 2>&3 makes a copy of original-stdout from file descriptor 3 (original-stdout) to file descriptor 2 (new-stderr).
  • 3>&- closes the new file descriptor 3 we created in the first step.

...and then tee /dev/fd/2 makes a copy of new-stdout (original-stderr) on original-stderr so there's a copy of content on the terminal, uncaptured.


Thus, these four redirections in order have the effect of swapping stdout and stderr for the duration of the capture, and logging a copy of what was originally stdout for human consumption via stderr.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441