66

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.

codeforester
  • 39,467
  • 16
  • 112
  • 140
djechlin
  • 59,258
  • 35
  • 162
  • 290
  • ah , there is no way to capture both without temp file, see my answer here which show how to get stderr and pass stdout to the screen (in case of dialog): http://stackoverflow.com/a/13427218/815386 – zb' Dec 10 '12 at 18:16
  • here is additional info http://mywiki.wooledge.org/BashFAQ/002 `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.` – zb' Dec 10 '12 at 18:17
  • is there some specific reason why you don't want to use temp files? Using temp files is very much idiomatic within a bash programming environment – frankc Dec 10 '12 at 18:54
  • Related (and having a pretty easy solution): [Bash script - store stderr in variable](http://stackoverflow.com/q/3130375/2533433) – Izzy Nov 16 '14 at 20:50
  • @eicto Yes, there is a way, read [here](http://stackoverflow.com/a/28796214/2350426). –  Mar 01 '15 at 23:12

6 Answers6

65

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 sourceing or evaling 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 whileread 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)
gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
  • 6
    Awesome answer — this is so powerful! – Adrian Günter Aug 26 '15 at 00:35
  • @gniourf_gniourf I am able to use your `eval` example and replicate the same `banana` output. Next, I tried to pass in a command via variable and have it working with `ls "foo"` but am having issues with `ls "foo bar"` -- note the space in the quoted string. In the latter case, I am seeing the following error captured in berr: `ls: cannot access "foo: No such file or directory ls: cannot access bar": No such file or directory` This leads me to believe the quoting around the file path is not sufficient to escape the space. Can you think of a solution for this? BTW setting `IFS=''` did not work. – John Mark Mitchell Apr 13 '16 at 19:53
  • @gniourf_gniourf The article [Bash: Preserving Whitespace Using set and eval](http://www.linuxjournal.com/content/bash-preserving-whitespace-using-set-and-eval) has me wondering if whitespace preservation with eval is the root issue. I am not certain I understand the issue well enough at this point to determine how to resolve it yet. – John Mark Mitchell Apr 13 '16 at 20:36
  • @gniourf_gniourf My sensibilities say to stay away from `eval` if at all possible but the `. <({...` example seemed to not work so I was trying to make the `eval` version work for my present needs. A bit more background: I am running this a computer that has bash version 3.1.21. – John Mark Mitchell Apr 13 '16 at 20:52
  • @JohnMarkMitchell: it works fine for me (even on 3.1): `eval "$({ berr=$({ bout=$(ls "foo bar"); bret=$?; } 2>&1; declare -p bout bret >&2); declare -p berr; } 2>&1)"; declare -p bout berr`. I get: `declare -- berr="ls: cannot access foo bar: No such file or directory"` as wanted. – gniourf_gniourf Apr 14 '16 at 08:05
  • @gniourf_gniourf Try setting a variable e.g. `command='ls "foo bar"'` then pass that to your masterful one liner as variable. That is the scenario that the problem happens. – John Mark Mitchell Apr 14 '16 at 10:56
  • 1
    @JohnMarkMitchell You're using an antipattern here! [Don't put commands into variables!](http://mywiki.wooledge.org/BashFAQ/050). – gniourf_gniourf Apr 14 '16 at 20:41
  • @gniourf_gniourf I appreciate your patience here. I have learned much from you and your informative remarks. Can you test the dot operator version of your script and see if you are getting the full return results from the %d {1..10} version of banana? I am only seeing "banana to stdout 1" or was that a known limitation? Is there a way around this? – John Mark Mitchell Apr 19 '16 at 21:37
  • @JohnMarkMitchell: In the last example I use `mapfile` to get the output lines in an _array_. To get the array `bout` printed on scren, don't use `echo "$bout"` (this only prints the first field of the array). I usually use `declare` to inspect the content of a variable: `declare -p bout` should show the correct content. – gniourf_gniourf Apr 20 '16 at 05:08
  • @gniourf_gniourf: I greatly appreciate all your help. As it turns out, some of the troubles I was having sourced from one of the target systems I was needed to run my script on having a version of `bash` < 4. This means the `mapfile` command had not been introduced yet and the `source` (`.`) version of your solution fails silently. As a result, I am back to using the `eval` version. I share this in hopes of sparing others the same troubles if they know their script might be run where the `bash` version is < 4. Gniourf, you might want to add the `bash` version dependency on your solution above. – John Mark Mitchell Apr 23 '16 at 14:22
  • 2
    so you basically generate source code dynamically! [ingenious](https://gist.github.com/AquariusPower/ee2b7fc44e674296a324)! thx vm! – Aquarius Power Oct 17 '16 at 03:36
  • @gniourf_gniourf I munched your answer - somewhat - into an easy to use [bash recipe](http://stackoverflow.com/a/41069638/490291) – Tino Dec 09 '16 at 22:19
47

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.

Neil
  • 24,551
  • 15
  • 60
  • 81
zb'
  • 8,071
  • 4
  • 41
  • 68
  • Thanks - accepting this answer because using file descriptors 3 and above avoids using "temp files" per what I intended to mean by temp files (even if technically, literally, a temp file). – djechlin Dec 11 '12 at 21:25
  • 1
    Thank you. Based on this answer I'm using fd 3 to send extra information between two scripts, and capturing it from the calling script using `{ output=$(command 3>&1 1>&4-) ;} 4>&1` – Enrico Mar 20 '16 at 22:06
  • 3
    I object! There is a way to capture both, stdout and stderr. [You can capture stdout into one and stderr into another variable with only FD redirections. No need to use some temporary file or named pipe](https://stackoverflow.com/a/41069638/490291). The only downside is that it's a bit ugly, though. – Tino Jun 13 '19 at 09:55
21

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.

anubhava
  • 761,203
  • 64
  • 569
  • 643
8

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"
jofel
  • 3,297
  • 17
  • 31
7

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.

jack
  • 71
  • 1
  • 1
-1
# 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")
}
  • 3
    This is wrong: the assignment `result=$(command)` is run in a subshell: everything inside the parentheses in `error=$( ... )` is in a subshell; hence `result` will never be seen. You can try it yourself: `c() { echo >&2 'to stderr'; echo 'to stdout'; }; error=$( { result=$(c); } 2>&1); echo "result: $result"; echo "error: $error"`. You'll see that `result` is empty. See my answer for a method that actually works. – gniourf_gniourf Feb 15 '15 at 10:21