This question already has an answer here:
-
Capture stdout and stderr into different variables
13 answers
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 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.
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)
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.
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"
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.
# 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")
}