opscape

Shell builtins · bash builtin

eval

Join the arguments into a string and run it as shell code, with a second round of parsing and expansion.

eval [ARGUMENT...]

10 of 10 examples run in Debian 13.7 on 2026-10-31 · manual: bash(1), SHELL BUILTIN COMMANDS

eval takes its arguments, joins them with spaces, and hands the result to the shell's parser as if you had typed it. The text is therefore expanded twice: once when the shell builds eval's arguments, and again when eval runs them. That second pass is the whole point: it can turn data into variable names, quotes, pipes and redirections.

It is also why eval is dangerous. Anything inside the evaluated string that comes from a user, a file name, an HTTP response or an environment variable is executed as code. A value such as x; rm -rf ~ is harmless in a variable and catastrophic inside eval.

Most historic uses of eval have safer replacements in modern bash: arrays for building commands, ${!name} and declare -n for indirect variables, printf -v for assigning to a variable whose name is in another variable, and printf %q when you truly must produce shell code. Legitimate uses remain, mostly for loading code a trusted program prints for you, such as eval "$(ssh-agent -s)".

Examples

Run a command stored in a string

cmd='grep -c ERROR logs/app.log'
eval "$cmd"
output
2

eval parses the string as a full command line. Plain $cmd would also work for this simple case, but not for anything with quotes, pipes or redirections, as the next example shows.

Pipes and redirections in a string need eval

cmd='sort data/fruits.txt | uniq -c | head -n 3'
$cmd
echo "---"
eval "$cmd"
output
sort: extra operand '|' not allowed with -c
---
      3 apple
      2 banana
      2 cherry

Expanding $cmd only splits it into words, so sort receives |, uniq, -c and the rest as arguments, takes -c as its "check" option and fails. eval reparses the text and the | becomes a real pipe. This is the kind of power that makes it risky.

Why eval is dangerous

greet-unsafe.sh
#!/usr/bin/env bash
name=$1
eval "echo Hello, $name"
bash greet-unsafe.sh 'Alice'
bash greet-unsafe.sh 'Alice; touch HACKED; ls H*'
output
Hello, Alice
Hello, Alice
HACKED

The second argument is ordinary data, but eval treats the ; in it as a command separator, so the "name" creates a file and lists it. With a less friendly payload this would delete files or leak secrets. Never pass untrusted text to eval.

File names can carry code too

Setup run before this example
touch 'report$(echo INJECTED >&2).txt'
for f in report*; do
  eval "echo processing $f"
done
output
INJECTED
processing report.txt

The file name contains $(...). The for loop handles it as plain text, but eval expands it a second time and runs the command inside, printing INJECTED. Anyone who can create a file in a directory your script walks can run code as you.

Build commands with an array instead

args=(-c -i)
pattern='error'
args+=(-- "$pattern" "logs/app.log")
grep "${args[@]}"
output
2

An array keeps every argument separate, spaces and special characters included, with no second parse. This replaces almost every "build a command string then eval it" pattern.

Read a variable whose name is in another variable

db_host=db01; web_host=web01
role=db
var="${role}_host"
echo "unsafe: $(eval "echo \$$var")"
echo "safe:   ${!var}"
output
unsafe: db01
safe:   db01

${!var} expands the variable named by var without running any code. The eval version gives the same result but would execute whatever var contained.

Assign to a variable named at run time

set_config() { printf -v "cfg_$1" '%s' "$2"; }
set_config port 8080
set_config motd 'hello; rm -rf /'
declare -p cfg_port cfg_motd
output
declare -- cfg_port="8080"
declare -- cfg_motd="hello; rm -rf /"

printf -v NAME assigns to a variable by name and treats the value as plain data. The equivalent eval "cfg_$1=$2" would have run rm -rf /. bash refuses a NAME that is not a valid identifier, so it cannot be abused either. declare -n is the other option.

Quote safely with %q when you must generate code

name='Alice; touch HACKED'
eval "echo Hello, $(printf '%q' "$name")"
ls HACKED
outputexit 2
Hello, Alice; touch HACKED
ls: cannot access 'HACKED': No such file or directory

printf %q escapes the value so the second parse reads it back as one literal word. The ; is printed instead of executed, and ls confirms that no file was created.

Load shell code printed by a trusted program

gen() { echo 'export BUILD_ID=42 BUILD_ENV=ci'; }
eval "$(gen)"
env | grep '^BUILD_'
output
BUILD_ENV=ci
BUILD_ID=42

Tools such as ssh-agent -s, dircolors and pyenv init - print shell assignments meant to be evaluated in the current shell. This is the accepted use of eval: the code comes from a program you trust, not from data. Always quote "$(...)".

Watch the two rounds of expansion

x='$HOME'
echo $x
eval echo $x
output
$HOME
/home/dev

The first echo prints the value of x as is, $HOME. With eval, the result of the first expansion is expanded again, and $HOME becomes the home directory.

Common pitfalls

  • Passing anything that contains user input, file names, or downloaded text to eval. It runs ;, $(...), backticks and redirections inside the data.

  • Unquoted eval $cmd joins and re-splits the words, mangling spacing and globbing them. If you must use it, write eval "$cmd".

  • Reaching for eval to get indirect variables: ${!name}, declare -n and printf -v do it without executing code.

  • Building a command line as a string and evaluating it; use an array (cmd=(grep -r -- "$pat" .); "${cmd[@]}").

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • eval is POSIX and behaves the same in every shell. The safer replacements are not: ${!var}, declare -n, printf -v and printf %q are bash features (zsh has its own ${(P)var}).

Related