More than just an IDE

Ins and outs of Tcl/Tk

Tcl is a fairly easy-to-learn script language, but there are a few peculiarities that might be confusing for beginners as well as for advanced users. This page is meant to clarify its behavior; it is not meant as an introduction. We assume that you already have some basic knowledge about Tcl.
Hopefully, after having read this page, your will have a better understanding about Tcl and its behavior and will make less errors in the future.

  1. Tcl is a command language
  2. Why are control structures interpeted as commands?
  3. eval and {*}
  4. Can I use arbitrary variable names?
  5. The differences between "", {} and ()
  6. Using a " \" at the end of a line
  7. Control statements are evaluated as Tcl commands
  8. Where to place comment lines
  9. Lists
  10. Namespaces
  11. Stack levels
  12. Tcl/Tk and IPEnv


1. Tcl is a command language

This means that the first token is always interpreted as a command or procedure. As a consequence, Tcl cannot cope with assignments such as a = b + c. The solution was to define additional commands such as set and expr:

    set a [expr $b+$c]

Another consequence is, that — when applying the rules as strictly as Tcl does — control structures (if, switch, etc.), procedure definitions (proc) and comments (#) are all interpreted as commands. This interpretation has a few other implications. One is that comment blocks can cover several lines without the need of commenting out each line separately. For example:

    # this a some comment \
      this is some additional comment

is the same as:

    # this a some comment
    # this is some additional comment

Another implication it that placing comment blocks is more restrictive than in C:

    string first "a" $str 	# find "a" in string str
    ==> wrong # args

See Section 7 and Section 8 for more details.

2. Why are control structures interpeted as commands?

This is related to how Tcl scripts are evaluated:

Step 1: The parser starts by checking the script for unmatched quotation marks and brackets, and splits the script in groups of characters. These groups can be a group of characters without a single space, a variable (a dollar sign followed by a simple word), a text string (between quote characters: ""), a fixed string (curly braces: {}), or a command (square brackets: []).

Step 2: The commands are identified and are demarcated. The beginning of a command starts on a new line, or after a semicolon indicating the end of a command.

Step 3: Before executing a command, all the arguments are pre-processed: The variables, backslashes, commands and {*}-constructions are substituted.
Fixed strings are left unchanged, including the backslashes (except at the end of a line). Curly braces in a text string are interpreted as ordinary characters and do not have a special meaning. They do not shield off variables ($). Hence, it is not necessary to place an escape character (\) in front of an unmatched braces. An open square bracket, however, is always interpeted as the beginning of a command, even if it is unmatched, and requires an escape character to be retained.

    set x {{a $b \n} c}
    ==> {a $b \n} c           # x is a fixed string. all characters are left unchanged
    set x "{a $b \n} c"
    ==> can't read "b": no such variable
    set x "a{b"
    ==> a{b                   # no escape character required
    set x "a[b"
    ==> (waiting for a close bracket: "[" is read as the beginning of a command)

An unmatched curly brace in a fixed sting is ambiguous, because it coud be interpreted a a single character or as a delimiter. If you intend to use it as a character, then it must be preceded by an escape character.

Step 4: The commands are executed.

This cycle (step 1-4) is repeated recursively to resolve nested commands. Nested commands are always executed first.
Example
Suppose that x equals {1 2 3 4}.
puts [lrange $x 1 end] is parsed as:

    "puts [lrange $x 1 end]"      # this is the input string
    puts [lrange $x 1 end]        # interprets the string as a command and separates the arguments
    puts [lrange {1 2 3 4} 1 end] # substitutes variable x
    puts {2 3 4}                  # executes lrange
    ==> 2 3 4                     # executes puts

Steps 1-3 are executed without any knowledge of the commands. This mechanism has the advantage that the command name can also be a variable, or a user-defined procedure not yet available during compilation. Suppose, for example, that we have a variable that is used as a command name. If there is an escape character at the end of the line, this line and the next one are concatenated, independent of the command itself. So inevitably, both lines are always evaluated as a single command:

    set cmd "list"
    $cmd arguments of $cmd\
        continue on next line
    ==> arguments of list continue on next line

Now, if we set cmd to "#", it will still be executed in the same way:

    set cmd "#"
    $cmd arguments of $cmd\
        continue on next line
    ==> (empty line)

It is not recommended to apply these constructions arbitrarily, but they are essential to Tk and other Tcl extentions in which the widgets are dynamically create and their names are stored in Tcl variables. Widget names are nothing else Tcl command of a special kind.

Remarks
Note that although system error messages might look the same, they can be generated on a different level. In the following example, a fixed string is passed to subst. subst analyzes the string and throws an error at step 4:

    subst {$a b}
    ==> can't read "a": no such variable

If the same input would be passed as a text string, the error message is caused by the parser at step 3, but with the same results:

    subst "$a b"
    ==> can't read "a": no such variable

Expressions
If you use expr to compare strings, you can get confusing error messages. Suppose you want to evaluate an option and stored the result in a boolean as:

    set opt "HIDE"
    set show [expr $opt eq "SHOW" ]

This looks OK, but you will get the following error message:

    invalid bareword "HIDE"
    in expression "HIDE eq SHOW";
    should be "$HIDE" or "{HIDE}" or "HIDE(...)" or ...
        (parsing expression "HIDE eq SHOW")

There is nothing wrong with the syntax. But what does go wrong is the way that the equation is resolved. First, the parser substitutes $opt and drops the quotation marks around SHOW (step 3).
Next, expr tries to resolve the expression "HIDE eq SHOW", which is obviously wrong.
It is always saver to use curly braces to guarantee that the expression is passed to expr, correctly. Therefore, the preferred syntax is:

    set show [expr {$opt eq "SHOW"} ]

Bindings
The bind command is a special case, because the binding script is postponed till the right event occurs. The script is not compiled and is executed in the global namespace. This implies that the variables created in the script, automatically become global variables and are retained after the script has finished:

    bind $path <1> {
        set pos [%W index @%x,%y]
        # pos is a new global variable
    }

If you want to use local variables in the script, you can place quotation marks or the list or subst command. They will be replaced by constant values. For example:

    set path .f
    bind $path.c <1> "foo $path"

stores the binding script as: "foo .f". The local variable path will no longer be available.

3. eval and {*}

If you want to execute a command that is stored in a string, you can use eval. If you have, for example, an interface that reads the command arguments from an entry widget and the options from a combobox, the command can be executed as:

    eval cmd -opt $value $arguments

eval concatenates all arguments, i.e. it interprets each index of each argument as a separate argument of cmd. It may cause problems, if value is a text string that you would want to retain as a single argument. Putting $value between quote characters wouldn't make a difference, because it would still be interpreted as multiple arguments. This can be inhibited by adding the list command, either to group $value:
eval cmd -opt [list $value] $arguments
or the entire command:
eval [list cmd -opt $value $arguments]
What happens is that eval cancels out the list commands and restores the original command.
With Tcl version 8.5, a new syntax element was introduced solving this annoyance. Instead of using the eval command, now you can write "{*}" in front of the arguments that need to be split. The command above can be rewritten as:

    cmd -opt $value {*}$arguments

{*} can be used more than once in the same command and is not related to cmd:
Another example that you can test easily is:

    set str "strerr test"
    puts $str
    ==> stderr test
    eval puts $str
    ==> test

or use {*}:

    puts {*}$str
    ==> test

The eval command still remains useful, for example, if you want to execute a command line that is read from a console as a whole.

4. Can I use arbitrary variable names?

• A variable name can be an arbitrary string of characters. Names such as "5", "a+b" or even "\$x; {2" are completely legal, but not recommended. Be aware that a variable name can also be an empty string. As a beginnner, it might be confusing to get can get cryptic error messages such as: can't read "": no such variable, but it just means exactly what is says. Suppose for example, that y already exists as an empty string and mistakenly you wrote "set $y" instead of "set y", then you would get this error message.
The syntax is alright, but you will get a runtime error.

• You can concatenate variables by writing them without whitespaces in between: $str1$str2, or by using the concat command.
Instead of "set str3 $str1$str2" you could also write "concat str3 $str1 $str2" (if str3 is empty or does not exist).
If it is not clear where a variable name ends, you can use curly braces to demarcate the variable. All operators and punctuation character, except for "::" are natural delimiters. You will get an error message if you write "$z::", because z will be interpreted as a namespace. If your really want to substitute $z, you must write: "${z}::". However, you do can write "$z:" (a single colon).
You can also use append to concatenate text strings:

    set y $var
    append y "str"

or:

    append y $var "str"; # (assumes that y is empty or does yet not exist)

This is the same as writing:
set y ${var}str
Note that you cannot write set y $var"str", because the quote characters will be interpreted literally. If you want to use this construction to append a string containing whitespaces, then you must use escape characters instead:

    set var 0
    set y $var" 1 2 3"
    ==> wrong # args: should be "set varName ?newValue?"
    set y $var"\ 1\ 2\ 3"
    ==> 0" 1 2 3"
    set y $var\ 1\ 2\ 3
    ==> 0 1 2 3
    set y $var"
    ==> 0"

As a matter of course, using append is alway the better option.

5. The differences between "", {} and ()

() are only used to indicate an array index. Both "" and {} mark the beginning and the end of a string. Hence, unlike other languages, a string can cover multiple lines. The difference is that curly brackets inhibit substitution of variables, commands, and backslashes. This does not mean, however, that a fixed string is always passed as is. The exception is, when a backslash is used to concatenate lines. In this case, the backslash and the leading whitespaces on the next line are replaced by a single space:

    set v {a\
	   b}
    ==> a b

If v is to be interpreted as a list, the backslash is redundant.
If you do not use backslashes, all character between the quotation marks or braces — including the newlines — are retained:

    string length {a\
	   b}
    ==> 3
    string length {a
	   b}
    ==> 10 (this includes the newline)

Note that backslash substitution are the same for text strings and fixed strings:

    set v "a\
	   b"
    ==> a b

Curly braces cannot be used to define a string containing a single curly brace:

    set x {\{}
    ==> \{

If you do not insert a backslash, the character is read as an unmatched brace, but if you do so, the backslash is retained and you cannot get rid of it. This contradiction can only be solved afterwards, by using the subst command:

   set x {a\{b}
   => a\{b
   subst $x
   ==> a{b

subst will replace all backslashes, commands and variables. If you want to retain commands or variables in a fixed string, you must add the -nobackslash, -nocommands and/or -novariable option to the subst command to inhibit complete substitution:

    set x {a\{b $c}
    ==> a\{b $c
    subst $x
    ==> can't read "c": no such variable
    subst -novariable $x
    ==> a{b $c

Note that if a string contains an unmatched square bracket that is not preceded by a backslash, you will also need to add the -nocommand option, even if you do not intend to use the bracket a the beginning of a command:

    set x {a[b}
    ==> a[b
    subst $x
    ==> missing close-bracket
    subst -nocommand $x
    ==> a[b

6. Using a " \" at the end of a line

There are three cases where backslashes to escape a newline is meaningful.

• The first one is already mentioned above: it can be used to remove redundant leading spaces in a string covering more than a single line. Note that the whitespaces in front of the backslash are not affected. A backslash does not require a delimiter in font:

    set x {a \
	   b}
    ==> a  b (two spaces in between. not just one)

If you only want to get rid of the leading spaces, but still want to retain the newlines, you can write:

    set txt "\
        line1\n\
        line2\n\
        line3"

• The second case is if you want to continue a command on the next line:

    command arg1 arg2 \
        arg3 arg4 ...

Without a preceding backslash, arg3 will be interpreted as a new command.
This construction may also be hidden in an embedded command:

    array set test [list \
        index1 val1 \
        index2 $val2 \
        index3 "$val3 $val4" \
    ]

The backslashes are needed, not to group the text between the square brackets — brackets are always matched automatically — but to indicate that the list command does not stop at the end of the line. Hence, the following syntax is also correct (although not recommended):

     array set test [
	 list \
	 index1 val1 \
	 index2 $val2 \
	 index3 "$val3 $val4"
     ]
 

You could also swap the quotation marks and the list command, leaving out the backslashes:

     array set test "
        index1 val1
        index2 $val2
        index3 [list $val3 $val4]
    "

• The third case is a trick to let Tcl scripts behave differently under bash and in Tcl:

    #!/bin/sh
    # The following line is hidden in Tcl, but not in bash \
    exec wish $0 ${1+"$@"}
    # Tcl commands follow

If you source this script in wish, exec is hidden and the Tcl commands following are evaluated. If you run this script in bash, then exec is executed and launches the current script in wish. wish will be run in the foreground. This will block further evaluation of the Tcl script. If you exit wish, this script will also end. The advantage is that you can treat the script name as a bash command by setting the access mode, and do not need to call wish explicitly.

In all other cases, a continuation token is redundant. You may read, for example, constructions such as:

    package ifneeded tdbc 1.0.6 \
        "package require TclOO 0.6-;\
        [list load [file join $dir tdbc106t.dll]]\;\
        [list source [file join $dir tdbc.tcl]]"

The first backslash is required to indicate that the subsequent lines are part of the package command. But the other backslashes are redundant, because the quotation marks already group these lines. Having placed the backslashes, however, the newlines — acting automatically as command delimiters — were removed and you are forced to insert additional semicolons as replacements. The list commands in this example are needed to hold the file names together in a single string (see: the eval command).
A clearer syntax would be:

    package ifneeded tdbc 1.0.6 [subst {
        package require TclOO 0.6-
        load "[file join $dir tdbc106t.dll]"
        source "[file join $dir tdbc.tcl]"
    } ]

Note the additional whitespace between } and ] on the last line.

7. Control statements are evaluated as Tcl commands

Interpreting control statements as commands has a few consequences.

• Conditions and arguments need to be placed in curly braces instead of parentheses as in other languages.

• Sometimes you can see:

    proc fnc x {...}; # no curly braces around the argument

instead of:

    proc fnc {x} {...}

This is correct, but it is a sloppy style of programming and is not recommended because it makes the code less readable. Moreover, leaving out curly braces may not always be correct, as we saw in the expr example above.
The following code is executed correctly:

    if $x {
	# do something
    }

but for different reasons: $x is replaced by its value before the if command is evaluated (step 3). If you place curly braces around $x, then $x is evaluated by the if command itself (step 4). As a user you will not notice the difference. If we would execute the following code, however, leaving out the curly braces, we would end up in an infinite loop, printing "9" indefinitely:

    set i 10
    while [incr i -1] {
	puts $i
    }

The switch statement is programmed differently, and does NOT evaluate the flag. It is replaced in advance before the statement is executed, and curly braces are definitely wrong:

    switch {$x} {	# compares the string "$x" and not its value
	# switches
    }

• Why can't we just write:

    if {$a == str} {...}

instead of:

    if {$a == "str"} {...}

It is because the condition is passed as a fixed string to the if command and is evaluated as an expression and not as a list.
This means that conditions do not require a strict mathematical format, and can contain any expression or procdedure that returns an integer. For example:

    # x contains an integer
    if {$x-5} {
	puts {$x does not equal 5}
    } else {
	puts {$x equals 5}
    }

Similarly, instead of:

    set j 1
    for {set i 10} {$j != 0} {incr i -1} {
	puts "$i $j"
	set j $i
    }

we could write:

    for {set i 10; set j 1} {$j} {set j $i; incr i -1} {
	puts "$i $j"
    }

The first and third argument are evaluated as a code block, the second argument is evaluated as an expression.

All variables in expressions, as well as the output of commands and procedures, are automatically interpreted either as a string or a number. Placing additional quotation marks around variables and commands are redundant. Quotation marks or curly braces are needed to indicate that a group of characters must be interpreted as a string. This is also the case for macros (%W etc.) in bindings. If a binding is executed, the macros are substituted by their values. If the macro represents a string, it does not include the quotation marks. Hence, leaving out the quotation marks in an expression will raise an error:

    bind $path <Event> {
        if {"[foo %W]" == ".widget"} { # redundant quotation marks around []
                                       # no quotation marks needed around %W
            do something
        }
        if {%W == ".widget"} {         # syntax error. %W requires quotation marks
            do something
        }
    }

8. Where to place comment lines

• Positioning comment lines is more restrictive than in C. It might help to realize that comments are just special Tcl commands. You wouldn't write:

    string first "a" $str 	set str "test"

then why writing:

    string first "a" $str 	# set str "test"
    ==> wrong # args: should be "string first needleString haystackString ?startIndex?"

The comment line is parsed as the arguments of string. You can solve this problem by inserting a semicolon between the two commands.

Generally, you can always start a comment string on a new line. This can be anywhere in a script, but not inside an argument list or an argument, except in the body of a statement. This also includes the switch statement:

    switch $n { # comment here is ok
      # comment here is also ok
      1 {... commands ...}
      # you can comment out a case option
      # 2 {... commands ...}
    }

• The parser checks whether the command is complete and this includes a comment block. As a result, commenting out a control statement as follows goes wrong:

    # while {$x} {
	(do something)
    # }
    ==> unmatched open brace in list

You can add a backslash in front of the unmatched open braces in the comment lines. An unmatched close brace does not cause any problem.

9. Lists

A list is not an intrinsic property of a string; it is just another interpretation. This interpretation is made by the command, and not by the parser. For example, lindex interprets the first argument as a list, whereas split interprets the same string as a fixed text:

    lindex {{$a b} {c d}} 0;   # interprets argument 1 as a list
    ==> a $b

but:

    split {{$a b} {c d}};      # interprets the same argument as a string
    ==> \{\$a b\} \{c d\};     # list of four elements

• A list can be created by parts as an ordinary string. The interpretation of the string comes afterwards:

    set lst {"a $b"}
    append lst " \{"
    append lst "c \$d\}"
    puts $lst
    ==> "a $b" {c $d}
    # we can read lst as an ordinary list:
    lindex $lst 0
    ==> a $b
    lindex $lst 1
    ==> c $d

• The lists

    {"a $b" {c $d}}

and

    {{a $b} {c $d}}

are identical. The quotation marks and braces just demarcate the beginning and the end of the elements. In both cases, the variables are not substituted.

However, the lists

    {{a b} {c d}}

and

    {{a b}   {c    d}}

are identical, only as far as the base elements, a, b, c, and d are concerned; i.e.

    [lindex {{a b} {c d}} 1 1] == [lindex {{a b} {c   d}} 1 1]

but

    [lindex {{a b} {c d}} 1] != [lindex {{a b} {c   d}} 1]

This is because the output of lindex are strings, and they are not compaired as a list of elements.

• In addition to quotation marks and curly braces, you can also group the elements by using backslashes in front of the whitespaces in the elements. The string {a\ $b c\ $d} represents the same list from above.
Backslashes are automatically inserted if the string contains braces. For example:

    list "a { b"
    ==> a\ \{\ b

this, in contrast to:

    list "a b c"
    ==> {a b c}

The reason is that curly braces cannot be used to demarcate fixed strings containing unmatched open braces (see Section 5). The backslashes can be seen as a necessary internal representation. If you stick to the list commands, you might not even notice them at all:

    lindex [list "a { b" "c d" ] 0;	# same list as above
    ==> a { b

• Not all strings can be interpreted as lists. A list requires that the string does not contain unmatched braces or quotation marks.
If you write:

    subst {\[ \{ (}
    ==> [ { (

the output cannot be interpreted as a list. Note that "{\[ \{ (}" already is a list containing braces and brackets, and it does not require substitution to be interpreted correctly:

    lindex {\[ \{ (} 0
    ==> [
    lindex [subst {\[ \{ (} ] 0
    ==> unmatched open brace in list

Example
If you want to get the procedure names in a Tcl script by checking it line by line, then the following method will fail:

    if {[regexp {\s*proc } $line]} { # check the lines starting with "proc"
        # the next command will fail because $line is not complete:
        lappend procs [lindex $line 1]
    }

You can check completeness with "info complete $string".

• If a variables is substituted then the replacement will still be interpreted as a single group of characters. So, foo "$x" and foo $x are identical. This is not the case for bindings. For example:

    set str "Hello World"
    bind $path <1> "puts $str"

will evoke an error. The reason is that the binding script is evaluated during runtime, whereas substitution takes place in an earlier stage during compilation. Consequently, the executable script will read: "puts Hello World!". You can write:

    bind .b <1> [list puts $str]

to avoid this problem. The list command is useful only if the script contains local variables. Otherwise, it is always better to use curly braces. Write:

    bind Tree <FocusIn> {
        after idle BWidget::refocus %W %W.c
    }

instead of:

    bind Tree <FocusIn> [list after idle {BWidget::refocus %W %W.c}]

• Tcl contains a set of commands, specially designed to process lists. They are:

    lappend lassign lindex linsert llength lmap lrange lreplace lreverse lsearch lset lsort

The first argument of these commands always represents a list.
lappend and lset read the list by name, the other commands read the list as a variable or as a literal list.

lappend and lset return the modified list, both as an argument and as the return value:

    set lst2 [lappend lst1 elem]
    # lst1 and lst2 are identical. lst1 is also modified

The list commands that modify the list, reformat the input string according to a standard form, using curly braces:

    set lst {"a $b"    {c $d}  }
    lappend lst "e f"
    ==> {a $b} {c $d} {e f}

• There are four Tcl commands that create new lists. They are: concat, lrepeat, split, and list.

10. Namespaces

A namespace is created as namespace eval name script. The script can be used to create namespace variables and namespace procedures, or execute commands, or can be empty.
• Once a namespace is created, you can dynamically add and delete as many procedures and variables as needed. This can be done in another namespace eval, or you can use set and proc, writing the namespace name expicitly. The following scripts are identical:

    namespace eval ns {
        # the commands are executed inside a namespace
        variable x 10;    # creates ns::x
        set y 0;          # creates ns::y
        proc fnc {} {     # creates ns::fnc
            puts "1   creates fnc2 in namespace [namespace current]"
            proc fnc2 {} {
                # this is ::ns::fnc2
                set frameinfo [info frame [info frame]]
                puts "2   runs: [dict get $frameinfo proc]"
                namespace eval :: {
                    puts "    evaluates script in global namespace\n   \
                        creates fnc2 in namespace [namespace current]"
                    proc fnc2 {} {
                        # this is ::fnc2
                        set frameinfo [info frame [info frame]]
			puts "3   runs: [dict get $frameinfo proc]"
                    }
                }
            }
        }
        namespace eval ns2 {puts [list creates namespace [namespace current]]}
        puts "Calling ns::fnc"
        fnc;              # executes ns::fnc; creates ns::fnc2
        puts "Calling ns::fnc2"
        fnc2;             # executes ns2::fnc2; creates ::fnc2
        puts "Calling ::fnc2"
        ::fnc2;           # executes ::fnc2
    }

and:

    # all commands are executed in the global namespace. This only shows the framework
    namespace eval ns {}
    set ns::x 10
    set ns::y 0
    proc ns::fnc {} {
        puts "proc ::ns::fnc"
        proc fnc2 {} {puts "proc ::ns::fnc2"}
        namespace eval :: {
            proc fnc2 {} {puts "proc ::fnc2"}
        }
    }
    namespace eval ns::ns2 {puts "namespace ::ns::ns2"}
    ns::fnc
    ns::fnc2
    fnc2

• All procedures and variables that are created by set, array, source, and inside proc and "namespace eval" are added to the current namespace. If this is not what you meant, then it can be overruled by calling "namespace eval {} {... commands ...}" or "namespace eval :: {...}" from within another namespace.

• The variable command declares a variable in a namespace, but if you do not assign a value to the variable, it cannot be accessed and unset:

    namespace eval ns {variable var}
    info vars ns::*
    ==> ::ns::var    # i.e. ns::var exists
    set ns::var
    ==> can't read "ns::var": no such variable
    unset ns::var
    ==> can't unset "ns::var": no such variable

• Global variables and procedures are stored in the global namespace. Its name is the empty string. Write ::varname to access a global variable from within another namespace or procedure, or to declare it as a global variable. You can also use the global statement and leave out ::.

• An callback event is always executed in the global namespace. If you create a variable in a binding, it will be retained as a global variable. Moreover, you can access global variables without adding ::. For example:

    set ::globvar 0
    bind $w <Button-1> {
        incr globvar
        puts " \$globvar and \$::globvar are the same variables: $globvar $::globvar"
    }

11. Stack levels

If you enter a procedure you will go to the next stack level. This is also the case inside a "namespace eval". The top level is the global namespace, and is level 0. The Tcl commands, upvar and uplevel, give full access to other stack levels.
For example, if you want to know the namespace from which a procedure is called, from within the procedure itself, you can write: "set ns [uplevel ::namespace current]".

uplevel
uplevel concatenates its arguments and executes the string in the stack frame indicated. The default uplevel is 1.
You may see constructions such as: "set ns [uplevel 1 [list ::namespace current]]", but both the uplevel count "1" and the list command are redundant and, for the sake of clearity, should be left out. Moreover, you can apply the same rules for uplevel as for eval, and use the list command only for arguments containing multiple words or a list that must be retained. For example, instead of:

    uplevel 1 [list catch [lreplace $args 0 0 [lindex $cmds $n]] ::result ::opt]

(meaning: replace the command argument in args by the n-th element is a list of commands, and execute the command in the stack level above),
you can write:

    uplevel catch [list [lreplace $args 0 0 [lindex $cmds $n]]] ::result ::opt

Note that the list command is needed, even if you would execute the command by steps:

    set cmd [lreplace $args 0 0 [lindex $cmds $n]]
    uplevel catch [list $cmd] ::result ::opt

This is due to how concatenation in Tcl is defined:

    set var val1; set lst {val2 val3}
    concat $var $lst
    ==> val1 val2 val3
    concat $var [list $lst]
    ==> val1 {val2 val3}

upvar
The most common implementation of upvar is to pass procedure arguments by pointer. Actually, there are no pointers in Tcl, but they are simulated by upvar as follows:

    proc copy {var1 var2} {
        upvar $var2 lvar2
        set lvar2 $var1
    }

    set var1 10
    copy $var1 var2
    set var2
    ==> 10

upvar links a local variable lvar2 with a variable in the other stack frame. upvar does not create a new variable, and $arg2 (i.e. var2) does not already need to exist before copy is called: setting lvar2 in the copy procedure will also change or create var2.
You could also write, seemingly with the same results:

    proc copy {var1 var2} {
        upvar var2 lvar2
        set lvar2 $var1
    }

    set var1 10
    copy $var1 var2
    set var2
    ==> 10

But then it will always create a variable with the same name, and argument var2 in the copy definition merely acts as a dummy and is not referenced at all.

12. Tcl/Tk and IPEnv

IPEnv is a multilingual IDE, but it has strong roots in Tcl/Tk. If you are programming in Tcl/Tk, you can profit from all the advantages. that IPEnv has to offer: sophisticated automatic code completion, clever goto and find functions and it recognizes Tcl procedure prototypes. You can load your own plug-ins written in Tcl/Tk.
IPEnv has a built-in multi-threaded console in which you can execute Tcl/Tk commands, interrupt Tcl commands and system commands as well as, run Tcl scripts in the background and much more.
The advantages of IPEnv over wish:

  • IPEnv has an advanced Tcl language editor;
  • You can change the working directory with a mouse click;
  • IPEnv has a mature interactive console:
  • Tcl scripts can be interrupted [Ctrl+C] and you can run scripts in the background [&];
  • IPEnv is multi-threaded. You can continue editing while a Tcl script is running, and system commands can be interrupted and do not block the interface and can be interrupted;
  • Tcl output can be redirected to a file;
  • Commands such as ls generate hyperlinked output, giving a easy access to the files;
  • IPEnv does not close down on exit commands in a script;
  • IPEnv has a fully-integrated editor, console and file browser;
  • IPEnv gives direct access to TclProDebugger and automatically loads the scripts in the debugger.

Just give it a try and you will realize what you are missing.
IPEnv comes with the latest stable version of Tcl/Tk, and includes several additional Tcl extensions, including BLT and TclProDebugger.