Of all the Bash commands, poor old eval
probably has the worst reputation. Justified, or just bad press? We discuss the use and dangers of this least-loved of Linux commands.
We Need To Talk About eval
Used carelessly, eval
can lead to unpredictable behavior and even system insecurities. From the sounds of it, we should probably not use it, right? Well not quite.
You could say something similar about automobiles. In the wrong hands, they’re a deadly weapon. People use them in ram-raids and as get-away vehicles. Should we all stop using cars? No, of course not. But they have to be used properly, and by people who know how to drive them.
The usual adjective applied to eval
is “evil.” But it all comes down to how it’s being used. The eval
command collates the values from one or more variables. It creates a command string. It then executes that command. This makes it useful when you need to cope with situations where the content of a command is derived dynamically during the execution of your script.
Problems arise when a script is written to use eval
on a string that has been received from somewhere outside the script. It may be typed in by a user, sent through an API, tagged onto an HTTPS request, or anywhere else external to the script.
If the string that eval
is going to work on was not derived locally and programmatically, there is a risk that the string might contain embedded malicious instructions or other badly-formed input. Obviously, you don’t want eval
to execute malicious commands. So to be safe, don’t use eval
with externally-generated strings or user input.
First Steps With eval
The eval
command is a built-in Bash shell command. If Bash is present, eval
will be present.
eval
concatenates its parameters into a single string. It will use a single space to separate concatenated elements. It evaluates the arguments and then passes the entire string to the shell to execute.
Let’s create a variable called wordcount
.
wordcount="wc -w raw-notes.md"
The string variable contains a command to count the words in a file called “raw-notes.md.”
We can use eval
to execute that command by passing it the value of the variable.
The command is executed in the current shell, not in a subshell. We can easily show this. We’ve got a short text file called “variables.txt.” It contains these two lines.
first=How-To second=Geek
We’ll use cat
to send these lines to the terminal window. Then we’ll use eval
to evaluate a cat
command so that the instructions inside the text file are acted on. This will set the variables for us.
cat variables.txt eval "$(cat variables.txt)" echo $first $second
By using echo
to print the values of the variables we can see that eval
command runs in the current shell, not a subshell.
A process in a subshell cannot change the shell environment of the parent. Because eval runs in the current shell, the variables set by eval
are usable from the shell that launched the eval
command.
Note that if you use eval
in a script, the shell that would be altered by eval
is the subshell that the script is running in, not the shell that launched it.
RELATED: How to Use the Linux cat and tac Commands
Using Variables in the Command String
We can include other variables in the command strings. We’ll set two variables to hold integers.
num1=10 num2=7
We’ll create a variable to hold an expr
command that will return the sum of two numbers. This means we need to access the values of the two integer variables in the command. Note the backticks around the expr
statement.
add="`expr $num1 + $num2`"
We’ll create another command to show us the result of the expr
statement.
show="echo"
Note that we don’t need to include a space at the end of the echo
string, nor at the start of the expr
string. eval
takes care of that.
And to execute the entire command we use:
eval $show $add
The variable values inside the expr
string are substituted into the string by eval
, before it is passed to the shell to be executed.
RELATED: How to Work with Variables in Bash
Accessing Variables Inside Variables
You can assign a value to a variable, and then assign the name of that variable to another variable. Using eval
, you can access the value held in the first variable, from its name which is the value stored in the second variable. An example will help you untangle that.
Copy this script to an editor, and save it as a file called “assign.sh.”
#!/bin/bash title="How-To Geek" webpage=title command="echo" eval $command ${$webpage}
We need to make it executable with the chmod
command.
chmod +x assign.sh
You’ll need to do this for any scripts you copy from this article. Just use the appropriate script name in each case.
When we run our script we see the text from the variable title
even though the eval
command is using the variable webpage
.
./assign.sh
The escaped dollar sign “$
” and the braces “{}
” cause eval to look at the value held inside the variable whose name is stored in the webpage
variable.
Using Dynamically Created Variables
We can use eval
to create variables dynamically. This script is called “loop.sh.”
#!/bin/bash total=0 label="Looping complete. Total:" for n in {1..10} do eval x$n=$n echo "Loop" $x$n ((total+=$x$n)) done echo $x1 $x2 $x3 $x4 $x5 $x6 $x7 $x8 $x9 $x10 echo $label $total
It creates a variable called total
which holds the sum of the values of the variables we create. It then creates a string variable called label
. This is a simple string of text.
We’re going to loop 10 times and create 10 variables called x1
up to x10
. The eval
statement in the body of the loop provides the “x” and takes the value of the loop counter $n
to create the variable name. At the same time, it sets the new variable to the value of the loop counter $n
.
It prints the new variable to the terminal window and then increments the total
variable with the value of the new variable.
Outside of the loop, the 10 new variables are printed once more, all on one line. Note that we can refer to the variables by their real names too, without using a calculated or derived version of their names.
Finally, we print the value of the total
variable.
./loop.sh
RELATED: Primer: Bash Loops: for, while, and until
Using eval With Arrays
Imagine a scenario where you have a script that is long-running and performing some processing for you. It writes to a log file with a name created from a time stamp. Occasionally, it will start a new log file. When the script has finished, if there have been no errors, it deletes the log files it has created.
You don’t want it to simply rm *.log
, you only want it to delete the log files it has created. This script simulates that functionality. This is “clear-logs.sh.”
#!/bin/bash declare -a logfiles filecount=0 rm_string="echo" function create_logfile() { ((++filecount)) filename=$(date +"%Y-%m-%d_%H-%M-%S").log logfiles[$filecount]=$filename echo $filecount "Created" ${logfiles[$filecount]} } # body of the script. Some processing is done here that # periodically generates a log file. We'll simulate that create_logfile sleep 3 create_logfile sleep 3 create_logfile sleep 3 create_logfile # are there any files to remove? for ((file=1; file<=$filecount; file++)) do # remove the logfile eval $rm_string ${logfiles[$file]} "deleted..." logfiles[$file]="" done
The script declares an array called logfiles
. This will hold the names of the log files that are created by the script. It declares a variable called filecount
. This will hold the number of log files that have been created.
It also declares a string called rm_string
. In a real-world script, this would contain the rm
command, but we’re using echo
so we can demonstrate the principle in a non-destructive fashion.
The function create_logfile()
is where each log file is named, and where it would be opened. We’re only creating the filename, and pretending it has been created in the file system.
The function increments the filecount
variable. Its initial value is zero, so the first filename we create is stored at position one in the array. This is done on purpose, as well see later.
The filename is created using the date
command, and the “.log” extension. The name is stored in the array at the position indicated by filecount
. The name is printed to the terminal window. In a real-world script, you’d also create the actual file.
The body of the script is simulated using the sleep
command. It creates the first log file, waits three seconds, and then creates another. It creates four log files, spaced out so that the timestamps in their filenames are different.
Finally, there is a loop that deletes the log files. The loop counter file is set to one. It counts up to and including the value of filecount
, which holds the number of files that were created.
If filecount
is still set to zero—because no log files were created—the loop body will never be executed because one is not less than or equal to zero. That’s why the filecount
variable was set to zero when it was declared and why it was incremented before the first file was created.
Inside the loop, we use eval
with our non-destructive rm_string
and the name of the file which is retrieved from the array. We then set the array element to an empty string.
This is what we see when we run the script.
./clear-logs.sh
It’s Not All Bad
Much-maligned eval
definitely has its uses. Like most tools, used recklessly it is dangerous, and in more ways than one.
If you make sure the strings it works on are created internally and not captured from humans, APIs, or things like HTTPS requests, you’ll avoid the major pitfalls.
RELATED: How to Display the Date and Time in the Linux Terminal (and Use It In Bash Scripts)