Capture both stdout and stderr in Bash
BashStreamPipeStdoutStderrBash Problem Overview
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.
Bash Solutions
Solution 1 - Bash
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)
Solution 2 - Bash
There is no way to capture both without temp file.
You can capture stderr to variable and pass stdout to user screen (sample from here):
exec 3>&1 # Save the place that stdout (1) points to.
output=$(command 2>&1 1>&3) # Run command. stderr is captured.
exec 3>&- # Close FD #3.
# Or this alternative, which captures stderr, letting stdout through:
{ output=$(command 2>&1 1>&3-) ;} 3>&1
But there is no way to capture both stdout and stderr:
> What you cannot do is capture stdout in one variable, and stderr in another, using only FD redirections. You must use a temporary file (or a named pipe) to achieve that one.
Solution 3 - Bash
You can do:
OUT=$(myscript.sh 2> errFile)
ERR=$(<errFile)
Now $OUT
will have standard output of your script and $ERR
has error output of your script.
Solution 4 - Bash
An easy, but not elegant way: Redirect stderr to a temporary file and then read it back:
TMP=$(mktemp)
var=$(myscript.sh 2> "$TMP")
err=$(cat "$TMP")
rm "$TMP"
Solution 5 - Bash
While I have not found a way to capture stderr and stdout to separate variables in bash, I send both to the same variable with…
result=$( { grep "JUNK" ./junk.txt; } 2>&1 )
… then I check the exit status “$?”, and act appropriately on the data in $result.
Solution 6 - Bash
# NAME
# capture - capture the stdout and stderr output of a command
# SYNOPSIS
# capture <result> <error> <command>
# DESCRIPTION
# This shell function captures the stdout and stderr output of <command> in
# the shell variables <result> and <error>.
# ARGUMENTS
# <result> - the name of the shell variable to capture stdout
# <error> - the name of the shell variable to capture stderr
# <command> - the command to execute
# ENVIRONMENT
# The following variables are mdified in the caller's context:
# - <result>
# - <error>
# RESULT
# Retuns the exit code of <command>.
# SOURCE
capture ()
{
# Name of shell variable to capture the stdout of command.
result=$1
shift
# Name of shell variable to capture the stderr of command.
error=$1
shift
# Local AWK program to extract the error, the result, and the exit code
# parts of the captured output of command.
local evaloutput='
{
output [NR] = $0
}
END \
{
firstresultline = NR - output [NR - 1] - 1
if (Var == "error") \
{
for (i = 1; i < firstresultline; ++ i)
{
printf ("%s\n", output [i])
}
}
else if (Var == "result") \
{
for (i = firstresultline; i < NR - 1; ++ i)
{
printf ("%s\n", output [i])
}
}
else \
{
printf ("%d", output [NR])
}
}'
# Capture the stderr and stdout output of command, as well as its exit code.
local output="$(
{
local stdout
stdout="$($*)"
local exitcode=$?
printf "\n%s\n%d\n%d\n" \
"$stdout" "$(echo "$stdout" | wc -l)" "$exitcode"
} 2>&1)"
# extract the stderr, the stdout, and the exit code parts of the captured
# output of command.
printf -v $error "%s" \
"$(echo "$output" | gawk -v Var="error" "$evaloutput")"
printf -v $result "%s" \
"$(echo "$output" | gawk -v Var="result" "$evaloutput")"
return $(echo "$output" | gawk "$evaloutput")
}