A bash helper library defines `die() { echo "$1" >&2; exit 1; }`. A colleague sources the library into their interactive shell to try it out, calls `die`, and their terminal window closes. Explain what happened and how `return` behaves differently in a sourced file.
answer
- there is no child to kill
- the caller is the process running it
- one verb ends a function, one ends a shell
- source returns a status the caller can ignore
- guard the entry point, not the library
basics
~20 sSourced code runs in the caller's own shell, so exit terminates that shell — an interactive session closes, and a calling script stops mid-run. return only leaves the current function, or stops the sourcing early and hands a status back to source. Libraries should return, not exit.
solid answer
~50 s`source` runs the file's commands in the current shell, so `exit` inside the library is the *caller's* `exit`. Interactively that ends the session and the terminal closes; inside a script it aborts the caller at whatever point the helper was called, skipping everything after it (EXIT traps still fire). `return` is the right verb for library code: inside a function it ends the function with a status, and at the top level of a sourced file it stops sourcing and makes `source` itself return that status. Note `return` at the top level is only legal in a sourced file or a function — bash errors otherwise. The convention is that library functions never exit; they return non-zero and let the entry point decide whether that is fatal, which is also what makes the library testable.
code
bash · 11 lines#!/usr/bin/env bash
# Safe to source and safe to run.
die() { printf 'error: %s\n' "$*" >&2; return 1; }
main() {
die "config missing" || exit 1
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
figo deeper
Remember that sourced code runs in your own shell, so exit in a sourced file ends your session or your script, while return just leaves the function. Do not put exit in a helper you expect others to source.
Explain both legal positions of return — ending a function, and ending the sourcing early with a status handed to source — and note that the latter does not stop the caller unless the caller checks source ./lib.sh || exit 1.
Argue the design rule: helpers return statuses, entry points decide fatality, and the [[ "${BASH_SOURCE[0]}" == "$0" ]] guard is what lets one file be both. Mention that library load-time side effects — set -e, cd, EXIT traps — hijack the caller just as much as exit does.
Own the contract for a shared shell library across teams: what load time is allowed to touch, how errors are signalled to unknown callers, and how you keep it testable by a harness that sources it and asserts statuses rather than forking a process per case.
## Why the terminal closed Sourcing means the library's commands execute in the shell you are already using — there is no separate process to end. `exit` terminates the shell that runs it. When that shell is your interactive session, the session ends and the terminal emulator, having nothing left to display, closes the window. It is not a crash; it is the library doing precisely what it was told, in a shell it did not expect to be living in. The same defect is quieter in a script. `main.sh` sources `lib.sh`, calls `validate_input`, the helper hits a bad value and calls `exit 1` — and `main.sh` stops right there. Everything the caller intended to do after that point, including its own error reporting, retry, or fallback path, is skipped. The caller's `trap ... EXIT` handler *does* run, so registered cleanup still happens, but the caller never gets to make a decision. ## What `return` does instead `return [n]` has two legal positions in bash: 1. **Inside a function** — ends the function immediately, with `n` (or the status of the last command) as its exit status. The shell keeps running. 2. **At the top level of a sourced file** — stops reading the rest of the file and makes the `source` command itself exit with `n`. Anywhere else, bash refuses: `return: can only \`return' from a function or sourced script`. That is why a library with an early-out guard at the top level works when sourced and errors if someone executes it directly. The second form has a subtlety worth stating: a top-level `return 1` in a sourced file does **not** stop the caller. It only makes `source lib.sh` yield status 1. If the caller cares, it must check: ```bash source ./lib.sh || { echo "cannot load lib.sh" >&2; exit 1; } ``` Without that check, a library that bailed out halfway is silently only half-loaded, and the failure surfaces later as a missing function. ## Designing library functions The rule that falls out of this: **library code returns, entry points exit.** A function such as `require_file` should `return 1` and print a message to stderr; the top-level script decides whether that means abort, warn, or try an alternative. This is not just tidiness — it is what makes the library reusable in a context that must survive the failure (a retry loop, a cleanup path, a test harness that sources the library and asserts on statuses). If you genuinely want a fatal helper, make its fatality explicit at the call site instead of hiding it in the helper: `require_file "$cfg" || exit 1`. The reader sees where the script dies. ## Files that are both a library and a command Many utilities want to be both: sourceable for their functions, runnable as a CLI. The standard guard compares the running file with the invocation name: ```bash if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi ``` When the file is executed, `$0` is that file and the two match, so `main` runs and may `exit` freely — it is the entry point. When the file is sourced, `$0` names the caller, the comparison fails, and the file only defines functions. (The comparison is between path strings as written, so it is reliable in the normal case and not a security boundary.) ## Related traps for library authors `exit` is the loudest way a sourced file can hijack its caller, but not the only one. A sourced file that runs `set -e`, `set -x`, `cd`, or reassigns `IFS` changes the caller's shell just as permanently, and a `trap ... EXIT` it installs *replaces* whatever handler the caller had registered rather than adding to it. A well-behaved library therefore does one thing at load time: define functions, and perhaps set variables under a clearly namespaced prefix. Everything else is the entry point's business. ## Recovering interactively If you are exploring a library at a prompt and want to survive its `exit`, source it inside a throwaway shell — `bash -c 'source ./lib.sh; die oops'` — so the fatal call kills that shell instead of yours. Better, fix the library: a helper that cannot be sourced without risking the session is a helper nobody will experiment with.
- What does a top-level `return 2` in a sourced file do to the caller?It stops sourcing at that line and makes the `source` command itself return 2. The caller keeps running with the library only partially loaded, so the caller must write `source ./lib.sh || exit 1` if a failed load should be fatal. Without that check the problem resurfaces later as a missing function.
- How can one file work as both a sourceable library and a runnable command?Put the executable work in a `main` function and guard the call: `if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi`. Executed, the two match and `main` runs; sourced, `$0` names the caller, so the file only defines functions. The entry point may `exit`; the functions should not.
- If a sourced helper calls `exit` inside a running script, does the caller's cleanup still happen?Yes for anything registered with `trap ... EXIT` — that handler fires as the shell terminates. What is lost is the caller's ordinary control flow: error handling, retries, subsequent steps and its own exit-status shaping never run, so the script reports the helper's status rather than a considered one.
saying these in an interview costs you the question
- Thinks exit in a sourced file only ends that file
- Believes a sourced file runs in its own subshell
- Says return and exit are interchangeable inside a function
- Assumes a top-level return in a library aborts the calling script
- Puts exit calls in shared helper functions and calls it error handling