14

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

StoneThrow
  • 5,314
  • 4
  • 44
  • 86
  • 2
    The question isn't "when", but "how". If the script is sourced, you need to use `return`. Whether the script should be sourced or executed in its own process is something that should be documented, and IMO there aren't a lot of use cases that require leaving it up to the user to pick one or the other. – chepner Aug 24 '18 at 22:31
  • But note the in-code comment...it looks like `return` can only be used in functions in bash scripts, not in "non-function" sections of the script...perhaps I'm not getting your point...if you post an answer/example, I might get what you mean... – StoneThrow Aug 24 '18 at 22:33
  • 2
    No, `return` can also be used to return from a script that is being sourced. – chepner Aug 24 '18 at 22:34
  • "IMO there aren't a lot of use cases that require leaving it up to the user to pick one or the other" - this is interesting food for thought; if you're so inclined, I'd be grateful for a more detailed explanation why this is the case. From my naive perspective, I simply perceive that `bash` scripts "can be" either sourced or run in subshells, so it "feels like" scripts ought to account for either possibility. – StoneThrow Aug 24 '18 at 22:40
  • @alvits - Ah: thank you - I think I understand: `return` works in the "non-function" section of a `bash` script` **if** it is sourced. So your solution is kind of "try `return` in case we are being sourced, and if it fails, it must be because we're not being sourced, so `exit`" - do I have that right? You should post as an answer if you'd like the answer points - that sounds like an on-point and useful answer to my question. – StoneThrow Aug 24 '18 at 22:58
  • Yes you understood it well. I'll post my answer. – alvits Aug 24 '18 at 23:00
  • Related: https://stackoverflow.com/questions/2683279/how-to-detect-if-a-script-is-being-sourced – Barmar Aug 24 '18 at 23:02
  • 1
    @StoneThrow, ...in terms of why it's the case -- to be safely sourced, a script needs to be written to not have side effects on the shell that invoked it, and needs to work even if a shell is in a non-default runtime state. That's a lot more than not just calling `exit` -- you need to be robust against different `IFS` values; you need to be robust against functions or aliases overriding commands; you need to avoid modifying global variables the user's other interactive functions might depend on. – Charles Duffy Aug 24 '18 at 23:22
  • 2
    In general, it just doesn't make sense to attempt: Users have no reasonable expectation that they can source scripts that aren't explicitly written and documented for that mode of use. – Charles Duffy Aug 24 '18 at 23:23
  • 1
    ...and even worse/further, sourcing a script doesn't honor its shebang, so you need to be compatible with every interactive shell on the system the user might be using! So you need to be sure your script works not just with bash but with ksh/zsh/csh/etc... – Charles Duffy Aug 24 '18 at 23:25
  • @CharlesDuffy In my work environment, there are a bunch of scripts that define `bash` variables; a frequent workflow is we open a `bash` shell, and source these scripts so that we "get" all these environment variables in our current shell. From this post, this now seems inherently "dangerous" because a script could contain a careless `exit`, as well as all the other dangers you noted, right? So what's a "safe" way to import a bunch of variables that've been specified in a script (i.e. multiple "KEY=VAL" lines) to the current shell? Or is that a case of a "script intended to be sourced"? – StoneThrow Aug 24 '18 at 23:42
  • 1
    @StoneThrow, those scripts should have `.bash` extensions (and no `+x` permissions, so they can *only* be sourced and not executed), marking them as scripts intended to be sourced by bash, whereas normal executables should have no file extension (whether they're implemented as scripts or not). Similarly, a script intended to be sourced by ksh should have a `.ksh` extension, one that's POSIX-compliant should have a `.sh` extension, etc; people who just name all shell scripts with `.sh` extensions make life worse for everyone by making it impossible to know usage mode by looking. – Charles Duffy Aug 25 '18 at 00:50
  • 1
    This is the same convention followed in Python, for example -- Python *modules* have `.py` extensions, but well-behaved Python *scripts* (like those created by the `setuptools` entry_point mechanism) have no extension at all except on platforms that require one. – Charles Duffy Aug 25 '18 at 00:53

1 Answers1

19

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.

alvits
  • 6,550
  • 1
  • 28
  • 28