OPPenguin
OPPenguin

Reputation: 318

Bash: graceful function death on error

I'm trying to find a way to emulate the behavior of set -e in a function, but only within the scope of that function.

Basically, I want a function where if any simple command would trigger set -e it returns 1 up one level. The goal is to isolate sets of risky jobs into functions so that I can gracefully handle them.

Upvotes: 1

Views: 838

Answers (4)

OPPenguin
OPPenguin

Reputation: 318

Doing more research, I found a solution I rather like in Google's Shell Style Guide. There are some seriously interesting suggestions here, but I think I'm going to go with this for readability:

if ! mv "${file_list}" "${dest_dir}/" ; then
  echo "Unable to move ${file_list} to ${dest_dir}" >&2
  exit "${E_BAD_MOVE}"
fi

Upvotes: 0

codeforester
codeforester

Reputation: 42999

Seems like you are looking for "nested exceptions" somewhat like what Java gives. For your requirement of scoping it, how about doing a set -e at the beginning of the function and making sure to run set +e before returning from it?

Another idea, which is not efficient or convenient, is to call your function in a subshell:

# some code

(set -e; my_function)
if [[ $? -ne 0 ]]; then
  # the function didn't succeed...
fi

# more code

In any case, please be aware that set -e is not the greatest way to handle errors in a shell script. There are way too many issues making it quite unreliable. See these related posts:

The approach I take for large scripts that need to exist for a long time in a production environment is:

  • create a library of functions to do all the standard stuff
  • the library will have a wrapper around each standard action (say, mv, cp, mkdir, ln, rm, etc.) that would validate the arguments carefully and also handle exceptions
  • upon exception, the wrapper exits with a clear error message
  • the exit itself could be a library function, somewhat like this:

--

# library of common functions

trap '_error_handler' ERR
trap '_exit_handler'  EXIT
trap '_int_handler'   SIGINT

_error_handler() {
  # appropriate code
}
# other handlers go here...
#

exit_if_error() {
  error_code=${1:-0}
  error_message=${2:-"Uknown error"}

  [[ $error_code == 0 ]] && return 0  # it is all good

  # this can be enhanced to print out the "stack trace"
  >&2 printf "%s\n" $error_message

  # out of here
  my_exit $error_code
}

my_exit() {
  exit_code=${1:-0}
  _global_graceful_exit=1  # this can be checked by the "EXIT" trap handler
  exit $exit_code
}

# simple wrapper for cp
my_cp() {
  # add code to check arguments more effectively
  cp $1 $2
  exit_if_error $? "cp of '$1' to '$2' failed"
}

# main code

source /path/to/library.sh

...
my_cp file1 file2
# clutter-free code

This, along with effective use of trap to take action on ERR and EXIT events, would be a good way to write reliable shell scripts.

Upvotes: 0

Fred
Fred

Reputation: 6995

If you want any failing command to return 1, you can achieve that by following each command with || return 1.

For instance:

false || return 1  # This will always return 1

I am a big fan of never letting any command fail without explicit handling. For my scripts, I am using an exception handling technique where I return errors in a way that is not return codes, and trap all errors (with bash traps). Any command with a non-zero return code automatically means an improperly handled situation or bug, and I prefer my scripts to fail as soon as such a situation occurs.

Upvotes: 3

William Pursell
William Pursell

Reputation: 212248

Caution: I highly advise against using this technique. If you run the function in a subshell environment, you almost get the behavior you desire. Consider:

#!/bin/bash

foo() (  # Use parens to get a sub-shell
        set -e  # Does not impact the main script
        echo This is executed
        false
        echo This should *not* be executed
)

foo  # Function call fails, returns 1
echo return: $?

# BUT: this is a good reason to avoid this technique
if foo; then  # Set -e is invalid in the function
        echo Foo returned 0!!
else
        echo fail
fi
false    # Demonstrates that set -e is not set for the script
echo ok

Upvotes: 0

Related Questions