A bash deploy script holds an environment name in $ENV (PROD or STAGING) and reads configuration from variables named PROD_HOST and STAGING_HOST. How does bash's indirect expansion ${!name} let you read the right one, and how does it compare with using eval?
answer
- you cannot write a double dollar sign
- one punctuation mark inside the braces
- it resolves a name, not code
- exactly one level, never a chain
- the safer half of an old eval idiom
basics
~20 sBuild the variable name in a string, then expand it indirectly: key="${ENV}_HOST"; host="${!key}". Bash reads the variable whose name is the value of key. Unlike eval, it substitutes only a value and never executes the name as code.
solid answer
~40 sPut the computed name in an ordinary variable and prefix the expansion with `!`: `key="${ENV}_HOST"; host="${!key}"` gives you `$PROD_HOST` when ENV is `PROD`. Bash resolves exactly one level of indirection — it does not chase a chain — and the referenced variable's own value is returned untouched, with no re-parsing. That is the whole advantage over `eval "host=\$${ENV}_HOST"`, which builds a string and hands it to the parser: if `ENV` ever carries attacker- or user-supplied text, `eval` executes it, while `${!key}` at worst reads a variable that does not exist. Indirection is a bashism, absent from POSIX `sh`, and it is still name-based access, so validate the name against a known set before using it. Bash 4.3 namerefs (`declare -n`) are the other tool for by-name access.
code
bash · 18 linesPROD_HOST=db-prod.internal
STAGING_HOST=db-staging.internal
ENV=${1:-PROD}
case $ENV in
PROD|STAGING) ;;
*) printf 'unknown environment: %s\n' "$ENV" >&2; exit 2 ;;
esac
key="${ENV}_HOST"
host="${!key}"
printf 'deploying to %s (%s)\n' "$host" "$key"
APP_NAME=billing
APP_PORT=8080
for name in ${!APP_*}; do
printf '%s=%s\n' "$name" "${!name}"
donego deeper
Know that bash cannot dereference twice with $$ and that ${!key} reads the variable whose name is stored in key.
Explain that indirection resolves exactly one level and returns the value without re-parsing it, and contrast that with eval building a string that the shell then executes.
Show the security reasoning: indirection downgrades an eval injection to an unintended read, and only an allow-list on the computed name closes the remaining hole in a script that echoes the value back.
Own the design call — parallel variable names are usually a map flattened into identifiers, so decide when the external contract genuinely forces indirection and when the script has outgrown ad-hoc name juggling.
## The problem A script that supports several environments often ends up with parallel variables: ```bash PROD_HOST=db-prod.internal STAGING_HOST=db-staging.internal ENV=PROD ``` You now need the value of the variable *whose name you just computed*. Naively you cannot write `$${ENV}_HOST` — `$$` is the shell's PID, and there is no double-dereference syntax. ## Indirect expansion Bash's answer is the `!` prefix inside a parameter expansion: ```bash key="${ENV}_HOST" # key now holds the string PROD_HOST host="${!key}" # host now holds db-prod.internal ``` The rule is: if the first character inside the braces is `!`, bash takes the value of the named parameter and uses **that** as the name of the parameter to expand. Three properties matter. 1. **Exactly one level.** If `key=other` and `other=third` and `third=x`, `${!key}` gives `third`, not `x`. There is no recursion. 2. **No re-parsing of the value.** Whatever `PROD_HOST` contains — spaces, `$`, backticks, a newline — comes back as a plain value, exactly like `"$PROD_HOST"` would. Only the *name* is computed. 3. **Missing names behave like any other unset variable.** The expansion is empty, and under `set -u` bash reports an unbound-variable error and the script fails, which is usually what you want. Quote it: `"${!key}"`. An unquoted indirect expansion is word-split and globbed just like any other unquoted expansion. ## Why not eval The pre-bash idiom was: ```bash eval "host=\$${ENV}_HOST" # do not do this with untrusted ENV ``` `eval` concatenates text and runs the result as shell code. If `ENV` ever contains something other than a bare identifier — because it came from a CLI argument, an HTTP payload, a CI variable someone can edit, or a filename — the constructed string is executed. `ENV='X; rm -rf /tmp/data #'` is not a hypothetical; it is the standard shape of a shell injection. `${!key}` performs a lookup rather than an evaluation, so the worst outcome is reading a variable that does not exist. That is a real reduction in blast radius, not a guarantee of safety. Indirection still lets caller-controlled data choose *which* variable is read, so a script that echoes `${!key}` back to the user can be steered into printing `AWS_SECRET_ACCESS_KEY` or any other variable in the environment. When the name comes from outside, validate it against an allow-list first: ```bash case $ENV in PROD|STAGING|DEV) ;; *) printf 'unknown environment: %s\n' "$ENV" >&2; exit 2 ;; esac key="${ENV}_HOST" host="${!key}" ``` The general principles of validating untrusted input belong to the security material; the shell-specific point here is simply that indirection narrows the failure from *code execution* to *unintended read*, and only an allow-list closes the rest. ## The name-listing cousin The same `!` prefix has a second, unrelated meaning with a trailing `*` or `@`: `${!prefix*}` expands to the **names** of all set variables that begin with `prefix`. ```bash for name in ${!APP_*}; do printf '%s=%s\n' "$name" "${!name}" done ``` That loop enumerates every `APP_*` variable and prints each one's value, combining both meanings of `!`. It is a compact way to dump a namespaced configuration block, and a good reason to prefix your script's variables consistently. ## When to reach for something else Indirection is a bashism — POSIX `sh` has neither `${!name}` nor `${!prefix*}` — so a `#!/bin/sh` script cannot use it. Bash 4.3 and later also offer namerefs, `declare -n`, which create an alias to another variable and are the better fit when a function must *write* through the name rather than just read it. More importantly, parallel variable names are frequently the wrong data structure in the first place. `PROD_HOST`/`STAGING_HOST` is a map with the key flattened into the identifier. Bash 4 has real associative arrays, and reaching for indirection to fake one is a design smell worth naming in a review. Indirection earns its place when the names are genuinely fixed by something outside your control — an existing environment-variable contract, a twelve-factor configuration convention, values injected by a CI system — rather than when you chose the naming scheme yourself.
- Does ${!key} follow a chain of references?No — it resolves exactly one level. If key holds `other` and `other` holds `third`, `${!key}` yields the string `third`, not whatever `third` contains. There is no recursive dereference in bash, so a second lookup requires a second expansion, and code that assumes chaining silently returns a name where a value was expected.
- Using ${!name} instead of eval removes code execution. What risk remains?The caller still chooses which variable is read. If the name derives from user input and the value is printed, logged or sent onward, an attacker can steer it at any variable in the environment, including credentials. Validate the name against an explicit allow-list — a `case` over the permitted values — before expanding it.
- When is an associative array the better answer than indirection?Almost whenever you control the naming. Parallel identifiers like PROD_HOST and STAGING_HOST are a map with the key welded into the variable name; a real map expresses that directly and keeps the key out of the identifier namespace. Indirection earns its place when the names are fixed by an external contract you cannot change.
- What does ${!APP_*} expand to?The names — not the values — of every set variable whose name begins with `APP_`. Combined with indirection it lets you enumerate a namespaced configuration block: loop over `${!APP_*}` and expand `${!name}` inside the loop to print each value. It is a distinct meaning of the same `!` prefix, triggered by the trailing `*` or `@`.
saying these in an interview costs you the question
- Thinks $${VAR}_HOST dereferences twice
- Claims ${!key} resolves a whole chain of names
- Says eval is fine because the input is internal
- Believes indirection is safe with any user-supplied name
- Assumes ${!name} works in a #!/bin/sh script