How do I get the output and exit value of a subshell when using "bash -e"?

Solution 1:

$() preserves the exit status; you just have to use it in a statement that has no status of its own, such as an assignment.

output=$(inner)

After this, $? would contain the exit status of inner, and you can use all sorts of checks for it:

output=$(inner) || exit $?
echo $output

Or:

if ! output=$(inner); then
    exit $?
fi
echo $output

Or:

if output=$(inner); then
    echo $output
else
    exit $?
fi

(Note: A bare exit without arguments is equivalent to exit $? – that is, it exits with the last command's exit status. I used the second form only for clarity.)


Also, for the record: source is completely unrelated in this case. You can just define inner() in the outer-scope.sh file, with the same results.

Solution 2:

See BashFAQ/002:

If you want both (output, and exit status):

output=$(command)
status=$? 

A Special Case

Note about a tricky case with function local variables, compare the following code:

f() { local    v=$(echo data; false); echo output:$v, status:$?; }
g() { local v; v=$(echo data; false); echo output:$v, status:$?; }

We'll get:

$ f     # fooled by 'local' with inline initialization
output:data, status:0

$ g     # a good one
output:data, status:1

Why?

When the output of a subshell is used to initialize a local variable, the exit status is no longer of the subshell, but of the local command, which is most likely to be 0.

See also https://stackoverflow.com/a/4421282/537554

Solution 3:

#!/bin/bash
set -e
source inner-scope.sh
foo=$(inner)
echo $foo
echo "I thought I would've died :("

By adding echo, the subshell does not stand alone (is not separately checked) and does not abort. Assignment circumvents this problem.

You can also do this, and redirect the output to a file, to later process it.

tmpfile=$( mktemp )
inner > $tmpfile
cat $tmpfile
rm $tmpfile