46

Consider this snippet:

stop () {
    echo "${1}" 1>&2
    exit 1
}

func () {
    if false; then
        echo "foo"
    else
        stop "something went wrong"
    fi
}

Normally when func is called it will cause the script to terminate, which is the intended behaviour. However, if it's executed in a sub-shell, such as in

result=`func`

it will not exit the script. This means the calling code has to check the exit status of the function every time. Is there a way to avoid this? Is this what set -e is for?

  • What are you trying to accomplish or is this a hypothetical question? – jw013 Sep 18 '12 at 16:37
  • 1
    i want a function "stop" that prints a message to stderr and stops the script, but it doesn't stop when the function that calls stop is executed in a sub-shell, like in the example – Ernest A C Sep 18 '12 at 16:41
  • 2
    Of course, because it exits from the subshell not the current one. Simply call the function directly: func. –  Sep 18 '12 at 16:42
  • 1
    i can't call it directly because it returns a string that has to be stored in a variable – Ernest A C Sep 18 '12 at 16:53
  • 1
    @ErnestAC Please provide all the details in the original question. The above function does not return a string. –  Sep 18 '12 at 17:03
  • 1
    @htor I changed the example – Ernest A C Sep 18 '12 at 17:29
  • If func is being called from within an evaluation like echo "hello $(func)", then this is the only working solution I have found: https://stackoverflow.com/a/9894126/1117929 – Jonathan Cross Mar 14 '22 at 15:48

6 Answers6

46

You could decide that the exit status 77 for instance means exit any level of subshell, and do

set -E
trap '[ "$?" -ne 77 ] || exit 77' ERR

(
  echo here
  (
    echo there
    (
      exit 12 # not 77, exit only this subshell
    )
    echo ici
    exit 77 # exit all subshells
  )
  echo not here
)
echo not here either

set -E in combination with ERR traps is a bit like an improved version of set -e in that it allows you to define your own error handling.

In zsh, ERR traps are inherited automatically, so you don't need set -E, you can also define traps as TRAPERR() functions, and modify them through $functions[TRAPERR], like functions[TRAPERR]="echo was here; $functions[TRAPERR]"

  • 2
    Interesting solution! Clearly more elegant than kill $$. –  Sep 19 '12 at 09:23
  • 4
    One thing to watch out for, this trap won't handle interpolated commands, e.g. echo "$(exit 77)"; the script will carry on as if we'd written echo "" – Warbo Mar 30 '16 at 14:58
  • Interresting! Is there any luck on (quite older) bash that doesn't have -E ? maybe we have to resort to defining a trap on a USER signal and using a kill to that signal? I'll do some reasearch too... – Olivier Dulac Apr 06 '16 at 18:44
  • How to know, when not being in a sub-shell trap, in order to return 1 instead of 77? – ceving Nov 09 '16 at 14:38
13

You could kill the original shell (kill $$) before calling exit, and that'd probably work. But:

  • it seems rather ugly to me
  • it will break if you have a second subshell in there, i.e., use a subshell inside a subshell.

Instead, you could use one of the several ways to pass back a value in the Bash FAQ. Most of them aren't so great, unfortunately. You may just be stuck checking for errors after each function call (-e has a lot of problems). Either that, or switch to Perl.

derobert
  • 109,670
  • 7
    Thanks. I'd rather switch to Python, though. – Ernest A C Sep 18 '12 at 18:35
  • 3
    As I write, it's the year 2019. Telling someone to "switch to Perl" is ridiculous. Sorry to be contentious, but would you tell someone frustrated with 'C' to switch to Cobol, which is IMO equivalent? As Ernest points out, Python is a much better choice. My preference would be Ruby. Either way, anything but Perl. – Graham Nicholls Mar 08 '19 at 11:00
  • 1
    Perl is still fine as a replacement for shell scripting, especially for short scripts, even though Python and Ruby are probably better for many cases. It's a matter of whether you want to learn yet another language. – reinierpost Feb 19 '20 at 20:14
  • 1
    I love python and don't like perl so much, but its important to understand that Perl has a very important historical presence in these kinds of "glue scripts". Perl's one liners are famous. Its probably a better language for that and its installed by default everywhere. I just wouldn't learn it just for this. – polvoazul Jul 21 '20 at 21:33
  • 2
    (I hope everyone reading the answer realizes that of course switching to Python, Ruby, Lua, or even NodeJS would work too; any scripting language which features exceptions. Perl happens to be a language I like, and has a long history in the Unix sysadmin world, as polvoazul points out.) – derobert Jul 21 '20 at 21:49
13

As an alternative to kill $$, you may also try kill 0, it will work in the case of nested subshells (all callers and side process will receive the signal) … but it's still brutal and ugly.

0

Try this ...

stop () {
    echo "${1}" 1>&2
    exit 1
}

func () {
    if $1; then
        echo "foo"
    else
        stop "something went wrong"
    fi
}

echo "shell..."
func $1

echo "subshell..."
result=`func $1`

echo "shell..."
echo "result=$result"

The results I get are ...

# test_exitsubshell true
shell...
foo
subshell...
shell...
result=foo
# test_exitsubshell false
shell...
something went wrong

Notes

  • Parameterized to allow the if test to be true or false (see the 2 runs)
  • When the if test is false, we never reach the subshell.
DocSalvager
  • 2,152
  • This is very similar to the original idea the user was posting about and said didn't work. I don't think this works for the subshell case. Your test using false exits after the "shell" case and never gets to the "subshell" test case. I believe it would fail for that case since the subshell would exit from the "exit 1" call but does not propagate the error to the outer shell. – stuckj Oct 12 '16 at 17:49
0

My example to exit in one liner:

COMAND || ( echo "ERROR – executing COMAND, exiting..." ; exit 77 );[ "$?" -eq 77 ] && exit
Anthon
  • 79,293
vicente
  • 21
  • 1
    This doesn't seem to actually be an answer that would work with the command running in a sub-shells as requested by OP...

    That said while I don't disagree with the down votes given the answer. Down votes without comment or reason are just as unhelpful as bad answers.

    – DVS Jul 17 '19 at 13:31
  • While it seems this wasn't exactly what the OP was searching for, it was exactly what I needed. Thank you! – heilerich Mar 04 '21 at 10:04
0

(Bash specific answer) Bash has no concept of exceptions. However, with set -o errexit (or the equivalent: set -e) at the outermost level, the failed command will result in the subshell exiting with a non-zero exit status. If this is a set of nested subshells without conditionals around the execution of those subshells, it will effectively 'roll up' the entire script and exit.

This can be tricky when trying to include bits of various bash code into a larger script. One chunk of bash may work just fine on its own, but when executed under errexit (or without errexit), behave in unexpected ways.

[192.168.13.16 (f0f5e19e) ~ 22:58:22]# bash -o errexit /tmp/foo
something went wrong
[192.168.13.16 (f0f5e19e) ~ 22:58:31]# bash /tmp/foo
something went wrong
But we got here anyways
[192.168.13.16 (f0f5e19e) ~ 22:58:37]# cat /tmp/foo
#!/bin/bash
stop () {
    echo "${1}"
    exit 1
}

if false; then
    echo "foo"
else
    (
        stop "something went wrong"
    )
    echo "But we got here anyways"
fi
[192.168.13.16 (f0f5e19e) ~ 22:58:40]#