The Tcl upvar
[upvar
manual] and uplevel
[uplevel
manual] commands allow a procedure
to modify the local variables of any procedure on the
call stack. They are very powerful, and it is easy to use
them to create code that is very hard to understand and
maintain. This page contains some guidelines for using them.
I posted the original version[Original version] of these guidelines to the
AOLserver mailing list[AOLserver list archive].
Please send me[E-mail me] comments, corrections, and additions. Note that these are just guidelines; advanced Tcl programmers will occasionally have reason to violate them.
Guidelines for upvar
-
Only access variables in the caller's environment, or in the global environment. In other words, always pass either "
1
" or "#0
" as the first argument to upvar. Otherwise you create a dependency on the program's call path. (That's a number one, not a lower-case "L".) -
Only access variables using names that the caller passed in; do not make assumptions about the caller's variable names. (This does not apply if you're accessing a global variable.) Otherwise you create a dependency on the caller's variable names. For example, do this:
proc goodExample {a b qName rName} { upvar 1 $qName quotient upvar 1 $rName remainder set quotient [expr {$a / $b}] set remainder [expr {$a % $b}] }
Do not do this:
proc badExample {a b} { upvar 1 quotient quotient upvar 1 remainder remainder set quotient [expr {$a / $b}] set remainder [expr {$a % $b}] }
-
Use upvar if you need to access an array in the caller's environment. For example, this procedure takes the name of an array in the caller's environment and returns a list of the array values:
proc arrayValues {arrayName} { upvar 1 $arrayName a set values {} foreach {name value} [array get a] { lappend values $value } return $values } array set myarray {george bush william clinton ronald reagan} set v [arrayValues myarray] # $v = {reagan bush clinton}
-
Use upvar if you need to set multiple variables in the caller's environment. For example, this procedure takes a list and any number of variable names in the caller's environment, and sets the variables to successive elements from the list:
proc unlist {list args} { foreach value $list name $args { if {![string length $name]} return upvar 1 $name var set var $value } } unlist {scrambled corned buttered} eggs beef toast # $eggs = scrambled # $beef = corned # $toast = buttered
-
(Thanks to Kevin Kenny for reminding me of this usage.) Use upvar if you want to create an alias for a global (or namespace) variable. For example, this procedure retrieves a URL and returns a list containing the HTTP status code and the URL entity:
proc wget {url} { set token [::http::geturl $url] upvar #0 $token state return [list [lindex $state(http) 1] $state(body)] }
The
http::geturl
command returns a token such as "::http::7
". The token is actually the name of an array variable in thehttp
namespace, and that array is part of thehttp
package's public interface. Withoutupvar
, accessing an element of the array would require an awkward expression such as "[set ${token}(element-name)]
" instead of the more natural "$state(element-name)
". -
Don't use upvar for anything else.
Guidelines for uplevel
-
Only access the caller's environment. In other words, always pass "1" as the first argument to uplevel. Otherwise you create a dependency on the program's call path. (That's a number one, not a lower-case "L".)
-
In an
uplevel
, only run code that the caller provided, or code that accesses no local variables. Otherwise you create a dependency on the caller's variable names. Remember that inside anuplevel
block, you cannot access your own local variables; only the caller's local variables are visible. -
Only use
uplevel
to create new control structures. The following example creates atry
command that provides a simple error-catching mechanism. It also usesupvar
, and follows all of theupvar
guidelines.proc try {block args} { if {[llength $args] % 3 != 0} { error "usage: try block [pattern resultvar block] ..." } set code [catch { uplevel 1 $block } result] if {$code != 1} { return -code $code $result } foreach {pattern name handler} $args { if {[string match $pattern $result]} { upvar 1 $name var set var $result uplevel 1 $handler return } } error $result $::errorInfo $::errorCode } try { expr {1 / 0} } "divide by zero" e { puts "got divide-by-zero error" } * e { puts "unexpected error: $e" } # prints "got divide-by-zero error"