skip to content

When a bash script needs to source a helper file that lives next to it, why is `${BASH_SOURCE[0]}` a better basis than `$0` for computing the file's own directory, and what does the usual `cd`-and-`pwd` wrapper add?

level: middleimportance: must knowfreq 55%

answer

  1. invocation name versus file name
  2. the caller's name does not change
  3. a parallel array to FUNCNAME
  4. relative until you make it absolute
  5. symlinks survive the resolution

basics

~20 s

$0 is the name the outermost command was invoked with, so inside a sourced library it names the caller, not the library. ${BASH_SOURCE[0]} is always the file whose code is running. Wrapping it in cd and pwd turns a possibly relative path into an absolute one.

solid answer

~40 s

`$0` holds whatever name the shell was started with — the executed script's path, or `bash`/`-bash` in an interactive session. That is fine for a top-level script, but the moment the same file is sourced as a library, `$0` still names the caller and `dirname "$0"` points at the wrong directory. `${BASH_SOURCE[0]}` is the path of the file that the currently executing code was read from, so it is correct in both cases. Neither is guaranteed absolute — both hold the path as written, relative to whatever the working directory was at invocation — so the idiom is `script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)`, which resolves it to an absolute path once, up front, before anything can `cd` elsewhere. Then `source "$script_dir/lib/log.sh"` works no matter where the caller was standing.

code

bash · 6 lines
bash
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
printf 'this file : %s\n' "${BASH_SOURCE[0]}"
printf 'invoked as: %s\n' "$0"
printf 'directory : %s\n' "$script_dir"

go deeper

for a junior

Know that a script cannot rely on the caller's working directory to find its own helper files, and recognise the script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) line as the standard way to locate them.

for a middle

Explain concretely why $0 is wrong inside a sourced file — it keeps naming the caller — and what each piece of the cd/pwd idiom buys: absolute path, unchanged caller directory, and a failure that is not swallowed.

for a senior

Show that you know the remaining edges: symlinks are not resolved, readlink -f is a GNU dependency, the value must be computed before any cd, and ShellCheck SC2164 exists because an unchecked cd silently yields a wrong path.

for a principal

Frame it as a packaging question — whether shared tooling locates its assets relative to the script at all, versus an installed prefix, an environment variable, or a container image layout — and what that choice costs when the tool is symlinked or vendored into other repos.

## The problem: a script that must find its own files A script that ships with helper files — `lib/log.sh`, a `templates/` directory, a bundled config — has to locate them at runtime. Using a relative path like `source lib/log.sh` resolves against the *caller's* working directory, so the script works when you run it from its own folder and breaks the moment cron or a CI job runs it from `/`. The script needs its own location. ## `$0` and where it breaks `$0` is the name the shell was invoked with. In a script started as `./deploy.sh` it is `./deploy.sh`; started as `bash /opt/tools/deploy.sh` it is `/opt/tools/deploy.sh`; in an interactive shell it is `bash` or `-bash` (the leading dash marking a login shell). Crucially, **`$0` does not change inside a sourced file or inside a function**. If `deploy.sh` does `source ./lib/log.sh`, then inside `log.sh` the value of `$0` is still `./deploy.sh`. A library that computes its own directory from `$0` therefore computes the *caller's* directory, which happens to work while both files sit together and fails as soon as they do not — or as soon as someone sources the library from an interactive shell, where `dirname "$0"` is `.` or worse. ## `BASH_SOURCE` is the call stack of files Bash maintains `BASH_SOURCE` as an array that parallels `FUNCNAME`: element 0 is the file containing the code currently executing, element 1 is the file that sourced it or that defined the calling function, and so on outward. So: - In the top-level script, `${BASH_SOURCE[0]}` equals `$0`. - In a sourced library, `${BASH_SOURCE[0]}` is the library's own path and `${BASH_SOURCE[1]}` is the file that sourced it. - Inside a function, `${BASH_SOURCE[0]}` is the file where the *function was defined*, which is exactly what a library needs to find its siblings. `BASH_SOURCE` is a bashism. It does not exist in `dash` or in a `#!/bin/sh` script on a system where `/bin/sh` is not bash, so a portable script has to fall back to `$0` and accept its limits. ## Why `cd` and `pwd` `${BASH_SOURCE[0]}` is the path *as written*, not a canonical one. If the script was launched as `./deploy.sh`, the value is `./deploy.sh` and `dirname` gives `.` — a path that stops meaning the right thing the instant any code does `cd`. The standard idiom fixes it once: ```bash script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) ``` Reading it inside out: `dirname` strips the filename (yielding `.` when there is no slash, which is correct), `cd` moves there *inside the command substitution* so the caller's directory is untouched, and `pwd` prints the absolute path. The `&&` matters: if the `cd` fails the substitution stays empty and returns non-zero, so a plain assignment under `set -e` aborts instead of silently producing `""` and later sourcing `/lib/log.sh`. ShellCheck's **SC2164** is the rule that flags a bare `cd` whose failure is unchecked. The `--` guards against a directory name beginning with a dash. Quoting both substitutions is what keeps paths containing spaces intact. ## What it still does not do The idiom gives you an absolute path, not a *canonical* one: symlinks in the path are preserved. If `/usr/local/bin/deploy` is a symlink to `/opt/tools/deploy.sh`, `script_dir` is `/usr/local/bin`, and the helper files next to the real script are not there. Resolving that needs `readlink -f` (GNU coreutils; the BSD `readlink` on older macOS has no `-f`) or a manual loop over `readlink`. Note this is a genuine judgment call rather than a bug: sometimes you *want* the symlink's directory, for instance when a symlink farm intentionally selects a configuration. Two more caveats. Compute `script_dir` at the top of the file, before any `cd`, because the relative form is only meaningful against the original working directory. And `source "$script_dir/lib/log.sh"` must keep the quotes and the slash — a slash-free argument makes bash search `$PATH` first. ## The habit to take away Use `${BASH_SOURCE[0]}` in anything that might ever be sourced, resolve it to an absolute directory in one line at the top, and address every bundled file through that variable. It costs one line and removes an entire class of "works on my machine, breaks in cron" failures.

  • What does `${BASH_SOURCE[1]}` hold?
    One frame out: the file that sourced the current file, or the file containing the code that called the currently running function. The array parallels `FUNCNAME`, so index 1 is the caller's file. Libraries occasionally use it for diagnostics — "loaded by X" — but relying on it for logic couples a library to how it was included.
  • How would you resolve symlinks so the directory is the script's real location?
    `readlink -f` from GNU coreutils canonicalises the whole path, so `script_dir=$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")`. It is not portable — older macOS `readlink` has no `-f` — so cross-platform code either loops on plain `readlink` or documents a GNU dependency. Decide deliberately: sometimes the symlink's own directory is what you want.
  • Why compute the directory at the very top of the script rather than where it is first needed?
    Because the value in `${BASH_SOURCE[0]}` may be relative, and it is only meaningful against the working directory the script started in. Any `cd` — yours or a helper's — invalidates it. Resolving once at the top gives an absolute path that stays correct for the rest of the run.

saying these in an interview costs you the question

  • Assumes $0 always names the file whose code is running
  • Writes $(dirname $0) unquoted and breaks on paths with spaces
  • Thinks BASH_SOURCE is available in dash or POSIX sh
  • Assumes the resolved directory has symlinks canonicalised
  • Computes the script directory after the script has already cd'd elsewhere

context