Search code examples
bashshellexitfail-fast-fail-early

How to safely exit early from a bash script?


I know there are several SO questions on exit vs. return in bash scripts (e.g. here).

On this topic, but different from existing questions, I believe, I'd like to know if there is a "best practice" for how to safely implement "early return" from a bash script such that the user's current shell is not exited if they source the script.

Answers such as this seem based on "exit", but if the script is sourced, i.e. run with a "." (dot space) prefix, the script runs in the current shell's context, in which case exit statements have the effect of exiting the current shell. I assume this is an undesirable result because a script doesn't know if it is being sourced or being run in a subshell - if the former, the user may unexpectedly have his shell disappear. Is there a way/best practice for early-returns to not exit the current shell if the caller sources it?

E.g. This script...

#! /usr/bin/bash
# f.sh

func()
{
  return 42
}

func
retVal=$?
if [ "${retVal}" -ne 0 ]; then
  exit "${retVal}"
#  return ${retVal} # Can't do this; I get a "./f.sh: line 13: return: can only `return' from a function or sourced script"
fi

echo "don't wanna reach here"

...runs without killing my current shell if it is run from a subshell...

> ./f.sh 
> 

...but kills my current shell if it is sourced:

> . ./f.sh 

One idea that comes to mind is to nest code within coditionals so that there is no explicit exit statement, but my C/C++ bias makes think of early-returns as aesthetically preferable to nested code. Are there other solutions that are truly "early return"?


Solution

  • The most common solution to bail out of a script without causing the parent shell to terminate is to try return first. If it fails then exit.

    Your code will look like this:

    #! /usr/bin/bash
    # f.sh
    
    func()
    {
      return 42
    }
    
    func
    retVal=$?
    if [ "${retVal}" -ne 0 ]; then
      return ${retVal} 2>/dev/null # this will attempt to return
      exit "${retVal}" # this will get executed if the above failed.
    fi
    
    echo "don't wanna reach here"
    

    You can also use return ${retVal} 2>/dev/null || exit "${retVal}".

    Hope this helps.