67

set -e (or a script starting with #!/bin/sh -e) is extremely useful to automatically bomb out if there is a problem. It saves me having to error check every single command that might fail.

How do I get the equivalent of this inside a function?

For example, I have the following script that exits immediately on error with an error exit status:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

The output is as expected:

the following command could fail:

Now I'd like to wrap this into a function:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Expected output:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Actual output:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(ie. the function is ignoring set -e)

This presumably is expected behaviour. My question is: how do I get the effect and usefulness of set -e inside a shell function? I'd like to be able to set something up such that I don't have to individually error check every call, but the script will stop on encountering an error. It should unwind the stack as far as is needed until I do check the result, or exit the script itself if I haven't checked it. This is what set -e does already, except it doesn't nest.

I've found the same question asked outside Stack Overflow but no suitable answer.

imz -- Ivan Zakharyaschev
  • 4,921
  • 6
  • 53
  • 104
Robie Basak
  • 6,492
  • 2
  • 30
  • 34
  • Similar: [Why is bash errexit not behaving as expected in function calls?](https://stackoverflow.com/q/19789102/55075) – kenorb Jan 03 '19 at 15:07
  • You could simulate `set -e` with a DEBUG trap. See my answer below: https://stackoverflow.com/a/62707941/2668666 – jmrah Jul 03 '20 at 03:24
  • 1
    In zsh, one can use `set -o err_return` which does exactly this. – Nahoj Jun 26 '23 at 08:39

11 Answers11

21

I eventually went with this, which apparently works. I tried the export method at first, but then found that I needed to export every global (constant) variable the script uses.

Disable set -e, then run the function call inside a subshell that has set -e enabled. Save the exit status of the subshell in a variable, re-enable set -e, then test the var.

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

You can't run f as part of a pipeline, or as part of a && of || command list (except as the last command in the pipe or list), or as the condition in an if or while, or other contexts that ignore set -e. This code also can't be in any of those contexts, so if you use this in a function, callers have to use the same subshell / save-exit-status trickery. This use of set -e for semantics similar to throwing/catching exceptions is not really suitable for general use, given the limitations and hard-to-read syntax.

trap err_handler_function ERR has the same limitations as set -e, in that it won't fire for errors in contexts where set -e won't exit on failed commands.

You might think the following would work, but it doesn't:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e doesn't take effect inside the subshell because it remembers that it's inside the condition of an if. I thought being a subshell would change that, but only being in a separate file and running a whole separate shell on it would work.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
antak
  • 19,481
  • 9
  • 72
  • 80
  • 2
    Note: This will not work when this is part of a function that is again used with `if`, `||` , `&&` , etc. :( – jomo Aug 24 '15 at 21:47
  • nicer-looking way to write the test: `if ((EXIT)); then echo "f failed"; fi`. Arithmetic contexts are quite nice for doing boolean stuff. – Peter Cordes Nov 14 '15 at 04:48
  • Please check the reusable function [try](https://stackoverflow.com/a/76359353) based on this answer. – Denis Ryzhkov May 29 '23 at 18:49
17

From documentation of set -e:

When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value > 0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.

In your case, false is a part of a pipeline preceded by ! and a part of if. So the solution is to rewrite your code so that it isn't.

In other words, there's nothing special about functions here. Try:

set -e
! { false; echo hi; }
Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
Roman Cheplyaka
  • 37,738
  • 7
  • 72
  • 121
  • There's no pipeline. It's a *compound* command as opposed to a *simple* command. – Dennis Williamson Nov 02 '10 at 01:21
  • 2
    There *is* a pipeline, although a degenerate one. If you check with the standard (particularly "2.10.2 Shell Grammar Rules"), you'll see that a pipeline consists of at least one command (which may be simple, compound of func.def.) preceded by optional bang sign. There's no other way to introduce a negation except pipeline. – Roman Cheplyaka Nov 02 '10 at 06:21
  • Now, *to be part of* is really ambiguous. One might expect (and I did) that this refers to immediate parts. In this interpretation `false` would be a part of the brace group, and the brace group would be a part of the pipeline, but `false` would not be a part of the pipeline. But apparently all implementations I have share another opinion -- where a *part* of a statement may be embedded however deeply in this statement. – Roman Cheplyaka Nov 02 '10 at 06:27
  • s/compound of func.def./compound or func.def./ – Roman Cheplyaka Nov 02 '10 at 06:34
  • Any link to the documentation of `set -e`? – Dmitri Zaitsev Apr 27 '15 at 11:47
  • @DmitriZaitsev [the POSIX standard](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#set) – Roman Cheplyaka Apr 27 '15 at 21:17
  • 8
    This explains behaviour - thanks - but it doesn't really solve my problem. How do I get the _behaviour_ of `set -e`, but localised to a function? I want to write my function with no care for whether individual simple commands succeed or fail, and have the function immediately return non-zero (presumably `$?`) as soon as any one of these simple commands fail. As Gintautas points out in another answer, combining with `&&` will do this, but I want something that means that for convenience I don't have to change the body of the function. – Robie Basak Feb 16 '16 at 20:35
  • 3
    @RobieBasak I appreciate why you're asking that, but your request doesn't really make sense. `set -e` *does* apply to the contents of functions, *unless* the function is invoked in one of the ways excluded from `set -e`, as described in the documentation Roman linked to. `set -e` only applies to certain invocation contexts, so If you want your function to return as soon as something goes wrong *no matter what* appending `|| return` to each command (or chaining everything with `&&`) is the standard solution. – dimo414 Mar 16 '17 at 23:42
  • 5
    Put another way, `set -e` is a fiddly beast, and often not really what you want your script to do even though it seems like a really useful hammer. – dimo414 Mar 16 '17 at 23:44
  • The long, "googlable" name of set -e is `errexit` – MarcH Nov 14 '18 at 17:23
14

You may directly use a subshell as your function definition and set it to exit immediately with set -e. This would limit the scope of set -e to the function subshell only and would later avoid switching between set +e and set -e.

In addition, you can use a variable assignment in the if test and then echo the result in an additional else statement.

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a
philz
  • 165
  • 1
  • 2
  • Interesting. `if ret=$( set -e; f );then` exits `f` early with `dash -e`, but not with `bash -e`. I defined `f() { echo a; false; echo "Should NOT get HERE"; }`, since I wanted to test it with normally-defined functions, not subshell functions. – Peter Cordes Nov 14 '15 at 05:30
  • 2
    I know this is an old answer, but this doesn't work for me in Bash 4.4 and Bash 5. I get the "Should NOT get HERE" output. My output is `++ echo a ++ false ++ echo Should NOT get HERE ++ exit 0 ` Does this still work for you? – jmrah Jul 02 '20 at 12:52
  • didn't work for me either – mhsekhavat Oct 09 '21 at 17:12
  • The technique of defining function f() as a subshell by using `f() (...)` instead of `f() {...}` worked for me, ie the set -e from subshell does not propagate to caller – Oliver May 02 '22 at 18:00
8

This is a bit of a kludge, but you can do:

export -f f
if sh -ec f; then 
...

This will work if your shell supports export -f (bash does).

Note that this will not terminate the script. The echo after the false in f will not execute, nor will the body of the if, but statements after the if will be executed.

If you are using a shell that does not support export -f, you can get the semantics you want by running sh in the function:

f() { sh -ec '
  echo This will execute
  false
  echo This will not
  '
}
William Pursell
  • 204,365
  • 48
  • 270
  • 300
7

Note/Edit: As a commenter pointed out, this answer uses bash, and not sh like the OP used in his question. I missed that detail when I originaly posted an answer. I will leave this answer up anyway since it might be interested to some passerby.

Y'aaaaaaaaaaaaaaaaaaallll ready for this?

Here's a way to do it with leveraging the DEBUG trap, which runs before each command, and sort of makes errors like the whole exception/try/catch idioms from other languages. Take a look. I've made your example one more 'call' deep.

#!/bin/bash

# Get rid of that disgusting set -e.  We don't need it anymore!
# functrace allows RETURN and DEBUG traps to be inherited by each
# subshell and function.  Plus, it doesn't suffer from that horrible
# erasure problem that -e and -E suffer from when the command 
# is used in a conditional expression.
set -o functrace

# A trap to bubble up the error unless our magic command is encountered 
# ('catch=$?' in this case) at which point it stops.  Also don't try to
# bubble the error if were not in a function.
trap '{ 
    code=$?
    if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
        # If were in a function, return, else exit.
        [[ $FUNCNAME ]] && return $code || exit $code
    fi
}' DEBUG

my_function() {
    my_function2
}

my_function2() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

# the || isn't necessary, but the 'catch=$?' is.
my_function || catch=$?
echo "Dealing with the problem with errcode=$catch (⌐■_■)"

echo "run this all the time regardless of the success of my_function"

and the output:

the following command could fail:
Dealing with the problem with errcode=1 (⌐■_■)
run this all the time regardless of the success of my_function

I haven't tested this in the wild, but off the top of my head, there are a bunch of pros:

  1. It's actually not that slow. I've ran the script in a tight loop with and without the functrace option, and times are very close to each other under 10 000 iterations.

  2. You could expand on this DEBUG trap to print a stack trace without doing that whole looping over $FUNCNAME and $BASH_LINENO nonsense. You kinda get it for free (besides actually doing an echo line).

  3. Don't have to worry about that shopt -s inherit_errexit gotcha.

jmrah
  • 5,715
  • 3
  • 30
  • 37
  • Interesting. But I should point out that OP was using bourne shell, not bash. – marcelm Nov 19 '20 at 18:50
  • 1
    You're totally right. I missed that detail :). Thanks for pointing it out. I've edited the answer to reflect that important difference. – jmrah Nov 20 '20 at 09:46
  • np; it's still an interesting solution. Have a +1! – marcelm Nov 20 '20 at 12:58
  • This is almost a perfect solution, except it makes || totally useless, is there any ways to match on || instead of catch? – izissise Jul 27 '22 at 14:56
  • @izissise, not sure what you mean. Why does || become useless? – jmrah Aug 04 '22 at 14:16
  • @jmrah Desired behavior is for one command to return from its function if it errors, but I still want to be able to handle errors when the bubble up. It works with `if` when expecting an error `if ! false; then foo; fi` but doesn't with `||` -> `false || foo`. It will exit current function without executing foo – izissise Aug 05 '22 at 18:53
  • Example https://onlinegdb.com/iWTt3BFdr – izissise Aug 08 '22 at 11:07
  • Yes, I see what you mean now. You're right, and I'm not sure I know of a solution off the top of my head. Perhaps this answer is not quite as useful as I originally thought. Good catch! – jmrah Aug 08 '22 at 13:27
  • Erik and me we used your idea and made a whole scripting framework out of it: bashkit -> https://github.com/Wuageorg/bashkit Many thanks for sharing it! – izissise Jan 27 '23 at 16:08
5

Join all commands in your function with the && operator. It's not too much trouble and will give the result you want.

Gintautas Miliauskas
  • 7,744
  • 4
  • 32
  • 34
  • 1
    Yes, I realise I can do this, but I don't like it. It's messy, too easy to miss one when modifying the function later, and gets complicated when dealing with other shell constructs and with control flow. The whole point of `set -e` is to avoid having to do this. I will accept the answer "there is no such mechanism" though! – Robie Basak Nov 01 '10 at 21:06
  • 1
    set -e has no "whole point" because it's a kludge. I use it all the time because it saved me a lot of time many times but I keep very low expectations about it and so should everyone. – MarcH Nov 14 '18 at 17:32
3

This is by design and POSIX specification. We can read in man bash:

If a compound command or shell function executes in a context where -e is being ignored, none of the commands executed within the compound command or function body will be affected by the -e setting, even if -e is set and a command returns a failure status. If a compound command or shell function sets -e while executing in a context where -e is ignored, that setting will not have any effect until the compound command or the command containing the function call completes.

therefore you should avoid relying on set -e within functions.

Given the following exampleAustin Group:

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

the set -e is ignored within the function, because the function is a command in an AND-OR list other than the last.

The above behaviour is specified and required by POSIX (see: Desired Action):

The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word, a pipeline beginning with the ! reserved word, or any command of an AND-OR list other than the last.

kenorb
  • 155,785
  • 88
  • 678
  • 743
1

I know this isn't what you asked, but you may or may not be aware that the behavior you seek is built into "make". Any part of a "make" process that fails aborts the run. It's a wholly different way of "programming", though, than shell scripting.

jcomeau_ictx
  • 37,688
  • 6
  • 92
  • 107
1

You will need to call your function in a sub shell (inside brackets ()) to achieve this.

I think you want to write your script like this:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Then the output is (as desired):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
lothar
  • 19,853
  • 5
  • 45
  • 59
  • 3
    My experiments show that using a subshell (my_function) call doesn't help here. You have just moved my_function execution out of the if statement and this has helped. I reopened this question here http://stackoverflow.com/questions/5754845/set-e-in-a-function . Please take a look if you know the correct answer. I appreciate it. – agsamek Apr 22 '11 at 11:16
  • 2
    When running the script above I only get this single line of output: `the following command could fail:`. The script seems to quit completely on the `false` command which makes it impossible to implement error handling. Tried it using bash and dash. – Daniel Alder Sep 24 '14 at 11:59
1

If a subshell isn't an option (say you need to do something crazy like set a variable) then you can just check every single command that might fail and deal with it by appending || return $?. This causes the function to return the error code on failure.

It's ugly, but it works

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

gives

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
cobbal
  • 69,903
  • 20
  • 143
  • 156
0

Reusable bash function based on the answer by @antak:

function try {
  # Run the args in a `set -e` mode.
  # Error code should be checked with `((ERR))`.
  # Please don't check it directly with `if try`, `try ||`, etc.
  # Explained: https://github.com/denis-ryzhkov/cheatsheets/tree/main/bash/try

  set +e
  (set -e; "$@")
  declare -gx ERR=$?
  set -e
}

Usage example:

function func {
  echo "func args: $@"
  func-bug
  echo func-never
}

try func arg1 arg2
if ((ERR))
then echo "func failed"
else echo "func succeeded"
fi

Output:

func args: arg1 arg2
./test.sh: line 3: func-bug: command not found
func failed

Details are explained here.

Denis Ryzhkov
  • 2,321
  • 19
  • 12