A bash script dies with `deploy.sh: line 87: ...` inside a helper function that is called from a dozen places, so the line number alone does not tell you which call path got there. Which bash builtin and which shell arrays let a script print its own call stack?
answer
- bash keeps its own frames
- one builtin walks them
- three arrays, same index
- BASH_LINENO records call sites
- FUNCNAME is empty at top level
basics
~20 sBash keeps a call stack you can read: the caller builtin walks it one frame at a time, and the parallel arrays FUNCNAME, BASH_SOURCE and BASH_LINENO expose it directly, so a script can print which function called which, from which file and line.
solid answer
~40 sBash exposes its own call stack. The `caller` builtin, called with a frame number, prints one frame as `<line> <function> <file>` describing the call site; looping `local i=0; while caller $i; do ((i++)); done` walks outward until `caller` returns non-zero and the loop ends. The same information lives in three parallel arrays: `FUNCNAME[0]` is the function currently running and higher indices are its callers, `BASH_SOURCE[i]` is the file where `FUNCNAME[i]` is defined, and `BASH_LINENO[i]` is the line in `BASH_SOURCE[i+1]` from which `FUNCNAME[i]` was called. Writing a small `stacktrace` function over those arrays and calling it from your failure path turns "line 87" into a path. Both only carry information inside a function or a sourced file; at the top level of a script `FUNCNAME` is empty, which is why prefixes use `${FUNCNAME[0]:-main}`.
code
bash · 15 lines#!/usr/bin/env bash
stacktrace() {
local i
for (( i = 1; i < ${#FUNCNAME[@]}; i++ )); do
printf ' at %s (%s:%s)\n' \
"${FUNCNAME[i]:-main}" "${BASH_SOURCE[i]}" "${BASH_LINENO[i-1]}" >&2
done
}
die() { printf 'error: %s\n' "$1" >&2; stacktrace; exit 1; }
require_dir() { [[ -d $1 ]] || die "no such directory: $1"; }
deploy() { require_dir "$1"; }
deploy /nonexistentgo deeper
Know that bash records which function called which, and that FUNCNAME holds those names with element 0 being the function currently running. You are not expected to write a stack printer yet.
Explain the three parallel arrays and the index relationship between them, and be able to write the loop that prints function (file:line) for each frame. Know that caller returns non-zero once the stack is exhausted.
Show where this pays off: a shared shell library whose generic helper fails, an intermittent scheduled job you get one shot at capturing. Connect the stack printer to a diagnostic helper, and note that set -T is needed for DEBUG and RETURN hooks to reach into functions.
Treat it as an inflection point. If scripts in your estate need a debugger's introspection to be operable, decide deliberately whether shell is still the right substrate, and set the boundary at which glue code graduates to a language with real stack traces and tests.
## Bash has a call stack, and it is readable Every function call and every `source` pushes a frame that bash keeps for the life of the call. Three arrays and one builtin expose it, and together they turn a bare line number into a path through the program. ## The arrays They are parallel: index `i` in each describes the same frame. - **`FUNCNAME`** — `FUNCNAME[0]` is the function currently executing, `FUNCNAME[1]` is the function that called it, and so on outward. Outside any function, `FUNCNAME` is empty, which is why prefixes and stack printers write `${FUNCNAME[0]:-main}`. - **`BASH_SOURCE`** — `BASH_SOURCE[i]` is the source file in which `FUNCNAME[i]` is defined. `BASH_SOURCE[0]` inside a function is therefore the library file that defines it, not necessarily the script you invoked. It is also the correct way, in general, for a script to learn its own path — unlike `$0`, it stays right in a sourced file. - **`BASH_LINENO`** — the off-by-one one. `BASH_LINENO[i]` is the line number, in the file `BASH_SOURCE[i+1]`, at which `FUNCNAME[i]` was called. So it describes call *sites*, not the line currently executing; the currently executing line is `$LINENO`. A stack printer falls straight out of that: ```bash stacktrace() { local i for (( i = 1; i < ${#FUNCNAME[@]}; i++ )); do printf ' at %s (%s:%s)\n' \ "${FUNCNAME[i]:-main}" "${BASH_SOURCE[i]}" "${BASH_LINENO[i-1]}" >&2 done } ``` It starts at 1 to skip `stacktrace` itself, and pairs frame `i`'s name with the call-site line recorded at `i-1`. ## The `caller` builtin `caller` reads the same stack. With no argument it prints the line number and source file of the current subroutine call. Given a non-negative integer it prints the line number, subroutine name and source file for that position in the stack, and it returns non-zero when the requested frame does not exist — which gives the idiomatic walk: ```bash print_stack() { local i=0 while caller "$i"; do ((i++)); done } ``` Each line describes where that frame was entered from, innermost first, and the loop terminates naturally when the stack runs out. `caller` is meaningful only inside a function or a sourced file; at the top level of a script there is no subroutine call to report and it returns non-zero. One related builtin behaviour worth knowing: with `shopt -s extdebug` enabled, `declare -F name` prints the function's name together with the line number and file where it was defined, which answers "where is this function actually defined?" in a script that sources several libraries. ## Hooking it up A stack printer is only useful if something calls it on the failure path. The natural places are a diagnostic helper — a `die()` that prints the message, prints the stack, and exits — or a debug hook. Bash's `DEBUG` trap runs before every simple command and makes the command about to run available in `$BASH_COMMAND`, and the `RETURN` trap runs when a function or sourced file returns; by default neither is inherited by shell functions, command substitutions or subshells unless `set -o functrace` (`set -T`) is enabled. Those hooks let you build step-tracing or timing on top of the same stack information. How error *policy* is wired — which failures abort, what an error trap should do, how exit codes are chosen — is a separate discipline from the mechanics of reading the stack, and belongs with the script's error-handling design. ## When it earns its keep For a fifty-line script, a line number is enough. The stack pays for itself when: - Several scripts `source` a shared library and a helper in it fails; the line number points into the library, and only the stack says which caller passed the bad argument. - The failing helper is generic — `require_var`, `retry`, `run` — and used everywhere. - The failure is intermittent in a scheduled job, where you get one chance to capture context and cannot reproduce interactively. ## The honest caveat If you are building a stack-trace framework in shell, note what that signals. Deep call graphs across sourced libraries, with a debugger's worth of introspection bolted on, is usually the point at which the script has outgrown bash and a real language with exceptions, stack traces and tests would be cheaper to operate. The stack tools are for making an existing script diagnosable, not for justifying a larger one.
- Why does a stack printer use `BASH_LINENO[i-1]` alongside `FUNCNAME[i]` rather than `BASH_LINENO[i]`?Because the arrays describe different things at the same index: `FUNCNAME[i]` is a function, while `BASH_LINENO[i]` is the line where `FUNCNAME[i]` was called, recorded in the file of frame `i+1`. Pairing a frame's name with the call site one index lower is what produces a conventional `at f (file:line)` line.
- Why prefer `${BASH_SOURCE[0]}` over `$0` when a script needs to know its own file?`$0` is the name the shell was invoked with, so inside a file pulled in with `source` it still names the top-level script. `BASH_SOURCE[0]` names the file currently being executed, which is what a library function needs to resolve paths relative to itself or to report where it lives.
- What does `set -o functrace` change about DEBUG and RETURN traps?By default those traps are not inherited by shell functions, command substitutions or subshells, so a hook installed at the top level silently stops applying inside functions. `set -o functrace` (`set -T`) makes them inherited, which is what you want if the hook is meant to observe the whole program rather than only its top level.
saying these in an interview costs you the question
- Says bash has no way to see the call stack
- Thinks FUNCNAME[0] is the calling function
- Uses $0 inside a sourced library to find the file
- Expects caller to work at a script's top level
- Reads BASH_LINENO as the currently executing line