Skip to content

Instantly share code, notes, and snippets.

@brabect1
Last active January 20, 2024 08:14
Show Gist options
  • Save brabect1/d96f5653c56f440e9ec1aac096334196 to your computer and use it in GitHub Desktop.
Save brabect1/d96f5653c56f440e9ec1aac096334196 to your computer and use it in GitHub Desktop.
Some advanced Tcl topics #tcl

Advanced Tcl Topics

This gist covers some areas that go beyond basic Tcl use. These are useful for anyone embarking on more complex Tcl programming. Some of the topics are fairly simple but very little advertised in Tcl snippets or tutorials.

Tcl is an untyped language where every variable is a string, or a list, depending how you look at it. Yet many oprations and decisions in programming depend on knowing the value type to interpret it correctly.

Take booleans. In some code you see set b 1 and in some set b true. For some types, the Tcl string comes with a string is sub-command to let you test the type. So you have string is boolean, string is true and string is false to help you detect if a particular value is indeed a Boolean and if it is interpreted as true or false.

It may be a surprise to some that all of 1, true, yes and on (irrespective of case, hence also true, Yes, ON, oN and so on) are indeed true. Similarly for 0, false, no and off. The complete list is in Tcl_GetInt documentation.

foreach c {boolean true false} {
    puts "string is ${c}";
    foreach b {0 1 2 -1 true True TRUE yes Yes YES ok Ok OK on ON no No false False FALSE off Off OFF} {
        puts "\t$c\(${b}\)=[string is $c $b]";
    }
}

Namespace is a mechanism to create a separate, named scope/context to avoid procedure/command and variable name colisions. It also sets the foundation for modularizing Tcl code.

The following example aggregates incr and decr commands under myspace:: context:

namespace eval myspace {
    proc incr {val} {
        return [expr ${val} + 2];
    }

    proc decr {val} {
        return [expr ${val} - 1];
    }
}

puts [myspace::incr 1]; # =3
puts [myspace::decr 1]; # =0

Next example shows that namespace changes visibility/context of variables. The same holds, of course, for commands.

variable a 12;

namespace eval myspace {
    variable a 4;
}

puts ${a};           # =12
puts ${myspace::a};  # =4

The body of namespace eval <name> { ... } can be any Tcl code. While aggregating commands and variables is the obvious case, you can call/execute commands there with no restrictions:

namespace eval myspace {

    puts "@1: [incr 3]"; # --> @1: 1

    proc incr {val} {
        return [expr ${val} + 2];
    }

    puts "@2: [incr 3]"; # --> @2: 5

    ...
}

The snippet above also shows that inside namespace eval <name> body the context automatically switches the local context to <name>::. However, the global namespace, ::, is still visible. Hence the first call of incr executes ::incr (as the local myspace::incr has not been defined yet at that point), while the other incr call aliases to myspace::incr.

Note

The fact that a local context gets prioritized over the global one lets you override the global context, such as overriding global commands. No need to tell you have to be careful about implications. For example:

variable a 12;
namespace eval override {
    proc set {a b} {
        puts "${a}=${b}";
    }

    set a 3;    # --> a=3
    puts ${a};  # --> 12
}

You can create hierarchy of namespaces:

# create namspace within namespace
namespace eval foo {
    namespace eval bar {
        proc squeak {} {
            puts "squeaking ...";
        }
    }
}

# create hierarchical namespace directly
namespace eval bar::foo {
    proc croak {} {
        puts "croaking ...";
    }
}

foo::bar::squeak
bar::foo::croak

With namespaces, it is possible to package one's code for reuse and distribute it to others. The contemporary approach to packaging Tcl mode is through Tcl Modules.

Note

The older packaging technique through Tcl Packages is less preferred for Tcl-only code.

You surely noticed some Tcl core commands have sub-commands, such as dict has dict set, dict get and others. And maybe you wondered how to implement similar for a command of yours. That is what Encsembles do. Here I cover some basics and then have Tcl Namespace Ensemble gist with more details.

Creating a command foo with subcommands foo bar and foo baz looks like below. It is simply a matter of crating the foo namespace, exporting its procedures bar and baz, and calling namespace ensemble create in the foo's context.

