I know this syntax
var=`myscript.sh`
or
var=$(myscript.sh)
Will capture the result (stdout
) of myscript.sh
into var
. I could redirect stderr
into stdout
if I wanted to capture both. How to save each of them to separate variables?
My use case here is if the return code is nonzero I want to echo stderr
and suppress otherwise. There may be other ways to do this but this approach seems it will work, if it's actually possible.
There's a really ugly way to capture stderr
and stdout
in two separate variables without temporary files (if you like plumbing), using process substitution, source
, and declare
appropriately. I'll call your command banana
. You can mimic such a command with a function:
banana() {
echo "banana to stdout"
echo >&2 "banana to stderr"
}
I'll assume you want standard output of banana
in variable bout
and standard error of banana
in variable berr
. Here's the magic that'll achieve that (Bash≥4 only):
. <({ berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)
So, what's happening here?
Let's start from the innermost term:
bout=$(banana)
This is just the standard way to assign to bout
the standard output of banana
, the standard error being displayed on your terminal.
Then:
{ bout=$(banana); } 2>&1
will still assign to bout
the stdout of banana
, but the stderr of banana
is displayed on terminal via stdout (thanks to the redirection 2>&1
.
Then:
{ bout=$(banana); } 2>&1; declare -p bout >&2
will do as above, but will also display on the terminal (via stderr) the content of bout
with the declare
builtin: this will be reused soon.
Then:
berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr
will assign to berr
the stderr of banana
and display the content of berr
with declare
.
At this point, you'll have on your terminal screen:
declare -- bout="banana to stdout"
declare -- berr="banana to stderr"
with the line
declare -- bout="banana to stdout"
being displayed via stderr.
A final redirection:
{ berr=$({ bout=$(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1
will have the previous displayed via stdout.
Finally, we use a process substitution to source the content of these lines.
You mentioned the return code of the command too. Change banana
to:
banana() {
echo "banana to stdout"
echo >&2 "banana to stderr"
return 42
}
We'll also have the return code of banana
in the variable bret
like so:
. <({ berr=$({ bout=$(banana); bret=$?; } 2>&1; declare -p bout bret >&2); declare -p berr; } 2>&1)
You can do without sourcing and a process substitution by using eval
too (and it works with Bash<4 too):
eval "$({ berr=$({ bout=$(banana); bret=$?; } 2>&1; declare -p bout bret >&2); declare -p berr; } 2>&1)"
And all this is safe, because the only stuff we're source
ing or eval
ing are obtained from declare -p
and will always be properly escaped.
Of course, if you want the output in an array (e.g., with mapfile
, if you're using Bash≥4—otherwise replace mapfile
with a while
–read
loop), the adaptation is straightforward.
For example:
banana() {
printf 'banana to stdout %d\n' {1..10}
echo >&2 'banana to stderr'
return 42
}
. <({ berr=$({ mapfile -t bout < <(banana); } 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)
and with return code:
. <({ berr=$({ mapfile -t bout< <(banana; bret=$?; declare -p bret >&3); } 3>&2 2>&1; declare -p bout >&2); declare -p berr; } 2>&1)