111

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?

slhck
  • 228,104
jabalsad
  • 1,477

3 Answers3

156

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

u1686_grawity
  • 452,512
  • 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 Dec 02 '11 at 16:12
  • 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. – u1686_grawity Dec 02 '11 at 18:10
  • 2
    Hmm, 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 May 04 '17 at 02:08
  • @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 Nov 08 '17 at 20:33
  • @jww It should be standard POSIX sh behavior. Are you sure the compiler actually returns a non-0 status when that happens? – u1686_grawity Nov 08 '17 at 20:57
  • @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 Nov 08 '17 at 21:01
  • 6
    if ! 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 Apr 06 '18 at 14:56
  • 5
    I unthinkingly assumed that since foo=$(...) was safe local foo=$(...) would be safe but it is not. Took me a long time before I even considered local might be the issue. Edit: I see that now mentioned in ryenus' answer – user1169420 Oct 10 '21 at 20:47
50

According to the manual of the set builtin, the shell does not exit if:

  1. the command that fails is part of the command list immediately following a while or until keyword,
  2. part of the test in an if statement,
  3. part of any command executed in a && or || list except the command following the final && or ||,
  4. any command in a pipeline but the last,
  5. or if the command’s return status is being inverted with !.

Any of the above would work, except the first since you're not running a loop. Though for readability it's probably better to use an if statement:

set -e

if x=$(echo a; false); then echo "subshell worked" else echo "subshell failed" fi

echo "x: $x"

Or you can invert the result:

bash -c 'set -e; ! x=$(echo a; false); echo "[$?]x:$x"'   # [0]x:a
bash -c 'set -e; ! y=$(echo b; true ); echo "[$?]y:$y"'   # [1]y:b

But beware that the result is now inverted. Fortunately bash doesn't exit even though a successful subshell is inverted.

Interestingly the following seems also work, by grouping the subshell invocation in a list, then invert the whole list, not just the subshell invocation:

bash -c 'set -e; ! { x=$(echo a; false); echo "[$?]x:$x"; }'  # [1]x:a

This somehow matches the 3rd case above, which is also a list, except it's not using && or ||.


A Special Case with Inline Initialization

Note about a tricky case with function local variables, compare the two almost identical functions below:

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

In both functions, the subshell ends with a false command which causes it to fail, however, when executed, we'll get:

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

$ g # good one, with separated declaration and initialization output:data, status:1

Why?

In bash, local is actually a builtin command. When the output of a subshell is used to initialize a local variable, the exit status is no longer the one of the subshell, but that of the local command, which is 0 as long as the local variable gets declared.

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

ryenus
  • 929
  • 1
  • 9
  • 15
  • 7
    While this didn't really answer the question, this came in useful to me today, so +1. – fourpastmidnight Jul 27 '16 at 01:32
  • 4
    The 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 Sep 17 '16 at 01:11
  • 4
    Wow, I ran into this exact issue just now and you cleared it up. Thanks! – krb686 Jan 07 '17 at 02:50
  • 1
    Just got bitten by the "local" special case. Figured it out on my own before finding this answer, but +1 for pointing this out, hopefully others find this before a lengthy debugging session. – flotzilla Feb 11 '22 at 11:23
  • Bitten-by-Bash-local-case-counter: +1 – Genzer Sep 22 '22 at 05:14
5
#!/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
  • 110,419