What Sourcing Actually Does
Running source file.sh (or the POSIX-equivalent . file.sh) reads and executes the file's contents inside the current shell process, rather than launching a new subshell the way ./file.sh would. This means every variable, function, and even cd performed inside the sourced file directly affects the calling shell's environment — which is exactly the mechanism that lets a small lib.sh of shared functions become available to any script that sources it, but also why a careless sourced script can silently change the caller's working directory or overwrite its variables.
Cricket analogy: A substitute fielder brought onto the field mid-over directly joins the existing team formation rather than starting a separate match — that's sourcing versus running a script as its own subshell.
Source vs Execute: A Concrete Comparison
./setup.sh forks a new process, runs the script there, and any cd or exported variable inside it disappears when that process exits — the calling shell is unaffected. source setup.sh runs the exact same commands in the calling shell's own process, so a cd /tmp inside it leaves the caller sitting in /tmp afterward, and any exit inside it terminates the caller's shell entirely, not just the sourced script. This is precisely why interactive shell configuration files like .bashrc and .bash_profile must be sourced, not executed — their whole purpose is to modify the current shell's environment.
Cricket analogy: Watching a match highlights reel (execute, isolated) versus actually walking onto the pitch and joining the live match (source, shared state) — one affects only itself, the other affects the real game in progress.
#!/usr/bin/env bash
set -euo pipefail
# lib/strings.sh — designed to be sourced, not executed
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
echo "strings.sh is a library; source it, don't execute it directly." >&2
exit 1
fi
strings::trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}" # trim leading whitespace
s="${s%"${s##*[![:space:]]}"}" # trim trailing whitespace
printf '%s' "$s"
}
# main.sh — the consumer script
# source "$(dirname "${BASH_SOURCE[0]}")/lib/strings.sh"
# trimmed=$(strings::trim " hello world ")
# echo "[$trimmed]"
Guarding Against Double-Sourcing and Unsafe Paths
If two different library files both source a common utils.sh, or a script sources the same file twice through different relative paths, functions get redefined harmlessly but constant readonly declarations will throw an error on the second attempt. Guard against this with an include-once pattern: check and set a uniquely-named flag variable (e.g. [[ -n "${_UTILS_SH_INCLUDED:-}" ]] && return; _UTILS_SH_INCLUDED=1) at the top of the library file. Also always resolve the library's own path relative to ${BASH_SOURCE[0]}, not $0 or a hardcoded path, so it works correctly regardless of which script sources it or from what working directory.
Cricket analogy: A player who has already been declared 'retired out' can't be re-added to the batting order a second time without the scorer flagging a clear rule violation — an include-once guard prevents exactly this kind of duplicate registration.
Resolve a sourced library's own directory reliably with: LIB_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd). Using ${BASH_SOURCE[0]} instead of $0 is essential here — $0 refers to the top-level script that was invoked, not the library file currently being sourced, so it would resolve to the wrong directory.
Never call exit inside a file meant to be sourced. Because sourcing runs in the caller's own shell process, exit terminates that entire shell — including an interactive terminal session if a user accidentally sources a library file directly. Use return for early exits from a sourced file instead.
source file.sh(or. file.sh) runs the file in the current shell process;./file.shruns it in a new subshell.- Only sourcing lets a script's variables, functions, and
cdcalls persist in the calling shell. .bashrc/.bash_profilemust be sourced, not executed, because their purpose is to modify the current shell.- Guard against double-sourcing with an include-once flag variable checked at the top of the file.
- Resolve a library's own path with
${BASH_SOURCE[0]}, never$0or a hardcoded absolute path. - Never call
exitinside a sourced file — usereturnto avoid killing the caller's shell. - A library file can detect direct execution vs sourcing by comparing
${BASH_SOURCE[0]}to$0.
Practice what you learned
1. What is the key difference between `source file.sh` and `./file.sh`?
2. Why must `.bashrc` be sourced rather than executed?
3. What does an 'include-once' guard at the top of a library file typically check?
4. Why should a sourced library use `${BASH_SOURCE[0]}` instead of `$0` to find its own directory?
5. What happens if a sourced file calls `exit 1`?
Was this page helpful?
You May Also Like
Advanced Bash Functions
Go beyond basic function syntax to master argument handling, scoping, namerefs, and composition patterns used in production Bash scripts.
Local vs Global Variables
Understand how Bash variable scope actually works, why globals are the default, and how to use `local` deliberately to write safer scripts.
Function Return Values and Exit Codes
Learn how Bash functions actually communicate results — via numeric exit codes, stdout capture, and namerefs — and how to check them reliably.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics