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


Constructing Code with the list Command

It can be tricky to assemble a command so that it is evaluated properly by eval. The same difficulties apply to commands like after, uplevel, and the Tk send command, all of which have similar properties to eval, except that the command evaluation occurs later or in a different context. Constructing commands dynamically is a source of many problems. The worst part is that you can write code that works sometimes but not others, which can be very confusing.

graphics/tip_icon.gif

Use list when constructing commands.


The root of the quoting problems is the internal use of concat by eval and similar commands to concatenate their arguments into one command string. The concat can lose some important list structure so that arguments are not passed through as you expect. The general strategy to avoid these problems is to use list and lappend to explicitly form the command callback as a single, well-structured list.

The eval Command

The eval command results in another call to the Tcl interpreter. If you construct a command dynamically, you must use eval to interpret it. For example, suppose we want to construct the following command now but execute it later:

puts stdout "Hello, World!"

In this case, it is sufficient to do the following:

set cmd {puts stdout "Hello, World!"}
=> puts stdout "Hello, World!"
# sometime later...
eval $cmd
=> Hello, World!

In this case, the value of cmd is passed to Tcl. All the standard grouping and substitution are done again on the value, which is a puts command.

However, suppose that part of the command is stored in a variable, but that variable will not be defined at the time eval is used. We can artificially create this situation like this:

set string "Hello, World!"
set cmd {puts stdout $string}
=> puts stdout $string
unset string
eval $cmd
=> can't read "string": no such variable

In this case, the command contains $string. When this is processed by eval, the interpreter looks for the current value of string, which is undefined. This example is contrived, but the same problem occurs if string is a local variable, and cmd will be evaluated later in the global scope.

A common mistake is to use double quotes to group the command. That will let $string be substituted now. However, this works only if string has a simple value, but it fails if the value of string contains spaces or other Tcl special characters:

set cmd "puts stdout $string"
=> puts stdout Hello, World!
eval $cmd
=> bad argument "World!": should be "nonewline"

The problem is that we have lost some important structure. The identity of $string as a single argument gets lost in the second round of parsing by eval. The solution to this problem is to construct the command using list, as shown in the following example:

Example 10-1 Using list to construct commands.
set string "Hello, World!"
set cmd [list puts stdout $string]
=> puts stdout {Hello, World!}
unset string
eval $cmd
=> Hello, World!

The trick is that list has formed a list containing three elements: puts, stdout, and the value of string. The substitution of $string occurs before list is called, and list takes care of grouping that value for us. In contrast, using double quotes is equivalent to:

set cmd [concat puts stdout $string]

graphics/tip_icon.gif

Double quotes lose list structure.


The problem here is that concat does not preserve list structure. The main lesson is that you should use list to construct commands if they contain variable values or command results that must be substituted now. If you use double quotes, the values are substituted but you lose proper command structure. If you use curly braces, then values are not substituted until later, which may not be in the right context.

Commands That Concatenate Their Arguments

The uplevel, after and send commands concatenate their arguments into a command and execute it later in a different context. The uplevel command is described on page 130, after is described on page 218, and send is described on page 560. Whenever I discover such a command, I put it on my danger list and make sure I explicitly form a single command argument with list instead of letting the command concat items for me. Get in the habit now:

after 100 [list doCmd $param1 $param2]
send $interp [list doCmd $param1 $param2];# Safe!

The danger here is that concat and list can result in the same thing, so you can be led down the rosy garden path only to get errors later when values change. The two previous examples always work. The next two work only if param1 and param2 have values that are single list elements:

after 100 doCmd $param1 $param2
send $interp doCmd $param1 $param2;# Unsafe!

If you use other Tcl extensions that provide eval-like functionality, carefully check their documentation to see whether they contain commands that concat their arguments into a command. For example, Tcl-DP, which provides a network version of send, dp_send, also uses concat.

Commands That Use Callbacks

The general strategy of passing out a command or script to call later is a flexible way to assemble different parts of an application, and it is widely used by Tcl commands. Examples include commands that are called when users click on Tk buttons, commands that are called when I/O channels have data ready, or commands that are called when clients connect to network servers. It is also easy to write your own procedures or C extensions that accept scripts and call them later in response to some event.

These other callback situations may not appear to have the "concat problem" because they take a single script argument. However, as soon as you use double quotes to group that argument, you have created the concat problem all over again. So, all the caveats about using list to construct these commands still apply.

Command Prefix Callbacks

There is a variation on command callbacks called a command prefix. In this case, the command is given additional arguments when it is invoked. In other words, you provide only part of the command, the command prefix, and the module that invokes the callback adds additional arguments before using eval to invoke the command.

For example, when you create a network server, you supply a procedure that is called when a client makes a connection. That procedure is called with three additional arguments that indicate the client's socket, IP address, and port number. This is described in more detail on page 227. The tricky thing is that you can define your callback procedure to take four (or more) arguments. In this case you specify some of the parameters when you define the callback, and then the socket subsystem specifies the remaining arguments when it makes the callback. The following command creates the server side of a socket:

set virtualhost www.beedub.com
socket -server [list Accept $virtualhost] 8080

However, you define the Accept procedure like this:

proc Accept {myname sock ipaddr port} { ... }

The myname parameter is set when you construct the command prefix. The remaining parameters are set when the callback is invoked. The use of list in this example is not strictly necessary because "we know" that virtualhost will always be a single list element. However, using list is just a good habit when forming callbacks, so I always write the code this way.

There are many other examples of callback arguments that are really command prefixes. Some of these include the scrolling callbacks between Tk scrollbars and their widgets, the command aliases used with Safe Tcl, the sorting functions in lsort, and the completion callback used with fcopy. Example 13-6 on page 181 shows how to use eval to make callbacks from Tcl procedures.

Constructing Procedures Dynamically

The previous examples have all focused on creating single commands by using list operations. Suppose you want to create a whole procedure dynamically. Unfortunately, this can be particularly awkward because a procedure body is not a simple list. Instead, it is a sequence of commands that are each lists, but they are separated by newlines or semicolons. In turn, some of those commands may be loops and if commands that have their own command bodies. To further compound the problem, you typically have two kinds of variables in the procedure body: some that are to be used as values when constructing the body, and some that are to be used later when executing the procedure. The result can be very messy.

The main trick to this problem is to use either format or regsub to process a template for your dynamically generated procedure. If you use format, then you can put %s into your templates where you want to insert values. You may find the positional notation of the format string (e.g., %1$s and %2$s) useful if you need to repeat a value in several places within your procedure body. The following example is a procedure that generates a new version of other procedures. The new version includes code that counts the number of times the procedure was called and measures the time it takes to run:

Example 10-2 Generating procedures dynamically with a template.
proc TraceGen {procName} {
   rename $procName $procName-orig
   set arglist {}
   foreach arg [info args $procName-orig] {
      append arglist "\$$arg "
   }
   proc $procName [info args $procName-orig] [format {
      global _trace_count _trace_msec
      incr _trace_count(%1$s)
      incr _trace_msec(%1$s) [lindex [time {
         set result [%1$s-orig %2$s]
      } 1] 0]
      return $result
   } $procName $arglist]
}

Suppose that we have a trivial procedure foo:

proc foo {x y} {
    return [expr $x * $y]
}

If you run TraceGen on it and look at the results, you see this:

TraceGen foo
info body foo
=>
     global _trace_count _trace_msec
     incr _trace_count(foo)
     incr _trace_msec(foo) [lindex [time {
        set result [foo-orig $x $y]
     }1] 0]
     return $result

      Previous section   Next section
    Top