How to Make a Shell Script Safe to Source and to Execute
Make one shell script work when run and when sourced: detect which, avoid leaking variables, never let exit kill the parent shell, and use a main function.

The same file is useful in two ways: as a command you run, and as a library of functions you load with source. Supporting both is a small pattern, but getting it wrong either kills the user's terminal or runs the script's main logic at the moment someone only wanted its functions. The sourcing rules come from variable scope, and the examples assume Bash 5.2.
Detect the mode
#!/usr/bin/env bash
greet() {
printf 'hello, %s\n' "${1:-world}"
}
main() {
greet "$@"
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
main "$@"
fi$ ./greet.sh Ada
hello, Ada
$ source ./greet.sh
$ greet Ada
hello, Ada
When the file is executed, $0 is the script path, which equals BASH_SOURCE[0]. When it is sourced, $0 is the calling shell's name and the two differ, so main does not run. This is the Bash counterpart of Python's if __name__ == "__main__" guard.
Do not exit a sourced file
require_root() {
if [[ $EUID -ne 0 ]]; then
echo "run as root" >&2
return 1 2>/dev/null || exit 1
fi
}Inside a function, return leaves the function, which is right for a library. At the top level of a sourced file, return stops the file and hands control back, while exit ends the user's whole shell session. The return ... || exit pair works in both modes: return fails with an error when the code is not in a function or sourced file, and the error text is hidden by the redirect.
Keep the caller's shell clean
# lib.sh: sourced
_lib_name="lib"
lib_hello() {
local who=${1:-you}
echo "${_lib_name}: hi $who"
}Every top-level assignment in a sourced file lands in the caller. Namespacing with a prefix keeps names from colliding, and local inside functions keeps temporaries out. Avoid set -e, set -u and shopt changes at file level, because they stay on in the interactive shell afterwards and cause surprises such as the terminal closing on the next typo. Set strict mode inside main instead, or in a subshell.
Use cases
This pattern fits environment setup files such as a project's activate script, which must be sourced because a child process cannot change the parent's directory or variables. It also fits a utility file that is executed for convenience and sourced by tests; the testing of Bash functions post uses it to load functions without running them. If a script must be sourced, say so in the first comment and fail politely when it is executed, with the same comparison reversed.
What sourcing actually does
Running a script starts a new process. That process gets a copy of the environment, runs, and exits, and nothing it changes comes back to the parent. Sourcing reads the file's commands into the current shell and runs them there, so every variable, function, alias and directory change stays. This is the entire reason sourcing exists: a script that sets PATH or activates a virtual environment can only do it by running in the shell that will use the result.
The same fact explains the hazards. A sourced file shares the caller's namespace, so a variable named i or result overwrites the caller's, a cd moves the user's prompt, and a failure with exit closes the terminal. Library authors need to assume the worst: the caller may have strict mode on or off, may have different options set and may already use any short name. Prefixing global names, declaring locals, and avoiding changes to shell options are the minimum courtesy.
Finding files relative to the script
A script that sources its neighbours needs to find them regardless of the directory the user is in. Relying on the current directory breaks as soon as the script is called from elsewhere. In Bash, BASH_SOURCE[0] gives the path of the file being executed or sourced, and dirname on it gives its directory. Resolve it with cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd in a subshell so the caller's directory is left alone. Symlinked scripts need readlink -f or realpath to find the true location, and macOS needs a newer version of readlink or a fallback for it.
Check that the file exists before sourcing it. Sourcing a missing path prints an error and continues in a non-strict shell, so the following calls fail with "command not found" and send the reader to the wrong place. A guard such as [[ -r $lib ]] || { echo "missing $lib" >&2; return 1; } puts the error where the cause is.
Testing both modes
A dual-mode file has two behaviours, and each deserves a check. For executed mode, run the script with typical arguments and assert on the output and exit status. For sourced mode, start a clean subshell with bash -c 'source ./greet.sh; greet Ada', which proves that sourcing produces no output of its own and that the functions exist afterwards. The second test catches the most common regression, a stray echo or cd added at the top level that fires on every source and goes unnoticed because the author always runs the file directly.
Also test with strict options on in the caller. Start a shell with set -euo pipefail, source the library and call its functions. If the library references an unset variable or returns a nonzero status on a normal path, the strict caller will exit, and the failure will show up in a test rather than in somebody's terminal.
Naming and documenting the interface
Treat a sourced file like a small module. Put a comment block at the top that lists the functions it defines, the variables it reads and any it sets, and the shell it needs. Use a consistent prefix for public functions and a leading _ for private ones, and do not rely on callers staying away from the private names. Version the interface when other scripts depend on it: renaming a function is a breaking change for everyone who sources the file, and a short changelog entry saves them an afternoon of debugging.
Finally, keep the amount of work done at source time small. A library that runs a network call or scans the disk while being sourced slows every shell that loads it, and a profile file that sources ten such libraries makes each new terminal wait. Define functions at load time and do the work only when a function is called; if setup is unavoidable, make it lazy and run it on first use.
Where this does not apply
POSIX sh has no BASH_SOURCE. A portable check is awkward and shell-specific, so write the library as sourced-only and the program as a separate file. Zsh uses ${(%):-%x} for the file name. The Bash variables reference documents BASH_SOURCE, and the builtins section covers return and source.
A last tip: when the file is meant to be sourced only, check at the top with [[ ${BASH_SOURCE[0]} != "$0" ]] || { echo "source this file" >&2; exit 1; }. Users learn the right usage from the message instead of from a confusing missing-variable error three commands later. Also see resolving a script's own directory for locating files beside a sourced script.


