www.gibmonks.com




  Previous section   Next section

Practical Programming in Tcl & Tk, Third Edition
By Brent B. Welch

Table of Contents
Chapter 10.  Quoting Issues and Eval


Exploiting the concat inside eval

The previous section warns about the danger of concatenation when forming commands. However, there are times when concatenation is done for good reason. This section illustrates cases where the concat done by eval is useful in assembling a command by concatenating multiple lists into one list. A concat is done internally by eval when it gets more than one argument:

eval list1 list2 list3 ...

The effect of concat is to join all the lists into one list; a new level of list structure is not added. This is useful if the lists are fragments of a command. It is common to use this form of eval with the args construct in procedures. Use the args parameter to pass optional arguments through to another command. Invoke the other command with eval, and the values in $args get concatenated onto the command properly. The special args parameter is illustrated in Example 7-2 on page 82.

Using eval in a Wrapper Procedure.

Here, we illustrate the use of eval and $args with a simple Tk example. In Tk, the button command creates a button in the user interface. The button command can take many arguments, and commonly you simply specify the text of the button and the Tcl command that is executed when the user clicks on the button:

button .foo -text Foo -command foo

After a button is created, it is made visible by packing it into the display. The pack command can also take many arguments to control screen placement. Here, we just specify a side and let the packer take care of the rest of the details:

pack .foo -side left

Even though there are only two Tcl commands to create a user interface button, we will write a procedure that replaces the two commands with one. Our first version might be:

proc PackedButton {name txt cmd} {
    button $name -text $txt -command $cmd
    pack $name -side left
}

This is not a very flexible procedure. The main problem is that it hides the full power of the Tk button command, which can really take about 20 widget configuration options, such as -background, -cursor, -relief, and more. They are listed on page 391. For example, you can easily make a red button like this:

button .foo -text Foo -command foo -background red

A better version of PackedButton uses args to pass through extra configuration options to the button command. The args parameter is a list of all the extra arguments passed to the Tcl procedure. My first attempt to use $args looked like this, but it was not correct:

proc PackedButton {name txt cmd args} {
    button $name -text $txt -command $cmd $args
    pack $name -side left
}
PackedButton .foo "Hello, World!" {exit} -background red
=> unknown option "-background red"

The problem is that $args is a list value, and button gets the whole list as a single argument. Instead, button needs to get the elements of $args as individual arguments.

graphics/tip_icon.gif

Use eval with $args


In this case, you can use eval because it concatenates its arguments to form a single list before evaluation. The single list is, by definition, the same as a single Tcl command, so the button command parses correctly. Here we give eval two lists, which it joins into one command:

eval {button $name -text $txt -command $cmd} $args

The use of the braces in this command is discussed in more detail below. We also generalize our procedure to take some options to the pack command. This argument, pack, must be a list of packing options. The final version of PackedButton is shown in Example 10-3:

Example 10-3 Using eval with $args.
# PackedButton creates and packs a button.
proc PackedButton {path txt cmd {pack {-side right}} args} {
   eval {button $path -text $txt -command $cmd} $args
   eval {pack $path} $pack
}

In PackedButton, both pack and args are list-valued parameters that are used as parts of a command. The internal concat done by eval is perfect for this situation. The simplest call to PackedButton is:

PackedButton .new "New" { New }

The quotes and curly braces are redundant in this case but are retained to convey some type information. The quotes imply a string label, and the braces imply a command. The pack argument takes on its default value, and the args variable is an empty list. The two commands executed by PackedButton are:

button .new -text New -command New
pack .new -side right

PackedButton creates a horizontal stack of buttons by default. The packing can be controlled with a packing specification:

PackedButton .save "Save" { Save $file } {-side left}

The two commands executed by PackedButton are:

button .new -text Save -command { Save $file }
pack .new -side left

The remaining arguments, if any, are passed through to the button command. This lets the caller fine-tune some of the button attributes:

PackedButton .quit Quit { Exit } {-side left -padx 5} \
    -background red}

The two commands executed by PackedButton are:

button .quit -text Quit -command { Exit }-background red
pack .quit -side left -padx 5

You can see a difference between the pack and args argument in the call to PackedButton. You need to group the packing options explicitly into a single argument. The args parameter is automatically made into a list of all remaining arguments. In fact, if you group the extra button parameters, it will be a mistake:

PackedButton .quit Quit { Exit } {-side left -padx 5} \
    {-background red}
=> unknown option "-background red"

Correct Quoting with eval

What about the peculiar placement of braces in PackedButton?

eval {button $path -text $txt -command $cmd} $args

By using braces, we control the number of times different parts of the command are seen by the Tcl evaluator. Without any braces, everything goes through two rounds of substitution. The braces prevent one of those rounds. In the above command, only $args is substituted twice. Before eval is called, the $args is replaced with its list value. Then, eval is invoked, and it concatenates its two list arguments into one list, which is now a properly formed command. The second round of substitutions done by eval replaces the txt and cmd values.

graphics/tip_icon.gif

Do not use double quotes with eval.


You may be tempted to use double quotes instead of curly braces in your uses of eval. Don't give in! Using double quotes is, mostly likely, wrong. Suppose the first eval command is written like this:

eval "button $path -text $txt -command $cmd $args"

Incidentally, the previous is equivalent to:

eval button $path -text $txt -command $cmd $args

These versions happen to work with the following call because txt and cmd have one-word values with no special characters in them:

PackedButton .quit Quit { Exit }

The button command that is ultimately evaluated is:

button .quit -text Quit -command { Exit }

In the next call, an error is raised:

PackedButton .save "Save As" [list Save $file]
=> unknown option "As"

This is because the button command is this:

button .save -text Save As -command Save /a/b/c

But it should look like this instead:

button .save -text {Save As}-command {Save /a/b/c}

The problem is that the structure of the button command is now wrong. The value of txt and cmd are substituted first, before eval is even called, and then the whole command is parsed again. The worst part is that sometimes using double quotes works, and sometimes it fails. The success of using double quotes depends on the value of the parameters. When those values contain spaces or special characters, the command gets parsed incorrectly.

graphics/tip_icon.gif

Braces: the one true way to group arguments to eval.


To repeat, the safe construct is:

eval {button $path -text $txt -command $cmd} $args

The following variations are also correct. The first uses list to do quoting automatically, and the others use backslashes or braces to prevent the extra round of substitutions:

eval [list button $path -text $txt -command $cmd] $args
eval button \$path -text \$txt -command \$cmd $args
eval button {$path} -text {$txt} -command {$cmd} $args

Finally, here is one more incorrect approach that tries to quote by hand:

eval "button {$path}-text {$txt}-command {$cmd} $args"

The problem above is that quoting is not always done with curly braces. If a value contains an unmatched curly brace, Tcl would have used backslashes to quote it, and the above command would raise an error:

set blob "foo\{bar space"
=> foo{bar space
eval "puts {$blob}"
=> missing close brace
eval puts {$blob}
=> foo{bar space

      Previous section   Next section
    Top