Since bash expands a command line only once, when does a script genuinely need `eval` to force a second pass, and which bash features let you avoid reaching for it?
answer
- one pass, one escape hatch
- structure computed, not just arguments
- tools that print shell code
- arrays and printf -v instead
- two rounds means two rounds of quoting
basics
~20 seval re-parses its argument as a fresh command line, giving a second full round of expansion. It is genuinely needed only when the command's structure, not just its arguments, is computed at run time — for example running the shell assignments a tool prints. Arrays, printf -v and namerefs cover most other cases.
solid answer
~50 sBash expands a line once, so any text that appears *inside* an expansion result is data. `eval` is the one builtin that waives that: it expands its arguments as a normal command line, then parses and executes the result from scratch, which is a second trip through the whole pipeline. The legitimate cases are narrow — consuming a tool that deliberately emits shell code, such as `eval "$(ssh-agent -s)"` or a shell hook a program prints, and applying quoting that was itself computed. Almost everything else has a first-class alternative: build a command as an array and run `"${cmd[@]}"` so the words are already words, use `printf -v "$name"` to assign to a computed variable name, use `declare -n` for a by-name reference on bash 4.3+, and use a plain arithmetic loop instead of `eval` on a brace range. The reason to prefer those is not style: whatever reaches `eval` becomes syntax, which is the shell-injection surface.
code
bash · 12 lines# Legitimate: the tool's contract is "my stdout is shell code"
eval "$(ssh-agent -s)"
# Avoidable: running an assembled command
cmd=(rsync -a --delete "/src dir/" /dst/)
"${cmd[@]}" # no eval, spaces safe
# Avoidable: assigning to a computed variable name
name=count_total
value=42
printf -v "$name" '%s' "$value" # instead of eval "$name=$value"
echo "$count_total" # 42go deeper
Know that eval takes a string and runs it as if you had typed it, and that arrays are the normal way to build and run a command with spaces in its arguments.
Explain the two rounds of expansion and why escaping inside eval is confusing — what survives round one becomes the input to round two — and name printf -v as the assign-by-computed-name alternative.
Show judgment about the boundary: the second pass is warranted when the structure is computed, as with a tool that prints shell code, and everything interpolated must be something you generated or %q-quoted.
Set the team rule: eval is the one construct that waives bash's data-is-never-syntax guarantee, so its use is an auditable exception with a named trusted source, not a general mechanism for running strings.
## Why a second pass is ever wanted Bash expands a command line once, in a fixed sequence of stages, and the pipeline never restarts. That gives a strong guarantee — text that arrives via a variable or a command's output is data, never syntax. It also creates a small class of problems that a single pass cannot express, because the thing you need to compute *is* the syntax: ```bash n=5 echo {1..$n} # {1..5} -- braces already ran cmd='ls -l "my file"' $cmd # four words: ls, -l, "my, file" ``` In the second line the quotes in the value are ordinary characters by the time they exist; quote removal already happened for this command line, and there is no stage left to interpret them. ## What eval actually does `eval` concatenates its arguments with spaces, expands that string once as part of the normal `eval` command line, and then hands the result back to the shell to be **parsed and executed as a fresh command line** — a second complete trip through parsing and all the expansion stages. Two rounds is why the escaping in `eval` code looks strange: you must reason about what survives round one to become the input to round two. ```bash n=5 eval "for i in {1..$n}; do echo \"\$i\"; done" # prints 1..5 ``` Here `$n` is expanded in round one, producing `for i in {1..5}; ...`, and round two finally gets a literal range. The `\$i` is protected so it survives to round two, where the loop variable actually exists. ## The genuinely legitimate uses **Consuming a tool that emits shell code.** Some programs are designed to print assignments for the calling shell to run, because a child process cannot set its parent's environment: ```bash eval "$(ssh-agent -s)" ``` `ssh-agent -s` prints Bourne-shell assignments (`SSH_AUTH_SOCK=…; export SSH_AUTH_SOCK; …`). Capturing them in a variable would leave them as inert text; `eval` is the documented interface. The same pattern appears wherever a tool prints a shell hook or an environment block. This is not injection-by-accident: the contract of the program is "my stdout is shell code", and the trust boundary is the program you chose to run. **Applying computed quoting.** When a string was produced *by the shell itself* in a re-parseable form — `printf '%q'` output, or `set -- "$@"` style argument reconstruction — `eval` is the matching reader for that writer. `%q` exists precisely so that its output is safe to feed back through a second pass. **Reaching a construct that only exists at parse time.** Redirections whose file descriptor number is computed, or the brace-range case above, cannot be expressed as arguments because they are syntax, not data. ## The alternatives that cover most other cases - **A command as an array.** If the problem is "run a command I assembled", store the words as elements and run `"${cmd[@]}"`. Each element stays one word regardless of spaces, with no parsing round and no quoting puzzle. This replaces the great majority of `eval "$cmd"` in real scripts. - **`printf -v` for a computed variable name.** `printf -v "$name" '%s' "$value"` assigns to the variable whose name is in `$name` without any re-parse. Compare `eval "$name=\$value"`, which is the classic injection example when `$name` is attacker-influenced. - **`declare -n` namerefs** (bash 4.3+) give a by-name alias for reading and writing another variable, and indirect expansion `${!name}` reads one — both without a second pass. - **An arithmetic loop** instead of `eval` on a brace range: `for ((i = 1; i <= n; i++))`. - **Functions instead of generated code.** If you find yourself building a script fragment as a string, a function taking parameters is almost always the same thing without the parse. ## The judgment an interviewer is listening for The wrong answer is "never use `eval`" delivered as a slogan, and the other wrong answer is using it as a general-purpose way to run strings. The right shape is: bash's one-pass model is a feature, `eval` is the deliberate escape hatch from it, and you take the hatch only when the *structure* rather than the *arguments* is computed. When you do, everything interpolated into the string must be something you generated — a literal, a number you validated, or `printf '%q'` output — because inside `eval` the data-is-never-syntax guarantee is gone. The security analysis of that boundary, and the wider family of injection sinks it belongs to (`sh -c`, `ssh`, `find -exec` with a shell), is a topic of its own; what belongs here is knowing *why* `eval` is the only thing in bash that grants a second expansion, and that a computed variable name or a computed command line rarely needs one.
- Why can't you replace `eval "$(ssh-agent -s)"` with capturing the output into a variable and running it?Because the output is shell syntax — assignments and `export` statements — and a variable's value is never re-parsed as syntax. Running `$output` would word-split it and try to execute a program named after the first word. Only a second pass interprets assignments, and `eval` is the only builtin that gives one.
- What is the safest way to interpolate a value into a string you are about to eval?Do not interpolate raw values at all; interpolate `printf '%q'` output, which bash guarantees is re-parseable back to the same string, or restrict the value to something you generated such as a validated integer. Anything else means the value's metacharacters become syntax in round two, which is the injection case.
- How does `declare -n ref="$name"` differ from `eval` for writing to a variable chosen at run time?A nameref makes `ref` an alias, so `ref=value` writes through to the named variable with no re-parse — the name is used as a name, never as code. It needs bash 4.3+. `printf -v "$name"` achieves the same on older bash. Both keep the data-is-not-syntax guarantee that `eval` gives away.
saying these in an interview costs you the question
- Uses eval as the normal way to run a built command string
- Says eval is fine as long as you quote the string once
- Claims eval runs the command in a subshell
- Thinks an array cannot hold a full command line
- Believes there is another builtin that re-parses text