skip to content

In a bash project, two helper files each `source common.sh`, so common.sh is loaded twice in the same shell. What can go wrong, and what does an include guard look like in bash?

level: middleimportance: nice to knowfreq 24%

answer

  1. source is not import
  2. the file runs again, in full
  3. redefining is fine, accumulating is not
  4. a sentinel checked before anything else
  5. return zero, never exit

basics

~20 s

Bash has no import deduplication: every source re-executes the whole file. Re-running it can abort a strict script when readonly variables are reassigned, duplicate PATH entries, reset counters, replace traps and repeat expensive setup. A guard variable checked at the top with an early return prevents it.

solid answer

~40 s

`source` is not `import` — bash keeps no record of what has been loaded, so the second `source common.sh` runs every line again. Pure function definitions are harmlessly redefined, but anything with a side effect is not: `readonly VERSION=1.2` fails the second time with "readonly variable" and, under `set -e`, kills the script; `PATH="$PATH:/opt/bin"` grows the path each time; accumulating arrays double; a `trap` is re-installed; slow setup work runs twice. The fix is a guard at the top of the library: `if [[ -n "${_COMMON_SH_LOADED:-}" ]]; then return 0; fi` followed by setting that variable. The `:-` keeps it safe under `set -u`, and `return` is legal because the file is being sourced. The deeper fix is making libraries side-effect free so double loading cannot matter.

code

bash · 8 lines
bash
# common.sh — safe to source more than once
if [[ -n "${_COMMON_SH_LOADED:-}" ]]; then
  return 0
fi
_COMMON_SH_LOADED=1

readonly COMMON_VERSION=1.2
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }

go deeper

for a junior

Know that source runs the file again every single time — bash has no import cache — so a shared helper can be loaded twice in one shell and its top-level commands will run twice.

for a middle

Name a concrete breakage such as a readonly reassignment aborting a strict-mode script or a duplicated PATH entry, and write the guard correctly: :- for set -u safety and return 0 rather than exit.

for a senior

Argue for side-effect-free libraries as the real fix, with one-time setup moved into an init function the entry point calls, and note that a guard is per shell and that load-time traps silently replace the caller's handlers.

for a principal

Own the convention for a shared shell library set: what a file may do at load time, how initialisation is ordered across libraries, and when the dependency graph is a signal that the tooling has outgrown shell entirely.

## Bash has no module system Languages with imports remember what they have loaded and skip the second request. `source` has no such memory. Each call opens the file and executes it start to finish, and bash neither notices nor cares that it just did. In any project with more than a couple of shared files this happens quickly: `lib/log.sh` and `lib/aws.sh` both source `lib/common.sh`, the entry point sources both, and `common.sh` runs twice. ## What survives a second load and what does not Redefining a function is harmless — the new definition simply replaces the old one, byte-identical in this case. Variable assignment is harmless too, unless the assignment is *derived from the current value*. The damage lives in side effects: - **`readonly` / `declare -r`.** `readonly VERSION=1.2` on the second pass prints `VERSION: readonly variable` and returns non-zero — even with the same value. In a script under `set -e`, that non-zero status aborts the run, and the failure message points at the library rather than at the double include. - **Accumulating assignments.** `PATH="/opt/tools:$PATH"` or `LD_LIBRARY_PATH` grows a duplicate entry each load. Rarely fatal, always confusing when someone debugs which binary is being found. - **Arrays and counters.** `SERVERS+=(web1 web2)` doubles the list. A counter reset to zero at load time loses whatever the caller had accumulated between the two sources. - **`trap`.** Installing a handler replaces the previous one rather than adding to it, so a library that sets `trap cleanup EXIT` at load time silently overwrites a caller's handler when reloaded at the wrong moment. - **Cost.** Anything expensive at load time — querying a cloud metadata endpoint, reading a large file, shelling out for a version string — pays twice. ## The guard The bash idiom mirrors C's header guard: a sentinel variable, checked first, set immediately after. ```bash # common.sh if [[ -n "${_COMMON_SH_LOADED:-}" ]]; then return 0 fi _COMMON_SH_LOADED=1 # ... definitions below ... ``` Three details make it correct. `${_COMMON_SH_LOADED:-}` uses the default-value expansion so the test does not blow up under `set -u`, which would otherwise fail on the first load when the variable does not exist. `return 0` — not `exit` — leaves the sourcing early with a success status and leaves the caller running; `exit` would terminate the caller's shell. And `return` at the top level is legal *only* because the file is sourced, which is correct for a pure library and is one reason libraries should not be marked executable. Name the sentinel after the file with a namespace prefix so two libraries cannot collide, and keep in mind the guard is per shell: a child shell starts with a clean environment (unless the variable is exported, which you should not do) and will load the library again — which is what you want, since it has none of the definitions. ## The better fix A guard is a workaround for a library that does things at load time. The structural answer is to make libraries **idempotent by construction**: define functions, and nothing else. No `readonly`, no `PATH` mutation, no traps, no work. Then a double load costs a few milliseconds of re-parsing and changes nothing observable, and you do not need the guard at all. Where a library genuinely needs one-time setup, put it in an explicit `common_init` function the entry point calls once, rather than in the file body — the caller then controls when and how often it happens, and it is testable. A related habit: source libraries through a single resolved absolute path (a `script_dir` variable computed at the top of the entry point) so the same file is not loaded under two spellings. The guard makes the spelling irrelevant, but consistent paths make the dependency graph readable, and they matter for the naive alternative of tracking loaded files in an array. ## What an interviewer is listening for That you know `source` re-executes unconditionally, that you can name a concrete breakage rather than saying "it might be slow" — the `readonly` abort is the sharpest one — and that you reach for side-effect-free libraries before reaching for the guard.

  • Why is `${_COMMON_SH_LOADED:-}` written with the `:-` rather than plain `$_COMMON_SH_LOADED`?
    Under `set -u` an unset variable is a fatal error, and on the very first load the sentinel is by definition unset — so the bare form would abort the script the first time the guard runs. `${var:-}` substitutes an empty string when unset, letting the `-n` test simply be false.
  • Why use `return 0` in the guard instead of `exit 0`?
    Sourced code runs in the caller's shell, so `exit` would terminate the calling script or interactive session. `return` unwinds only the sourcing and hands its status back to the `source` command, leaving the caller running. It is legal at the top level precisely because the file is being sourced.
  • What would make an include guard unnecessary?
    A library that only defines functions. Redefining a function is a no-op in effect, so double loading changes nothing. Move any one-time work — `readonly` constants, `PATH` edits, traps, expensive lookups — into an explicit init function the entry point calls once, and the reload hazard disappears along with the guard.

saying these in an interview costs you the question

  • Assumes bash skips a file it has already sourced
  • Thinks redefining a readonly variable with the same value is allowed
  • Uses exit instead of return in the guard
  • Writes the sentinel test without :- and breaks under set -u
  • Puts PATH edits and traps in a library's file body

context