107

I was using the "exit 1" statement in my Bash functions to terminate the whole script and it worked fine:

function func()
{
   echo "Goodbye"
   exit 1
}
echo "Function call will abort"
func
echo "This will never be printed"

But then I realized that it doesn't do the work when called like:

res=$(func)

I understand that I created a subshell and "exit 1" aborts that subshell and not the primary one; is there a way to write a function which aborts the whole execution, no matter how it is called?

I just need to get the real return value (echoed by the function).

nyedidikeke
  • 6,899
  • 7
  • 44
  • 59
LiMar
  • 2,822
  • 3
  • 22
  • 28

7 Answers7

121

What you could do, is register the top level shell for the TERM signal to exit, and then send a TERM to the top level shell:

#!/bin/bash
trap "exit 1" TERM
export TOP_PID=$$

function func()
{
   echo "Goodbye"
   kill -s TERM $TOP_PID
}

echo "Function call will abort"
echo $(func)
echo "This will never be printed"

So, your function sends a TERM signal back to the top level shell, which is caught and handled using the provided command, in this case, "exit 1".

FatalError
  • 52,695
  • 14
  • 99
  • 116
  • 1
    I was thinking of the C `setsid()` function, but it doesn't work quite the same way. Updated to not use the `setsid` command, as it would require us to start a new process. – FatalError Mar 27 '12 at 17:09
  • Is it possible to make a general abort() function out of this that exits using the code of the first argument, e.g. `abort 2` would do the `trap "exit 2" TERM` before `kill` ? – Neil C. Obremski Nov 22 '13 at 20:53
  • @NeilC.Obremski: Sure. It's mainly an issue of how to get the value to propagate back to the top level shell. Probably use a command like `tempfile` to select a place to store the code in the top shell, then have the shell that's exiting write the code to that file, then just read in the parent's handler. – FatalError Nov 25 '13 at 14:34
  • 2
    Seems to not work when there are intervening subshells. See alt solution for bash using `set -E` [here](http://unix.stackexchange.com/questions/48533/exit-shell-script-from-a-subshell) – Ron Burk May 10 '15 at 05:18
  • Yes, it does not work like it should as @RonBurk says, if there are some subshells – Igor Chubin Jul 11 '17 at 10:03
  • `TERM`? Don't you mean [`SIGTERM`](https://en.wikipedia.org/wiki/Signal_(IPC)#POSIX_signals)? – Peter Mortensen Feb 28 '21 at 00:41
  • 1
    @PeterMortensen `TERM` works... why would `SIGTERM` be better? – Jonathan Cross Mar 14 '22 at 15:50
56

You can use set -e which exits if a command exits with a non-zero status:

set -e 
func
set +e

Or grab the return value:

(func) || exit $?
Community
  • 1
  • 1
brice
  • 24,329
  • 7
  • 79
  • 95
  • 3
    Interestingly I see a solution. "set -e" in the main script causes any function returning 1 (or exiting with it) to abort the whole execution. Unfortunately "set -e" changes drastically all the behavior and I cannot use it. – LiMar Mar 27 '12 at 16:49
  • 1
    You should use (( )) for numerical comparisons. Inside of [ ] -gt, -eq, etc should be used. – jordanm Mar 27 '12 at 17:04
  • 21
    or even `res=$(func) || exit` – glenn jackman Mar 27 '12 at 17:14
  • @glennjackman gets the cookie, I think. that's much cleaner that the garbage I have! – brice Mar 27 '12 at 17:15
  • 3
    that kind of defies the spirit of the question. if every function call needs to catch the error itself, there is no point in using `exit` in the first place. the function could simply return. / however, if `set -e` is set globally, this does make good sense – phil294 Sep 14 '17 at 22:41
  • What @glennjackman posted won't work if you had `local res=...` instead of `res=...`, just wasted more time than I wish on that... – captainGeech Jul 04 '21 at 08:15
  • @captainGeech, shellcheck.net will warn you about that: https://github.com/koalaman/shellcheck/wiki/SC2155 – glenn jackman Jul 04 '21 at 16:59
  • @glennjackman haven't heard of shellcheck before, that's awesome! thanks for sharing – captainGeech Jul 07 '21 at 01:06
3

I guess better is

#!/bin/bash
set -e
trap "exit 1" ERR

myfunc() {
     set -x # OPTIONAL TO SHOW ERROR
     echo "Exit with failure"
     set +x # OPTIONAL
     exit 1
}
echo "BEFORE..."
myvar="$(myfunc)"
echo "AFTER..But not shown"
2

A child process can't force the parent process to close implicitly. You need to use some kind of signaling mechanism. Options might include a special return value, or perhaps sending some signal with kill, something like

function child() {
    local parent_pid="$1"
    local other="$2"
    ...
    if [[ $failed ]]; then
        kill -QUIT "$parent_pid"
    fi
}
Daenyth
  • 35,856
  • 13
  • 85
  • 124
1

If you just need top be able to bomb out of your scripts from within a function, you can do:

function die () {
    set -e
    /bin/false
}

then elsewhere, within your functions, instead of using "exit", use "die".

John
  • 11
  • 1
0

This works and is "clean" without a lot of extra hoops, make sure the top level function is defined with parenthesis instead of curly braces. This provides 1) immediate exit as well as 2) the exit status is available:

TestExitAllFunctions ()
(
    function FatalError ()
    {
        echo "$1" 1>&2;
        exit 1
    };
    echo "Function call will abort";
    FatalError "Oops, fatal error found, sorry";
    echo "This will never be printed"
)

TestExitAllFunctions
Function call will abort
Oops, fatal error found, sorry

echo $?
1
0

But is there a way to write a function which aborts the whole execution, no matter how it is called?

No.

I just need to get the real return value (echoed by the function).

You can

res=$(func)
echo $?
Luca
  • 4,223
  • 1
  • 21
  • 24