28

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?

  • 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. – user13742 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. – user13742 Sep 18 '12 at 17:03
  • 1
    @htor I changed the example – Ernest A C Sep 18 '12 at 17:29
10

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.

  • 5
    Thanks. I'd rather switch to Python, though. – Ernest A C Sep 18 '12 at 18:35
  • 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 8 at 11:00
36

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]"

  • 1
    Interesting solution! Clearly more elegant than kill $$. – user13742 Sep 19 '12 at 9:23
  • 3
    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 6 '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 9 '16 at 14:38
7

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.

  • 2
    Wouldn't this kill process id 0? – Ernest A C Sep 18 '12 at 18:35
  • 5
    That'll kill the entire process group. You may hit things you don't want (if for example you've started some things in the background). – derobert Sep 18 '12 at 19:11
  • 2
    @ErnestAC see the kill(2) manpage, pids ≤0 have special meaning. – derobert Sep 18 '12 at 19:15
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.
  • 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

(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]#
-2

My example to exit in one liner:

COMAND || ( echo "ERROR – executing COMAND, exiting..." ; exit 77 );[ "$?" -eq 77 ] && exit

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.