7

My goal is to call a command, get stderr in a variable, but keep stdout (and only stdout) on the screen. Yep, that's the opposite of what most people do :)

For the moment, the best I have is :

#!/bin/bash
pull=$(sudo ./pull "${TAG}" 2>&1)
pull_code=$?

if [[ ! "${pull_code}" -eq 0 ]]; then
  error "[${pull_code}] ${pull}"
  exit "${E_PULL_FAILED}"
fi

echo "${pull}"

But this can only show the stdout in case of success, and after the command finish. I want to have stdout on live, is this possible ?

EDIT

Thanks to @sebasth, and with the help of Redirect STDERR and STDOUT to different variables without temporary files, I write this :

#!/bin/bash
{
  sudo ./pull "${TAG}" 2> /dev/fd/3
  pull_code=$?
  if [[ ! "${pull_code}" -eq 0 ]]; then
    echo "[${pull_code}] $(cat<&3)"
    exit "${E_PULL_FAILED}"
  fi
} 3<<EOF
EOF

I admit this not really "beautiful", seems tricky, and I don't really understand why the heredoc is needed...

Is this the better way to achieve that ?

Doubidou
  • 173

4 Answers4

12

Just use:

{ err=$(cmd 2>&1 >&3 3>&-); } 3>&1

To get the stderr of cmd while leaving its stdout untouched (here by using fd 3 to bring the original stdout (copied with 3>&1) inside the command substitution (restored with >&3 after we've redirected fd 2 to the pipe created by the command substitution with 2>&1)).

  • 1
    Works perfectly, thanks. I enjoy the use of the word "Just" to introduce this bit of syntax. – MattG Jun 16 '23 at 09:12
  • I think the 3>&1 should instead happen before as exec 3>&1 followed by a exec 3>&- at the end. The reason is that, if no command name results (which is the case here), POSIX leaves the order in which redirections and assignments happen open. See (2.9.1 Simple Commands). – calestyo Feb 05 '24 at 03:32
  • 1
    @calestyo, here we have a compound command (a command group) with redirection. Inside that we have a simple-command with assignment, no command, no redirection. The value of the assignment is a command substitution, inside we have another simple command with a command, redirection and no assignment. The text you're refering to would apply if it was the err=... assignment we were redirecting as in err=$(...) 3>&1, but here it's a command group we're redirecting – Stéphane Chazelas Feb 05 '24 at 06:41
  • 1
    I see, so it's basically just the grouping command, which does the trick?! And since one can always just put a single assignment inside { var=val; }, one never really needs to use exec there for that niche case, but can just directly redirect? – calestyo Feb 06 '24 at 16:08
5

You can use the answer provided by Stéphane Chazelas with minor modifications: only redirect stderr and do not use command substitution to run your command.

{
  command 2> /dev/fd/3
  res=$?
  err=$(cat<&3)
} 3<<EOF
EOF

printf 'stderr: %s\n' "$err"

Output of command will be normally in stdout and stderr is in err variable.

sebasth
  • 14,872
  • Ah, you wrote this while I was editing my questions ^^ - mark a accepted answer, but I'm waiting to see if there is a less complicated way :) – Doubidou Oct 09 '18 at 09:22
  • 1
    That convoluted stuff in the other answer was because we needed a temp file there. We don't need one here as we don't need stdout and stderr in different variables. As noted there, it's shell and OS dependent. – Stéphane Chazelas Oct 09 '18 at 09:44
1

Just exchange stdout and stderr for the command to capture stderr.

pull=$(sudo ./pull "${TAG}" 3>&2 2>&1 1>&3)

And then redirect stderr back to stdout:

{ pull=$(sudo ./pull "${TAG}" 3>&2 2>&1 1>&3; } 2>&1

Explanation:

In the same way as when you capture only stdout of a command:

var=$(cmd)

the output of stderr still goes to the command line. You can swap both channels to capture only the stderr:

var=$(cmd 3>&2 2>&1 1>&3)

And then, redirect the stderr back to stdout (to show the output (if any)).

{ var=$(f 3>&2 2>&1 1>&3); }  2>&1
1

Combining answers from both How to output to screen overriding redirection and How to capture stderr of a bash keyword (e.g. time)?, I get

var=$( (cmd >/dev/tty) 2>&1)

If you're not using a builtin or keyword, that simplifies to

var=$( cmd 2>&1 >/dev/tty )
JigglyNaga
  • 7,886