A bash script needs the directory it lives in so it can load a file next to itself. Why is `dirname "$0"` unreliable, and what does `${BASH_SOURCE[0]}` give you instead?
answer
- invocation name versus source file
- sourcing does not change the first one
- it is an array, matching the call stack
- relative today, wrong after a cd
- a symlink is still not the real file
basics
~20 s$0 is the name the shell was invoked with, so it points at the caller when a file is sourced and may be a symlink or a relative path. ${BASH_SOURCE[0]} is the path of the file whose code is executing, which is correct whether the file is run or sourced.
solid answer
~50 s`$0` answers "what was this shell invoked as", not "which file is this code in". If the file is sourced rather than executed, `$0` still holds the *caller's* name, so `dirname "$0"` resolves to the wrong directory. It is also whatever text the user typed — a relative path, or a symlink in a bin directory rather than the real file. `BASH_SOURCE` is a bash array parallel to the call stack, and `${BASH_SOURCE[0]}` is the source file containing the code currently running, so it is right in both the executed and the sourced case. The usual idiom is `script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)`, which also turns a relative path into an absolute one. It still does not resolve symlinks, so if the entry point is a symlink into a package directory you need an explicit resolution step.
code
bash · 12 lines#!/usr/bin/env bash
set -euo pipefail
# resolve once, at the top, before anything cd's
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
echo "invoked as : $0"
echo "source file: ${BASH_SOURCE[0]}"
echo "directory : $script_dir"
# load a sibling file by absolute path
# source "$script_dir/lib/common.sh"go deeper
Know the idiom and why it exists: resolve the script's directory at the top with ${BASH_SOURCE[0]} rather than $0, and use that absolute path to load files next to the script.
Explain the divergence — $0 is the invocation name and stays pointing at the caller when a file is sourced, while ${BASH_SOURCE[0]} names the file whose code is running — and why cd ... && pwd is needed to absolutise a relative path.
Enumerate the real-world ways self-location breaks: symlinked entry points, PATH invocation, a cd mid-script, piping the script into bash. Say which of these your deployment actually hits and harden only for those, including symlink resolution and its GNU-versus-BSD split.
Set the convention: whether tools are installed as real files, symlinks or wrappers, and what the entry-point contract is. Weigh self-locating scripts against explicitly configured install roots, since the latter removes a whole class of packaging bugs.
## Two different questions "Where am I installed?" and "how was I invoked?" are different questions, and bash has a different variable for each. `$0` is a positional parameter: it holds the name the current shell or script was invoked with. For `./deploy.sh` it is `./deploy.sh`; for `bash /opt/tools/deploy.sh` it is `/opt/tools/deploy.sh`; for an interactive shell it is `bash` or `-bash`. It reflects the invocation, not the file layout. `BASH_SOURCE` is a bash array that runs parallel to the function call stack. `${BASH_SOURCE[0]}` is the name of the source file where the currently executing code lives; deeper indices name the files of the callers. At the top level of a script that was executed, `${BASH_SOURCE[0]}` and `$0` are the same. They diverge in the case that matters. ## The sourcing divergence ```bash # lib.sh echo "\$0 = $0" echo "BASH_SOURCE[0] = ${BASH_SOURCE[0]}" ``` Run `bash lib.sh` and both print `lib.sh`. Now source it from `deploy.sh`: ```bash # deploy.sh source ./lib/lib.sh ``` Sourcing does not start a new shell, so `$0` is unchanged — it still names `deploy.sh`. `${BASH_SOURCE[0]}` correctly reports `./lib/lib.sh`. A library that computed its own directory from `$0` would look for its data files next to the *calling* script. Since a shell library is exactly the kind of file that gets sourced, this is not a corner case; it is the normal case for library code. ## The other ways `$0` misleads Even for an executed script, `$0` carries whatever the user typed: - **Relative paths.** Invoked as `./deploy.sh`, `dirname "$0"` is `.`. If the script then `cd`s anywhere, that stored `.` now points at the wrong place. Resolve to an absolute path immediately, at the top of the file, before any `cd`. - **Symlinks.** If `/usr/local/bin/deploy` is a symlink to `/opt/mytool/deploy.sh`, then `$0` — and `${BASH_SOURCE[0]}` too — is the symlink path, so the computed directory is `/usr/local/bin`, not the package directory where the siblings live. - **PATH lookup.** Invoked bare as `deploy`, `$0` is just `deploy` with no directory part at all, and `dirname` yields `.`. - **Piped input.** `curl ... | bash` has no file at all; `${BASH_SOURCE[0]}` is `main` or empty, and there is no directory to find. Installer scripts have to fetch what they need rather than look beside themselves. ## The idiom, and what each piece does ```bash script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) source "$script_dir/lib/common.sh" ``` - `${BASH_SOURCE[0]}` picks the file that is actually executing. - `dirname` strips the filename; `--` protects against a path beginning with a dash. - `cd ... && pwd` converts a relative path to an absolute one, and the `&&` means a failed `cd` does not leave you capturing the wrong directory. (`cd` inside the command substitution runs in a subshell, so the surrounding script's working directory is untouched.) - Assign it once at the top, before anything changes directory, and quote every later use — a path with a space is otherwise split. A common hardening step is `set -euo pipefail` plus checking that the resolved directory exists, since a silently empty `script_dir` turns `"$script_dir/lib/common.sh"` into an absolute path at the filesystem root. ## Symlink resolution and portability If your entry point is deliberately a symlink, you must resolve it. On GNU systems `readlink -f` does the whole job; macOS ships the BSD `readlink`, which does not support `-f` in the same way, so cross-platform scripts either loop on `readlink` until the target stops being a link, or depend on GNU coreutils explicitly. Decide which you need — many scripts genuinely do not, because they are installed as real files and only ever invoked directly. Finally, `BASH_SOURCE` is a bash variable, not POSIX. A script with `#!/bin/sh` running under dash has no `BASH_SOURCE`, and there is no fully portable equivalent; `$0` plus a documented invocation convention is the usual compromise there. If you need the self-location trick, say so in the shebang by requiring bash.
- Why does the idiom use `cd ... && pwd` instead of just `dirname`?`dirname` returns whatever form the path came in as, so a relative invocation gives you `.` or `./lib`. That value goes stale the moment the script changes directory. `cd` into it and print `pwd` to get an absolute path fixed at start-up, and the `&&` ensures a failed `cd` does not leave you capturing an unrelated directory.
- What are the deeper indices of BASH_SOURCE for?`BASH_SOURCE` parallels the call stack, so `${BASH_SOURCE[1]}` is the file of the caller, and so on. Together with `FUNCNAME` and `BASH_LINENO` it is what error handlers use to print a stack trace naming file and line for each frame, which is far more useful than a bare message when a sourced library fails.
- What happens to this idiom under `curl ... | bash`?There is no file, so there is no directory to compute — `${BASH_SOURCE[0]}` is not a real path and the resolved directory is meaningless or the current one. Installer scripts delivered that way must fetch their assets over the network or embed them, and should detect the case explicitly rather than silently loading whatever happens to sit in `$PWD`.
saying these in an interview costs you the question
- Says $0 always holds the running script's own path
- Uses dirname "$0" inside a sourced library
- Stores a relative script dir and then cd's away
- Assumes BASH_SOURCE resolves symlinks
- Expects readlink -f to work the same on macOS