skip to content

Bash functions may call themselves. What limits how deep a recursive bash function can go, what does bash's FUNCNEST variable do, and how can such a function tell how deep it currently is?

level: middleimportance: nice to knowfreq 20%

answer

  1. no default limit at all
  2. the failure mode is memory, not an error
  3. one variable caps nesting depth
  4. an array names the call stack
  5. each level gets its own local

basics

~20 s

By default bash imposes no recursion limit, so a runaway recursive function grows the call stack until the shell exhausts memory and dies. Setting FUNCNEST to a positive number caps nesting depth, and the FUNCNAME array records the current call stack.

solid answer

~50 s

Bash supports recursion, and each invocation gets its own copy of anything the function declared `local`, so a recursive helper does not stomp on its own state. What bash does not give you is a default depth limit: without one, an accidental infinite recursion keeps allocating frames and saved locals until the shell is killed by the out-of-memory killer or crashes — there is no clean "maximum recursion depth" error like Python's. Setting the shell variable `FUNCNEST` to a value greater than zero caps the nesting level; exceeding it aborts the current command with a `maximum function nesting level exceeded` message instead of consuming the machine. `FUNCNEST` is a bash 4 feature, so it is absent from the bash 3.2 that macOS ships. For depth, `FUNCNAME` is an array of the active call stack, with `${FUNCNAME[0]}` the current function and `main` at the bottom when running a script, so `${#FUNCNAME[@]}` gives the frame count.

code

bash · 12 lines
bash
#!/usr/bin/env bash
FUNCNEST=20                       # bash 4+: abort instead of exhausting memory

countdown() {
  local n=$1                      # per-invocation copy; without local, recursion breaks
  echo "depth=${#FUNCNAME[@]} n=$n in ${FUNCNAME[0]}"
  if [ "$n" -gt 0 ]; then
    countdown "$((n - 1))"
  fi
}

countdown 3

go deeper

for a junior

Know that bash functions can call themselves, that each level needs its own local variables, and that an infinite recursion in bash does not stop politely on its own.

for a middle

Explain that there is no default depth limit, that FUNCNEST caps nesting in bash 4 and later, and that FUNCNAME is the call-stack array whose length gives the current depth.

for a senior

Focus on the operational consequence: a runaway recursion exhausts shell memory and gets the process killed, so set an explicit cap, guard the base case, and know the bash 3.2 fallback of checking FUNCNAME length.

for a principal

Own the choice of tool: argue when recursive traversal or nested-data parsing has left the range where shell is appropriate, and what limits you require on any unattended script so a logic bug cannot exhaust a shared host.

## Recursion works, with a caveat about state Nothing stops a bash function from calling itself, and the scoping rules make it usable: `local` is per invocation, so each level of a recursive call gets its own copy of the name, saved on entry and restored on return. ```bash countdown() { local n=$1 echo "n=$n" if [ "$n" -gt 0 ]; then countdown "$((n - 1))" fi } ``` Drop the `local` and every level shares one `n`, so the innermost call's assignment is what the unwinding outer calls see — a classic way to turn a correct-looking recursion into an infinite one. That direct connection between per-invocation locals and workable recursion is why the two subjects belong together. ## There is no default depth limit Compare with other languages: CPython raises `RecursionError` at a configurable limit; the JVM throws `StackOverflowError`. Bash, by default, does neither. Frames and saved local values are heap allocations in the shell process, and they simply keep accumulating. A recursion that never reaches its base case ends with the shell consuming gigabytes, then being killed — the process disappears, often with a signal rather than an exit code, and in a container that can mean the OOM killer takes something else with it. That is the practical risk: the failure mode of a buggy recursive bash function is a dead host or a dead pod, not a stack trace. ## FUNCNEST `FUNCNEST` is the guard rail. If it is set to a numeric value greater than zero, it defines the maximum function-nesting level; a call that would exceed it is aborted and bash reports that the maximum function nesting level was exceeded. Setting it to an empty value, zero, or a non-numeric string removes the limit. ```bash FUNCNEST=50 runaway() { runaway; } runaway # aborts at depth 50 instead of eating memory ``` Two caveats worth stating in an interview. First, it is a *bash 4* feature — the bash 3.2 that ships as `/bin/bash` on macOS does not have it, so a script that relies on it for safety is not portable to that interpreter. Second, it counts function nesting, not general recursion: a script that recurses by re-executing itself as a new process is unaffected. ## Knowing your own depth: FUNCNAME `FUNCNAME` is a bash array variable holding the names of the shell functions currently on the execution call stack. `${FUNCNAME[0]}` is the function executing right now, `${FUNCNAME[1]}` is its caller, and so on; when a script (rather than an interactive shell) is running, the bottom-most element is `main`. It only exists while a function is executing. ```bash walk() { local depth=$(( ${#FUNCNAME[@]} - 1 )) # subtract the "main" frame echo "depth=$depth in ${FUNCNAME[0]} called by ${FUNCNAME[1]}" } ``` That makes `FUNCNAME` useful for two things beyond recursion: enforcing your own depth cap portably (compare `${#FUNCNAME[@]}` against a constant and `return 1`), and writing log lines that name the calling function, which is far more informative than a bare message. ## Judgment: should this be recursive at all? Bash function calls are relatively expensive, and every level of recursion allocates saved locals. Directory walking, the most common temptation, is better served by `find` or by `shopt -s globstar` with a `**` glob than by a hand-rolled recursive descent. Parsing nested data — JSON, YAML — is a signal to reach for a tool built for it rather than to recurse in shell. When you do write a recursive function, the checklist is short: declare every working variable `local`; make the base case the *first* thing in the body so it cannot be skipped; cap depth explicitly, either with `FUNCNEST` when you control the interpreter or with a `${#FUNCNAME[@]}` check when you do not; and pass state as arguments rather than relying on the caller's variables being visible.

  • What actually happens if a bash function recurses forever and FUNCNEST is not set?
    Nothing stops it. Each call allocates a frame plus saved copies of the locals, so the shell's memory grows until allocation fails or the kernel's OOM killer terminates the process. You get a dead shell, not a recursion error, which on a shared host or in a container can take other workloads down with it.
  • Why does dropping `local` usually break a recursive bash function?
    Because without it there is one shared variable. The deepest call's assignment is what the outer calls see as they unwind, so a counter or accumulator no longer tracks the level it belongs to and a base-case test can stop being reachable. Locals are saved and restored per invocation, which is what makes recursion behave.
  • How would you cap recursion depth on a host whose bash is 3.2?
    Check the call stack yourself: FUNCNAME exists in bash 3, so compare ${#FUNCNAME[@]} against a constant at the top of the function and return non-zero when it is exceeded. It is explicit rather than automatic, but it works on any bash and it lets you emit a useful error message.

saying these in an interview costs you the question

  • Says bash raises a recursion-depth error like Python does
  • Thinks FUNCNEST is set to a useful default value
  • Believes each recursive call gets fresh variables automatically
  • Assumes FUNCNEST exists on the bash 3.2 that macOS ships
  • Reaches for recursive shell functions to walk directory trees

context