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

81

20

Consider the following code

outer-scope.sh

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

inner-scope.sh

#!/bin/bash
function inner() { echo "winner"; return 1; }

I'm trying to get outer-scope.sh to exit when a call to inner() fails. Since $() invokes a sub-shell, this doesn't happen.

How else do I get the output of a function while preserving the fact that the function may exit with a non-zero exit code?

jabalsad

Posted 2011-12-01T13:22:25.967

Reputation: 1 137

Answers

116

$() 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.

user1686

Posted 2011-12-01T13:22:25.967

Reputation: 283 655

1Hmm, when I type if ! $(exit 1) ; then echo $?; fi, I get 0. Not sure if is the way to go if you need to preserve that exit value. – Ron Burk – 2017-05-04T02:08:46.373

@grawity - Does this work with Dash? I'm trying to work around some annoying Autoconf behavior (namely, Autoconf reporting success when Sun or IBM's compiler prints illegal option to the terminal). – jww – 2017-11-08T20:33:25.510

@jww It should be standard POSIX sh behavior. Are you sure the compiler actually returns a non-0 status when that happens? – user1686 – 2017-11-08T20:57:03.463

@grawity - Thanks. I'm not sure what the compiler is returning. Then again, Autoconf never states what the criteria for success is when using AC_COMPILE_IFELSE. It may be as weak as an executable is produced. I'm trying to gather that information now at More robust AC_COMPILE_IFELSE feature testing?

– jww – 2017-11-08T21:01:48.157

3if ! output=$(inner); then exit $?; fi will exit with a return code of 0 because $? will give the return code of ! instead of the return code of inner. You could get the desired behavior with if output=$(inner); then : ; else exit $?; fi but that's obviously more verbose – SJL – 2018-04-06T14:56:19.257

Why is it that, even though $? contains the exit status of $() that the script does not exit automatically (given that -e is set)? EDIT: nevermind, I think you have answered my questions, thanks! – jabalsad – 2011-12-02T16:12:27.877

I'm not sure. (I haven't tested any of the above.) But there are some restrictions on -e, all explained in bash's manpage; also, if you are asking about echo $(), then it might be because the subshells' exit codes are ignored when the line - the echo command - has an exit code (usually 0) of its own. – user1686 – 2011-12-02T18:10:55.670

31

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

ryenus

Posted 2011-12-01T13:22:25.967

Reputation: 679

4While this didn't really answer the question, this came in useful to me today, so +1. – fourpastmidnight – 2016-07-27T01:32:48.903

2The exit status for bash commands is always that of the last command executed. When we spend so much time in strongly typed languages it's easy to forget that "local" isn't a type specifier but just another command. Thanks for re-iterating this point here, helped me today. – markeissler – 2016-09-17T01:11:28.863

2Wow, I ran into this exact issue just now and you cleared it up. Thanks! – krb686 – 2017-01-07T02:50:52.453

4

#!/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

Daniel Beck

Posted 2011-12-01T13:22:25.967

Reputation: 98 421

Of course, the $tmpfile continues to exist in the second variant... – Daniel Beck – 2011-12-01T13:43:30.970