Summary
A developer attempted to extend an existing bash completion function (foo) by wrapping the original function inside a new one. While this worked during the initial shell session, re-sourcing the configuration file caused an infinite recursion loop, leading to a stack overflow and a session crash. This occurs because the mechanism used to “capture” the original function fails to account for the fact that the function being captured is the newly wrapped version after the second source command.
Root Cause
The failure stems from a circular dependency created during the shell’s state mutation.
- First Source: The script captures the original
fzfcompletion function and stores it in__orig_foo_completion_func. It then defines_foo_compand maps it tofoo. - The Mutation: The variable
__orig_foo_completion_funcnow holds the name of the “real” underlying function. - Second Source: When
~/.bashrcis sourced again, the line__orig_foo_completion_func=$(...)executes. However,complete -p foonow returns the definition for the wrapper (_foo_comp), not the original function. - The Loop: The script extracts
_foo_compand assigns it to__orig_foo_completion_func. Now, when_foo_compis called, it calls__orig_foo_completion_func, which is itself_foo_comp.
Why This Happens in Real Systems
In complex production environments, this is a classic idempotency failure.
- Non-Idempotent Scripts: Most shell configuration scripts are written with the assumption they will run exactly once.
- Stateful Side Effects: Functions like
complete -poraliasmodify the global environment. If a script modifies a global state and then attempts to read that same state to perform its logic, it is no longer “pure.” - Namespace Pollution: Using global variables like
__orig_foo_completion_funcwithout checking if they already exist or if they point to the expected implementation leads to state corruption over multiple execution cycles.
Real-World Impact
- Developer Friction: Engineers spend hours debugging “ghost” issues that only appear after they reload their environment.
- CI/CD Failures: If shell wrappers are used in automated provisioning or deployment scripts, a non-idempotent script can cause a blocking failure in the build pipeline.
- System Instability: In extreme cases, recursive shell expansions can consume high CPU or memory before the process terminates, potentially affecting other processes in a shared environment.
Example or Code (if necessary and relevant)
# The problematic pattern:
# 1. Capture current state
__orig_func=$(complete -p my_cmd | awk '{print $NF}')
# 2. Define new wrapper
my_cmd_wrapper() {
"$__orig_func" "$@"
COMPREPLY+=("extra_arg")
}
# 3. Re-bind
complete -F my_cmd_wrapper my_cmd
# BUG: If sourced again, __orig_func becomes 'my_cmd_wrapper'
# Result: my_cmd_wrapper -> calls __orig_func (which is my_cmd_wrapper) -> Loop
How Senior Engineers Fix It
Senior engineers focus on idempotency and guard clauses. To fix this, we must ensure the “capture” logic only executes if the function hasn’t already been wrapped.
- Guard Clauses: Check if the wrapper is already present before attempting to re-wrap.
- Unique Namespacing: Use a specific naming convention to distinguish between “original” and “wrapped” functions.
- Deterministic State: Instead of parsing the current completion to find the old function, use a hardcoded or uniquely identified “base” function.
The Correct Implementation:
# Check if we have already wrapped this function to prevent recursion
if [[ "$(complete -p foo 2>/dev/null)" != *"_foo_comp"* ]]; then
# Capture the original logic
__orig_foo_completion_func=$(complete -p foo 2>/dev/null | sed -r 's/.*\s+-F\s+([a-zA-Z_]+)\s+[a-zA-Z_]+/\1/')
_foo_comp() {
# Call the original captured function
"$__orig_foo_completion_func" "$@"
# Append new completions
COMPREPLY+=($(compgen -W '--bar' -- "${COMP_WORDS[COMP_CWORD]}"))
}
complete -F _foo_comp foo
fi
Why Juniors Miss It
- Happy Path Bias: Juniors often test their code by opening a new terminal (clean state), verifying it works, and assuming it is correct. They rarely test by re-sourcing the environment.
- Mental Model Gap: They view
sourceas a way to “apply changes” rather than a way to “re-execute a sequence of mutations on a living state.” - Lack of Idempotency Awareness: They focus on the functionality (adding
--bar) rather than the lifecycle of the configuration (how the script behaves on the 2nd, 3rd, or 100th execution).