namespace eval foo {
    namespace export bar baz;

    proc bar {} {
        puts "[lindex [info level 0] 0]";
    }

    proc baz {} {
        puts "[namespace current]::baz";
    }

    proc bax {} {
        puts "[namespace current]::bax";
    }

    namespace ensemble create; #                       <--- simplest way of creating an ensemble
}

foo::bar; # --> foo:bar
foo::baz; # --> ::foo::baz
foo::bax; # --> ::foo::bax

foo bar;  # --> ::foo::bar
foo baz;  # --> ::foo::baz
foo bax;  # yields an ERROR as `bax` is not exported

Most know how to create a command/procedure with fixed arguments: proc mycmd {a {b 1}} { puts "a=${a}\nb=${b}"; } Tcl, like some other languages, allow a variable length arguments through the special args argument name. Note that args has to come last (at least until TIP 288 gets implemented).

proc demo {first {second "none"} args} {
       puts "$first X $second X $args"
}

demo one                        ; # --> one X none X
demo one two                    ; # --> one X two X
demo one two three four five    ; # --> one X two X three four five

The args really becomes a list and may be processed like that:

proc mymsg {args} {
    array set _opts { wrn 0 err 0 }
    set i 0;
    while {$i < [llength $args]} {
        switch -glob -- [lindex $args $i] {
            -warn {
                set _opts(wrn) 1;
            }
            -err {
                set _opts(err) 1;
            }
            -- { incr i 1; break; }
            -* {
                error "Unknown option '[lindex $args $i]'!";
            }
            default {
                break;
            }
        }
        incr i 1;
    }; # while

    if {${_opts(err)}} {
        puts "ERROR: [lrange $args ${i} end]";
    } elseif {${_opts(wrn)}} {
        puts "Warning: [lrange $args ${i} end]";
    } else {
        puts "[lrange $args ${i} end]";
    }
}

mymsg hello world       ; # --> hello world
mymsg -err hello world  ; # --> ERROR: hello world
mymsg -warn hello world ; # --> Warning: hello world

Sometimes you may wish to just pass args downstream to another command. However, beware that it already became a list; no longer separate values:

proc a {args} {
    foreach a ${args} {
        puts ${a};
    }
}

proc b {args} {
    a ${args};
}

a 1 2 3 ; # --> 1
          # --> 2
          # --> 3
b 1 2 3 ; # --> 1 2 3             <-- not what we wanted!

The trick is to ... (see Interpreting Lists):

proc c {args} {
    a {*}${args};
}

c 1 2 3

A typical problem encountered with commands taking variable number of arguments is this: If I collect arguments into a list, how do I seperate them back into individual values for the command to call? Consider you have a key-value represented like key.subkey.subsubkey=value and you want to set it into a Tcl dictionary. You might start like this:

set rec key.subkey.subsubkey=value;
lassign [split $rec "="] key val;
dict set d [split ${key} "."] ${val};
puts ${d}; # --> {key subkey subsubkey} value           <-- but you wanted `key {subkey {subsubkey value}}`

Aha! Use eval, Luke, you say:

...
set cmd "dict set d [split ${key} "."] ${val}";
eval ${cmd};
puts ${d}; # --> key {subkey {subsubkey value}}

