Design a site like this with WordPress.com
Get started

Avoiding ‘eval’ with SwiftDialog

I first came across the eval command when looking at some community SwiftDialog scripts. eval holds dark arcane secrets. There’s no man page, because it’s a shell built in command. Invoking help eval tells you the following:

eval [arg…]

Read ARGs as input to the shell and execute the resulting command(s).

The simplicity of this definition didn’t help my confusion about how this tool works, it took a while of testing various ways of building strings to pass to it before I began to understand.

eval Solves a Tricky SwiftDialog Problem…

… but it comes at a cost. Consider the following block of code:

#!/bin/zsh
set -x
dialogPath="/usr/local/bin/dialog"
listitems=""
for app in /Applications/*; do
    listitems="$listitems --listitem $app"
done
$dialogPath $listitems

This code will result in a variable containing the name of every item in your /Applications folder. However, if you run it you’ll notice it doesn’t properly pass our list to the Dialog command. This is because our entire variable is being interpreted as a single argument, instead of splitting it into the many arguments we need to create a list in SwiftDialog. Since we’ve used set -x, you’ll see a very verbose output when you run this script. If you look closely at what command is being passed to the shell, you’ll notice the entire block of apps is enclosed by single quotes.

The eval command will split this singular argument along its whitespace, ignoring single and double quotes in the variable string. So we can change out the last line of the script to be:

eval $dialogPath $listitems

This seems to solve the problem, except now we have another issue: File names with spaces in them are not being properly handled. Even though the $app variable is quoted in the script, eval ignores those quotes and is splitting the file name if it contains a space.

So now we need to somehow “trick” eval into respecting quotation marks where we want but ignoring them where we don’t. We can do this by escaping our quotes, which results in them being passed to be interpreted by the shell. Example:

#!/bin/zsh
set -x
dialogPath="/usr/local/bin/dialog"
listitems=""
for app in /Applications/*; do
    listitems="$listitems --listitem \"$app\""
done
eval $dialogPath $listitems

In this script, we’ve now added the eval command and we’ve done some serious tricks on line 6 where we’ve escaped quotes that we want eval to respect. This is now a functional script.

The Cost of eval is Complexity

I’ve got a handful of projects in use that use eval exactly in this way. The problem, in my opinion, is that it leads to tricky and error prone code. Consider the following example script:

#!/bin/zsh
set -x
dialogTitle="This is my title"
dialogMessage="This is my message"
dialogPath="/usr/local/bin/dialog"
dialogCMD="$dialogPath --title \"$dialogTitle\" \
--message \"$dialogMessage\" \
--icon \"/System/Applications/App Store.app\" \
"
eval "$dialogCMD"

In order to pass the contents of my $dialogTitle and $dialogMessage variables through eval, I have to escape quotation marks when building the command. Then i do the same for my --icon file path. I’m also escaping line breaks to make this readable.

Consider this next snippet I wrote for a recent project. The absurdity of having to think this through is what made me want to find a better way to solve the problem.

dialogListOptions+=' --listitem "'"$i"'"'

This line is perfectly valid, and it works exactly as I need it to when it passes through eval, and yet it still makes me want to weep tears of joy and failure at the same time.

These are dark arts, and they are not to be trifled with.

We Can Solve This Problem by Using Arrays

I went through my own projects, as well as a few of the community SwiftDialog scripts, and was able to make simple rewrites which allowed me to avoid having to use the eval command. In my opinion this solution results in cleaner code that is more forgiving and readable.

Consider this final example script:

#!/bin/zsh

#set -x gives us very verbose output about what the script is doing
set -x 

#Path to the dialog command
dialogPath="/usr/local/bin/dialog"
dialogTitle="Listing Applications"
dialogMessage="This is the contents of your /Applications folder."

#Set our swiftdialog message options. Remember to use an array=()
dialogOptions=(
    --messagefont "name=Impact"
    --titlefont "name=Impact"
    --icon none
    --messagealignment center
)

#Set our swiftdialog message content. Remember to use an array=()
dialogContent=(
    --title "$dialogTitle"
    --message "$dialogMessage"
)

#Loop through the contents of the /Applications folder
for app in /Applications/*; do
    #Append the name of this item to our dialog list
    dialogContent+=(
        --listitem "$(basename "$app")"
    )
done

#Call our dialog command
#Anywhere we're calling an array containing instructions for swiftDialog, be sure to use the format: "${array[@]}"
"$dialogPath" "${dialogOptions[@]}" "${dialogContent[@]}"

In this example script, I’ve cleaned up our code and made a functional SwiftDialog command to achieve the following:

  • Generate a Dialog list with the contents of our Applications folder
  • Maintained whitespace in the file names
  • Kept the code clean in the sense that each option is listed on it’s own line for readability
  • Avoided the use of eval
  • Avoided the need to escape any linebreaks or quotes
  • Demonstrate that it is simple to use multiple sets of dialog argument arrays to build your final command.
    • In this example I used a dialogOptions array to set options while using the dialogContent array to set the actual message contents of the dialog window
  • Demonstrate that we can use command substitution within an array.
    • In this case I used basename so that Dialog will show only the file/folder name of each item, rather than showing the entire file path

I came up with a few basic guidelines for rewriting SwiftDialog scripts to avoid eval:

  1. When creating options I want to pass to the Dialog command, always use an array. For bash and zsh, this means using parenthesis like this: array=(values in parenthesis). These can be on multiple lines. Any string with whitespace or special characters should be quoted.
  2. When calling my Dialog command, use the array in the following format: "${dialogOptions[@]}". The [@] portion specifies that you want to print each entry of the array separately. Using [*] instead would specify that you want to print every entry of the array together as a single string (which would result in us needing to use eval again.)

The Same for Bash or Zsh

This example script should work fine using either #!/bin/bash or #!/bin/zsh.

There Are Surely Situations I Haven’t Considered Here

I am certain there are use cases and situations for using eval that I haven’t considered here. For all of the use cases I’ve had for eval in my projects, this array method should work just as well for me.

I would very much welcome any feedback or additional information around what I’ve shared here. If you have examples where my suggestion won’t work, I would love to know learn more about it. Feel free to comment here or @ me in the #swiftdialog channel of the Mac Admins slack.

As always, thanks to the Mac Admins community for sharing scripts and information so openly. The concepts in this post snowballed for me from a comment made by @Pico in the #bash Slack channel regarding the use of eval being avoidable by using arrays.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s