Dynamic Dispatch and Metaprogramming
Chapter 10 — Dynamic Dispatch and Metaprogramming
Metaprogramming is writing code that treats other code as data — inspecting it, generating it, or modifying it at runtime. Bash supports a surprisingly rich set of these techniques: introspecting function definitions with declare -f, calling functions by name stored in variables, building command-dispatch tables, generating functions in loops, and constructing code strings that are safely evaluated. Mastering these patterns produces dramatically shorter, more consistent, and more easily extended scripts — at the cost of code that is harder to follow if the reader is unfamiliar with the idioms. This chapter covers every major technique, explains when each is appropriate, and shows where the sharp edges are.
1 — Introspecting Functions with declare -f
declare -f prints the definition of a function exactly as Bash has parsed it. declare -F prints only the name (and, with -p, the source file and line number). These are the building blocks of every introspection-based pattern.
# Does this function exist? function_exists() { declare -f "$1" >/dev/null 2&1 } # Print full definition greet() { echo "hello $1"; } declare -f greet # greet () # { # echo "hello $1" # } # List all defined functions declare -F # declare -f greet # declare -f function_exists # ... # List function names only declare -F | awk '{print $3}' # Where was a function defined? (requires extdebug) shopt -s extdebug declare -F greet # greet 1 /path/to/lib.sh ← name, line, file shopt -u extdebug # Copy a function to a new name function_copy() { local src="$1" dst="$2" function_exists "$src" || { echo "no such function: $src" >&2; return 1; } local body # Grab body then replace the name body=$(declare -f "$src") eval "${dst}() ${body#*()}" } greet world # → hello world function_copy greet hello_alias hello_alias world # → hello world # Rename a function (copy + unset) function_rename() { function_copy "$1" "$2" && unset -f "$1" }
2 — Calling Functions by Name
The simplest metaprogramming trick in Bash: store a function name in a variable, then call it. Because Bash resolves commands by name at runtime, a variable holding a function name is a first-class function reference.
cmd_start() { echo "starting..."; } cmd_stop() { echo "stopping..."; } cmd_status() { echo "running"; } # Direct call via variable action=start "cmd_${action}" # calls cmd_start # Validated call — never call an arbitrary variable without validation dispatch() { local action="$1"; shift local fn="cmd_${action}" if function_exists "$fn"; then "$fn" "$@" else printf 'Unknown action: %s\n' "$action" >&2 return 1 fi } dispatch start dispatch stop dispatch unknown # → Unknown action: unknown # Passing a function as a callback (higher-order functions) apply_to_each() { local fn="$1"; shift local item for item in "$@"; do "$fn" "$item" done } shout() { printf '%s!\n' "${1^^}"; } apply_to_each shout alpha beta gamma # ALPHA! # BETA! # GAMMA! # map/filter/reduce in pure Bash using function callbacks array_map() { local fn="$1"; shift local -a result=() local item for item in "$@"; do result+=( "$("$fn" "$item")" ) done printf '%s\n' "${result[@]}" } double() { printf '%d' $(( $1 * 2 )); } array_map double 1 2 3 4 # 2 # 4 # 6 # 8 array_filter() { local pred="$1"; shift local item for item in "$@"; do "$pred" "$item" && printf '%s\n' "$item" done } is_even() { (( $1 % 2 == 0 )); } array_filter is_even 1 2 3 4 5 6 # 2 # 4 # 6
3 — Command-Dispatch Tables
An associative array that maps command names to function names (or to short inline strings) is the Bash equivalent of a jump table. It is cleaner than a long case statement and trivially extensible at runtime.
# ── Dispatch table via associative array ───────────────────────── declare -A CMDS CMDS[start]=do_start CMDS[stop]=do_stop CMDS[reload]=do_reload CMDS[status]=do_status CMDS[help]=do_help run_cmd() { local name="$1"; shift if [[ -v CMDS["$name"] ]]; then "${CMDS[$name]}" "$@" else printf '%s: unknown command "%s"\nAvailable: %s\n' \ "$0" "$name" "${!CMDS[*]}" >&2 return 1 fi } # Register a new command at runtime register_cmd() { local name="$1" fn="$2" function_exists "$fn" || { echo "no such function: $fn" >&2; return 1; } CMDS["$name"]="$fn" } # ── Dispatch table with metadata ───────────────────────────────── # Store description alongside function name using a compound key declare -A CMD_FN CMD_DESC register() { local name="$1" fn="$2" desc="$3" CMD_FN["$name"]="$fn" CMD_DESC["$name"]="$desc" } print_help() { printf 'Available commands:\n' local name for name in $(printf '%s\n' "${!CMD_FN[@]}" | sort); do printf ' %-15s %s\n' "$name" "${CMD_DESC[$name]}" done } register start do_start "Start the service" register stop do_stop "Stop the service" register status do_status "Show service status" register help print_help "Show this message"
4 — Plugin Systems
A naming convention is all you need for a plugin system in Bash. Define a hook name (e.g. plugin_init, plugin_run) and source plugin files. Each plugin that defines a function matching the naming convention is automatically discovered and invoked.
#!/usr/bin/env bash # Plugin system: load *.plugin.sh files and call their hooks PLUGIN_DIR="${PLUGIN_DIR:-/etc/myapp/plugins}" # ── Load all plugins ───────────────────────────────────────────── load_plugins() { local file for file in "$PLUGIN_DIR"/*.plugin.sh; do [[ -f "$file" ]] || continue # Snapshot functions before sourcing local -A before while IFS= read -r line; do before["${line##* }"]=1 done < <(declare -F) source "$file" # Find newly added functions while IFS= read -r line; do local fname="${line##* }" [[ -v before["$fname"] ]] || \ printf '[plugin] loaded %s from %s\n' "$fname" "$file" done < <(declare -F) done } # ── Invoke a hook in all plugins that define it ─────────────────── run_hook() { local hook="$1"; shift local fn # List all functions matching plugin_*_HOOKNAME pattern while IFS= read -r fn; do [[ $fn =~ ^plugin_[a-z_]+_${hook}$ ]] || continue printf '[hook:%s] %s\n' "$hook" "$fn" "$fn" "$@" done < <(declare -F | awk '{print $3}') } # ── Example plugin file: /etc/myapp/plugins/slack.plugin.sh ────── # # plugin_slack_init() { echo "Slack plugin loaded"; } # plugin_slack_run() { curl -s "$SLACK_URL" -d "text=$*"; } # plugin_slack_finish(){ echo "Slack plugin done"; } # load_plugins run_hook init run_hook run "build completed" run_hook finish
5 — Generating Functions at Runtime
When a set of functions differ only in a parameter — a resource name, a prefix, a verb — generate them in a loop instead of copy-pasting. This is the Bash equivalent of a factory function or decorator.
# ── Generate CRUD functions for multiple resource types ────────── for resource in user group project; do eval " ${resource}_create() { printf 'Creating %s: %%s\n' \"${resource}\" \"\$1\" } ${resource}_delete() { printf 'Deleting %s: %%s\n' \"${resource}\" \"\$1\" } ${resource}_list() { printf 'Listing all %ss\n' \"${resource}\" } " done user_create alice # → Creating user: alice group_delete admins # → Deleting group: admins project_list # → Listing all projects # ── Safer alternative: printf-built function strings ───────────── # Avoid embedding $resource directly in eval strings when the value # comes from outside the script — use printf %q to quote it. make_getter() { local name="$1" value="$2" # printf %q produces a shell-quoted string safe for eval local qval; printf -v qval '%q' "$value" eval "get_${name}() { printf '%s' ${qval}; }" } make_getter hostname "webserver-01" make_getter region "us-east-1" get_hostname # → webserver-01 get_region # → us-east-1 # ── Memoisation decorator ───────────────────────────────────────── # Wrap any function so its results are cached by argument memoize() { local fn="$1" function_exists "$fn" || { echo "no such function: $fn" >&2; return 1; } # Copy original to __orig_FUNCNAME function_copy "$fn" "__orig_${fn}" # Replace fn with a caching wrapper eval " declare -A __memo_${fn} ${fn}() { local _key=\"\$*\" local -n _cache=__memo_${fn} if [[ -v _cache[\"\$_key\"] ]]; then printf '%s' \"\${_cache[\$_key]}\" return fi local _result _result=\$( __orig_${fn} \"\$@\" ) _cache[\"\$_key\"]=\"\$_result\" printf '%s' \"\$_result\" } " } slow_upper() { sleep 1; printf '%s' "${1^^}"; } memoize slow_upper slow_upper hello # takes ~1s slow_upper hello # instant — cache hit slow_upper world # takes ~1s — different key
6 — Safe eval Patterns
eval is necessary for some metaprogramming tasks in Bash, but it is the single most dangerous command in the language. Every unsanitised piece of data that reaches eval is a command-injection vulnerability. Use these patterns to stay safe.
# ── Rule 1: never eval externally-sourced data directly ────────── # WRONG: $name comes from user input or a file eval "${name}_init()" # name='; rm -rf /' → catastrophic # RIGHT: validate with an allowlist before eval [[ $name =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || { echo "bad name" >&2; exit 1; } eval "${name}_init() { echo initialised; }" # ── Rule 2: use printf %q for values that cannot be allowlisted ── safe_set_var() { local varname="$1" value="$2" # Validate name is a legal identifier [[ $varname =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || return 1 local qval printf -v qval '%q' "$value" # escapes all shell metacharacters eval "$varname=$qval" } safe_set_var greeting "hello world; rm -rf /" echo "$greeting" # → hello world; rm -rf / (treated as literal string) # ── Rule 3: use declare -p round-trip for structured data ───────── # declare -p produces output that eval can safely consume because # Bash itself generates it — no untrusted input involved. declare -A original=([key1]=val1 [key2]="value with spaces") # Serialise serialised=$(declare -p original) printf '%s\n' "$serialised" # declare -A original=([key1]="val1" [key2]="value with spaces" ) # Deserialise into a new name eval "${serialised/original/restored}" declare -p restored # declare -A restored=([key1]="val1" [key2]="value with spaces" ) # ── Rule 4: prefer declare -n (namerefs) over eval for indirection # nameref is safe — it only allows variable access, not code execution # AVOID (eval for indirection): varname=myarray eval "echo \${$varname[@]}" # PREFER (nameref): declare -n ref=$varname echo "${ref[@]}" # no eval, no injection risk
7 — Namerefs: Safe Indirect Variable Access
declare -n creates a nameref — a variable that is an alias for another variable whose name is its value. Namerefs let you pass array names to functions without using eval, and they support all compound-variable operations.
# ── Basic nameref ──────────────────────────────────────────────── original="hello" declare -n ref=original echo "$ref" # → hello ref="world" echo "$original" # → world (same storage) # ── Passing arrays by name ──────────────────────────────────────── array_sum() { declare -n _arr="$1" # _arr is an alias for the named array local sum=0 local x for x in "${_arr[@]}"; do (( sum += x )) done printf '%d' "$sum" } numbers=(10 20 30 40) array_sum numbers # → 100 # ── Returning values via nameref (avoids subshell) ──────────────── parse_kv() { # $1 = name of associative array to populate # $2 = string of key=value pairs (space-separated) declare -n _out="$1" local pair k v for pair in $2; do k="${pair%%=*}" v="${pair#*=}" _out["$k"]="$v" done } declare -A config parse_kv config "host=localhost port=5432 dbname=mydb" printf 'host=%s port=%s\n' "${config[host]}" "${config[port]}" # host=localhost port=5432 # ── Nameref caution: circular reference ────────────────────────── a=b declare -n b=a # b → a → b: circular! # Bash detects this and prints a warning; $b evaluates to empty. # ── Use a unique prefix for internal namerefs in functions ──────── # If a function uses -n ref=$1 and the caller passes "ref" as the # array name, there is a self-reference collision. Use an unlikely # prefix (double underscore + function name) to avoid it. safe_push() { declare -n __safe_push_arr="$1"; shift __safe_push_arr+=("$@") }
8 — Generating Code at Runtime
Sometimes the right output of a script is another script. Code generation — emitting shell code as text — is a legitimate and powerful technique for configuration management, test fixtures, and build systems.
# ── Emit a self-contained script from a template ───────────────── generate_deploy_script() { local env="$1" version="$2" outfile="$3" # Validate inputs before embedding in generated code [[ $env =~ ^[a-z]+$ ]] || { echo "bad env" >&2; return 1; } [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "bad version" >&2; return 1; } # Heredoc with NO quoting on the delimiter = variable expansion # Heredoc with QUOTED delimiter ('EOF') = literal output, no expansion cat > "$outfile" <<EOF #!/usr/bin/env bash # Auto-generated deploy script # Generated: $(date -u '+%F %T UTC') set -euo pipefail ENV=${env} VERSION=${version} echo "Deploying v\${VERSION} to \${ENV}" # ... rest of deploy logic ... EOF chmod +x "$outfile" } generate_deploy_script staging 1.4.2 /tmp/deploy_staging.sh # ── Emit a config file from variable state ──────────────────────── emit_env_file() { # Print all variables matching PREFIX_* as KEY=value env file local prefix="$1" local var val qval while IFS='=' read -r var val; do [[ $var == "${prefix}_"* ]] || continue printf -v qval '%q' "$val" printf '%s=%s\n' "$var" "$qval" done < <(declare -x) } export APP_HOST=localhost export APP_PORT=8080 export APP_DB="my database" emit_env_file APP # APP_DB=my\ database # APP_HOST=localhost # APP_PORT=8080
9 — Tracing and Decorating Functions
# ── Automatically wrap every function with a timing decorator ───── trace_all_functions() { local fn while IFS= read -r fn; do # Skip internal and already-wrapped functions [[ $fn == __* || $fn == trace_* ]] && continue function_copy "$fn" "__orig_${fn}" eval " ${fn}() { local _t0=\${EPOCHREALTIME} __orig_${fn} \"\$@\" local _rc=\$? local _elapsed printf -v _elapsed '%.3f' \"\$(echo \"\${EPOCHREALTIME} - \${_t0}\" | bc)\" printf '[trace] %s() %.3fs rc=%d\n' '${fn}' \"\${_elapsed}\" \"\${_rc}\" >&2 return \$_rc } " done < <(declare -F | awk '{print $3}') } # ── before/after hooks on a specific function ───────────────────── add_before_hook() { local fn="$1" hook="$2" function_copy "$fn" "__orig_${fn}" eval "${fn}() { ${hook} \"\$@\"; __orig_${fn} \"\$@\"; }" } add_after_hook() { local fn="$1" hook="$2" function_copy "$fn" "__orig_${fn}" eval " ${fn}() { __orig_${fn} \"\$@\" local _rc=\$? ${hook} \"\$@\" return \$_rc } " } deploy() { echo "deploying..."; } pre_deploy_log() { echo "[audit] deploy started by $USER"; } post_deploy_log(){ echo "[audit] deploy finished"; } add_before_hook deploy pre_deploy_log add_after_hook deploy post_deploy_log deploy # [audit] deploy started by alice # deploying... # [audit] deploy finished
Exercises
Exercise 1 — Build a subcommand CLI dispatcher
Write a script bin/cli.sh that acts as a dispatcher for
subcommands, similar to how git or
kubectl work. Requirements:
- Subcommands are loaded from
lib/commands/— each file is namedCMD.shand must definecmd_CMD_run()and optionallycmd_CMD_help() - Running
cli.sh SUBCMD [ARGS...]sources the right file then callscmd_SUBCMD_run "$@" - Running
cli.sh help [SUBCMD]callscmd_SUBCMD_helpif defined, or prints a generic list of available subcommands - Running
cli.shwith no arguments prints the command list and exits 1 - Unknown subcommands print a helpful error and exit 2
Also write two example command files: lib/commands/greet.sh
(prints "Hello, NAME!") and lib/commands/shout.sh (prints the
argument in uppercase).
#!/usr/bin/env bash # bin/cli.sh set -euo pipefail CMD_DIR="${CMD_DIR:-${BASH_SOURCE[0]%/*}/../lib/commands}" function_exists() { declare -f "$1" >/dev/null 2&1; } list_commands() { local f printf 'Available commands:\n' for f in "$CMD_DIR"/*.sh; do [[ -f "$f" ]] || continue printf ' %s\n' "${f##*/}" "${f%.sh}" # prints just the name without .sh done | sed 's|.*/||; s|\.sh||' | sort | xargs -I{} printf ' {}\n' } load_command() { local name="$1" [[ $name =~ ^[a-z][a-z0-9_-]*$ ]] || { printf 'Invalid command name\n' >&2; return 1; } local file="$CMD_DIR/$name.sh" [[ -f "$file" ]] || return 1 source "$file" } subcmd="${1:-}" if [[ -z "$subcmd" ]]; then list_commands exit 1 fi if [[ "$subcmd" == "help" ]]; then if [[ -n "${2:-}" ]]; then load_command "$2" || { printf 'Unknown command: %s\n' "$2" >&2; exit 2; } function_exists "cmd_${2}_help" && "cmd_${2}_help" || printf 'No help for %s\n' "$2" else list_commands fi exit 0 fi if ! load_command "$subcmd"; then printf '%s: unknown command "%s"\n' "${0##*/}" "$subcmd" >&2 exit 2 fi fn="cmd_${subcmd}_run" function_exists "$fn" || { printf '%s: %s defines no cmd_%s_run function\n' \ "${0##*/}" "$subcmd.sh" "$subcmd" >&2 exit 1 } shift "$fn" "$@"
# lib/commands/greet.sh cmd_greet_run() { local name="${1:-World}" printf 'Hello, %s!\n' "$name" } cmd_greet_help() { printf 'Usage: cli.sh greet [NAME]\n Prints a greeting.\n' }
# lib/commands/shout.sh cmd_shout_run() { local msg="${*:-NOTHING}" printf '%s\n' "${msg^^}" } cmd_shout_help() { printf 'Usage: cli.sh shout MESSAGE\n Prints MESSAGE in uppercase.\n' }
Exercise 2 — Generic CRUD factory with namerefs
Write a function make_resource RESOURCE_NAME ARRAY_NAME that
generates four functions for a given resource type: RESOURCE_add,
RESOURCE_remove, RESOURCE_list, and
RESOURCE_contains. All four functions operate on the array
variable named by ARRAY_NAME using declare -n.
Then demonstrate by creating tag_* functions operating on a
TAGS array, and server_* functions operating on a
SERVERS array. Do not use eval for
the array access — only for function definition.
#!/usr/bin/env bash set -euo pipefail function_exists() { declare -f "$1" >/dev/null 2&1; } make_resource() { local res="$1" # e.g. "tag" local arrname="$2" # e.g. "TAGS" # Validate both names are legal identifiers [[ $res =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || { echo "bad resource name" >&2; return 1; } [[ $arrname =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || { echo "bad array name" >&2; return 1; } eval " ${res}_add() { declare -n __${res}_arr=${arrname} __${res}_arr+=(\"\$1\") } ${res}_remove() { declare -n __${res}_arr=${arrname} local item=\$1 i new=() for i in \"\${__${res}_arr[@]}\"; do [[ \"\$i\" == \"\$item\" ]] || new+=(\"\$i\") done __${res}_arr=(\"\${new[@]}\") } ${res}_list() { declare -n __${res}_arr=${arrname} printf '%s\n' \"\${__${res}_arr[@]}\" } ${res}_contains() { declare -n __${res}_arr=${arrname} local item=\$1 i for i in \"\${__${res}_arr[@]}\"; do [[ \"\$i\" == \"\$item\" ]] && return 0 done return 1 } " } # ── Demo ───────────────────────────────────────────────────────── declare -a TAGS=() declare -a SERVERS=() make_resource tag TAGS make_resource server SERVERS tag_add production tag_add europe tag_add production # duplicate (allowed — remove for set semantics) tag_list # production # europe # production tag_remove production tag_list # europe # production tag_contains europe && echo "europe: yes" tag_contains asia || echo "asia: no" server_add web-01 server_add web-02 server_list # web-01 # web-02
Exercise 3 — Memoisation library with TTL
Extend the memoisation pattern from Section 5 into a proper library
lib/memo.sh. The memo_wrap FUNCTION [TTL_SECONDS]
function should wrap any function so that:
- Results are cached by argument string
- Cached results expire after TTL seconds (default: no expiry)
- A cache miss calls the original function and stores both the result and the timestamp
memo_clear FUNCTIONinvalidates the entire cache for that functionmemo_stats FUNCTIONprints hit count, miss count, and current cache size
Demonstrate with a function that simulates a slow API call
(sleep 1) and show the cache working with a 5-second TTL.
#!/usr/bin/env bash # lib/memo.sh function_exists() { declare -f "$1" >/dev/null 2&1; } function_copy() { local body body=$(declare -f "$1") || return 1 eval "${2}() ${body#*()}" } memo_wrap() { local fn="$1" local ttl="${2:-0}" # 0 = no expiry function_exists "$fn" || { echo "no such function: $fn" >&2; return 1; } function_copy "$fn" "__memo_orig_${fn}" # Per-function cache and stats arrays eval " declare -A __memo_cache_${fn} declare -A __memo_ts_${fn} __memo_hits_${fn}=0 __memo_misses_${fn}=0 " eval " ${fn}() { local _key=\"\$*\" local -n _cache=__memo_cache_${fn} local -n _ts=__memo_ts_${fn} local _now printf -v _now '%d' \"\${EPOCHSECONDS}\" if [[ -v _cache[\"\$_key\"] ]]; then local _age=$(( _now - _ts[\$_key] )) if (( ${ttl} == 0 || _age < ${ttl} )); then (( __memo_hits_${fn}++ )) || true printf '%s' \"\${_cache[\$_key]}\" return fi fi (( __memo_misses_${fn}++ )) || true local _result _result=\$( __memo_orig_${fn} \"\$@\" ) _cache[\"\$_key\"]=\"\$_result\" _ts[\"\$_key\"]=\"\$_now\" printf '%s' \"\$_result\" } " } memo_clear() { local fn="$1" eval "__memo_cache_${fn}=(); __memo_ts_${fn}=()" } memo_stats() { local fn="$1" declare -n _c="__memo_cache_${fn}" declare -n _h="__memo_hits_${fn}" declare -n _m="__memo_misses_${fn}" printf '%s: hits=%d misses=%d size=%d\n' "$fn" "$_h" "$_m" "${#_c[@]}" } # ── Demo ───────────────────────────────────────────────────────── fake_api_call() { sleep 1 printf 'result_for_%s' "$1" } memo_wrap fake_api_call 5 # 5-second TTL time fake_api_call foo # miss → ~1s time fake_api_call foo # hit → instant time fake_api_call bar # miss → ~1s memo_stats fake_api_call # fake_api_call: hits=1 misses=2 size=2 sleep 6 # let cache expire time fake_api_call foo # miss again → ~1s memo_stats fake_api_call # fake_api_call: hits=1 misses=3 size=2
Exercise 4 — Config-driven function generator
Write a script that reads a configuration file in this format:
ALIAS greet "printf 'Hi, %s\n'"
ALIAS bye "printf 'Goodbye, %s\n'"
ALIAS shout "printf '%s\n' \"\${1^^}\""
WRAP greet audit_log
WRAP shout rate_limit
The ALIAS name body directive creates a function called
name using the body string (safely, without allowing injection).
The WRAP fn decorator directive wraps fn so that
decorator is called before it (the decorator receives the same
arguments). Validate that both function names and wrapper names are legal
identifiers. Include working implementations of audit_log and
rate_limit so the demo runs.
#!/usr/bin/env bash set -euo pipefail function_exists() { declare -f "$1" >/dev/null 2&1; } function_copy() { local body; body=$(declare -f "$1") || return 1 eval "${2}() ${body#*()}" } is_ident() { [[ $1 =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; } # ── Decorators ──────────────────────────────────────────────────── declare -A _RATE_LAST RATE_LIMIT_SECS=2 audit_log() { printf '[audit] %s called with: %s\n' "${FUNCNAME[1]}" "$*" } rate_limit() { local fn="${FUNCNAME[1]}" local now="${EPOCHSECONDS}" local last="${_RATE_LAST[$fn]:-0}" if (( now - last < RATE_LIMIT_SECS )); then printf '[rate-limit] %s called too quickly, skipping\n' "$fn" >&2 return 1 fi _RATE_LAST["$fn"]="$now" } # ── Config parser ──────────────────────────────────────────────── load_config() { local file="$1" local directive name rest while IFS= read -r line; do [[ $line =~ ^[[:space:]]*(#|$) ]] && continue read -r directive name rest <<< "$line" is_ident "$name" || { printf 'Invalid identifier: %s\n' "$name" >&2; continue } case "$directive" in ALIAS) # rest is the body string — strip surrounding quotes local body="${rest#\"}"; body="${body%\"}" # Embed body literally — it's from a trusted config file # In a hardened implementation, scan body for dangerous patterns eval "${name}() { ${body} \"\$@\"; }" printf '[config] defined %s()\n' "$name" ;; WRAP) local dec="$rest" is_ident "$dec" || { printf 'Invalid decorator: %s\n' "$dec" >&2; continue; } function_exists "$name" || { printf 'WRAP: function not defined yet: %s\n' "$name" >&2; continue } function_exists "$dec" || { printf 'WRAP: decorator not defined: %s\n' "$dec" >&2; continue } function_copy "$name" "__wrapped_${name}" eval "${name}() { ${dec} \"\$@\" || return; __wrapped_${name} \"\$@\"; }" printf '[config] wrapped %s() with %s()\n' "$name" "$dec" ;; *) printf 'Unknown directive: %s\n' "$directive" >&2 ;; esac done < "$file" } # ── Demo ───────────────────────────────────────────────────────── load_config functions.conf greet Alice # [audit] then Hi, Alice shout hello # [rate_limit check] then HELLO shout hello # rate-limited on rapid second call bye Bob # Goodbye, Bob