That usually works but may get ugly if you have nested lists or quoted strings. It is generally fragile (see e.g. Eval and double substitution in Tcl Wiki's eval or in TIP 157):

set b {the total is $20}
eval {set a $b}; puts "a=${a}"; unset a;       # --> a=the total is $20
eval "set a $b";                               # error

Hence Tcl 8.5 came with {*} that is documented in Tcl semantics' rule No.5, argument expansion. As put here:

{*} promotes the items in a list to individual arguments of the current command. For example, set {*}[list greeting hello] is equivalent to set greeting hello.

Getting back to our problem, this is how to solve it:

...
dict set d {*}[split ${key} "."] ${val};
puts ${d}; # --> key {subkey {subsubkey value}}

Well, normally it is try-catch but the try (and finally) part first appeared in Tcl 8.6. As in other languages, you have options to sanity check all agruments before calling a command or just give it a try and deal with the outcome if that command error's out.

Note

This is not to say that you should avoid testing arguments. It is just that you have to compromise code clarity with likelyhood of raising errors.

Also note that Tcl's catch does not act only on ``error``s but intercepts the interpreter's processing [Tcl wiki]:

catch is used to intercept the return code from the evaluation of script, which otherwise would be used by the interpreter to decide how to proceed: Whether an error occurred, whether to break out of the current loop, whether to jump to the beginning of the loop and enter it again, whether the caller itself should return, etc.

catch is supposed to be robust [Tcl documentation]:

The catch command calls the Tcl interpreter recursively to execute script, and always returns without raising an error, regardless of any errors that might occur while executing script.

The typical use of catch is in combination with if:

if {[catch {open $someFile w} fid]} {
    puts stderr "Could not open $someFile for writing\n$fid"
    exit 1
}

If the <script> in catch {<script>}, catch raises an error, catch will return a non-zero integer value corresponding to the exceptional return code returned by evaluation of script. Hence code in the if statement may react to the exceptional situation.

The full synopsys of the command is catch script ?resultVarName? ?optionsVarName?, where the resultVarName variable would get set to the result of the script if it completed normally; on error, the variable will contain the error message. The optionsVarName is used for more complex cases; it generally contains detailed information on the result of script's execution. For details see Tcl documentation.

Below is an example of how to catch the error, do some cleanup actions and re-throw the exception/error upstream in the call stack:

if {[set err_code [catch {
        ... # script
    } err_msg err_opts]]} {
    set einfo $::errorInfo;
    set ecode $::errorCode;
    if {${quiet} == 0} {
        # re-throw the error/exception
        return -code ${err_code} -errorcode ${ecode} -errorinfo ${einfo} ${err_msg};
    }
    return {}; # quietly ignore the exception
}

Note

As mentioned, Tcl 8.6 added the try command (and throw, too) that has more convenient syntax than catch. An example to re-throw the error upstream in the call stack looks like follows with try [Tcl Wiki]:

try {
    set f [open /some/file/name]
} on error {result options} {
    puts {something broke!}
    return -options $options $result
}

The Tcl apply command is somewhat strange, at first glance. Its synopsys looks like apply {args body ?namespace} ?val1 ?val2 ... and the documentation says the apply command executes the body script by applying values val1, val2 and on to args variables (used in the body). Sounds weird? Then see the example:

puts [apply {{a b} {return [expr $a + $b];}} 1 2]; # --> 3

The above example applies return [expr $a + $b]; on variables a and b giving them values 1 and 2; hence it computes a sum 1 + 2. Still puzzled why would one use apply instead of creating a sum procedure? The answer is just because of that; you don't want to create a named procedure when you are going to execute the body just once.

So what apply really does is creating an anonymous context (with its own stack frame) and executes the body there. Any variables declared in that context are local and get destroyed with the anonymous stack frame. Hence if you just want to execute some code and do not worry about unsetting all intermediate variables you created, just use apply:

apply {{} {
    set a 1;
    set b 2;
    puts [expr $a + $b];
}}
puts "[info exists a]";

Remember the optional namespace from apply's synopsis? If given, the apply will be executed in that namespace's context (as a new anonnymous context there, again). Hence it lets you inject an anonymous code into that namespace.

Note

There are some stricter rules about using variables outside of the apply's context [Tcl documentation]:

Global variables can only be accessed by invoking the global command or the upvar command. Namespace variables can only be accessed by invoking the variable command or the upvar command.

Hence for example:

set g 12;
apply {{} {global g; puts "$g";}}; # --> 12
  • Like some other scripting languages, Tcl can be bent to do weird things. There is an example on Tcl Wiki how to create new do ... while control structure.
  • ::tcl::mathop and its counterpart ::tcl::mathfunc are namespaces that provide expr-like math oprators and functions operating on varying number of arh=guments. For example: puts [::tcl::mathop::+ 1 2 3 4], or if using a list puts [::tcl::mathop::* {*}[list 1 2 3 4]].
puts [apply {{a b} {return [expr $a + $b];}} 1 2]; # --> 3
apply {{} {
set a 1;
set b 2;
puts [expr $a + $b];
}}
puts "[info exists a]"; # --> 0
puts "[info exists b]"; # --> 0
set g 12;
apply {{} {global g; puts "$g";}}; # --> 12
foreach c {boolean true false} {
puts "string is ${c}";
foreach b {0 1 2 -1 true True TRUE yes Yes YES ok Ok OK on ON no No false False FALSE off Off OFF} {
puts "\t$c\(${b}\)=[string is $c $b]";
}
}
namespace eval foo {
namespace export bar baz;
proc bar {} {
puts "[lindex [info level 0] 0]";
}
proc baz {} {
puts "[namespace current]::baz";
}
proc bax {} {
puts "[namespace current]::bax";
}
namespace ensemble create; # <--- simplest way of creating an ensemble
}
foo::bar; # --> foo:bar
foo::baz; # --> ::foo::baz
foo::bax; # --> ::foo::bax
foo bar; # --> ::foo::bar
foo baz; # --> ::foo::baz
foo bax; # yields an ERROR as `bax` is not exported
set rec key.subkey.subsubkey=value;
lassign [split $rec "="] key val;
dict set d [split ${key} "."] ${val};
puts ${d}; # --> {key subkey subsubkey} value <-- but you wanted `key {subkey {subsubkey value}}`
unset d;
set cmd "dict set d [split ${key} "."] ${val}";
eval ${cmd};
puts ${d}; # --> key {subkey {subsubkey value}}
unset d;
dict set d {*}[split ${key} "."] ${val};
puts ${d}; # --> key {subkey {subsubkey value}}
# showing some problems with `eval` (from "Eval and double substitution"
# in https://wiki.tcl-lang.org/page/eval)
set b {the total is $20}
set a $b; puts "a=${a}"; unset a; # --> a=the total is $20
eval [list set a $b]; puts "a=${a}"; unset a; # --> a=the total is $20
eval {set a $b}; puts "a=${a}"; unset a; # --> a=the total is $20
eval "set a $b"; # error
variable a 12;
namespace eval myspace {
variable a 4;
puts "@1: [incr 3]"; # --> @1: 1
proc incr {val} {
return [expr ${val} + 2];
}
puts "@2: [incr 3]"; # --> @2: 5
proc decr {val} {
return [expr ${val} - 1];
}
}
puts [myspace::incr 1]; # =3
puts [myspace::decr 1]; # =0
puts ${a};
puts ${myspace::a}
namespace eval override {
proc set {a b} {
puts "${a}=${b}";
}
set a 3;
puts ${a};
}
# create namspace within namespace
namespace eval foo {
namespace eval bar {
proc squeak {} {
puts "squeaking ...";
}
}
}
# create hierarchical namespace directly
namespace eval bar::foo {
proc croak {} {
puts "croaking ...";
}
}
foo::bar::squeak
bar::foo::croak
proc demo {first {second "none"} args} {
puts "$first X $second X $args"
}
demo one ; # --> one X none X
demo one two ; # --> one X two X
demo one two three four five ; # --> one X two X three four five
proc mymsg {args} {
array set _opts { wrn 0 err 0 }
set i 0;
while {$i < [llength $args]} {
switch -glob -- [lindex $args $i] {
-warn {
set _opts(wrn) 1;
}
-err {
set _opts(err) 1;
}
-- { incr i 1; break; }
-* {
error "Unknown option '[lindex $args $i]'!";
}
default {
break;
}
}
incr i 1;
}; # while
if {${_opts(err)}} {
puts "ERROR: [lrange $args ${i} end]";
} elseif {${_opts(wrn)}} {
puts "Warning: [lrange $args ${i} end]";
} else {
puts "[lrange $args ${i} end]";
}
}
mymsg hello world ; # --> hello world
mymsg -err hello world ; # --> ERROR: hello world
mymsg -warn hello world ; # --> Warning: hello world
proc a {args} {
foreach a ${args} {
puts ${a};
}
}
proc b {args} {
a ${args};
}
a 1 2 3 ; # --> 1
# --> 2
# --> 3
b 1 2 3 ; # --> 1 2 3
proc c {args} {
a {*}${args};
}
c 1 2 3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment