trace

trace , a built-in Tcl routine, manages traces on variables and routines.

See Also

An equation solver
Illustrates the use of trace to update dependent resources.
Traces
Examples and discussion.
Tracing inappropriate variable access
Whole-Script Tracing, by DKF
An example of data objects
AM: Using trace I implemented an idea by George Howlett, the author of BLT.
Tiny Excel-like app in plain Tcl/Tk
Uses traces to implement cascading recalculation of the values of spreadsheet cells.
ycl eval untraced
Evaluates a script with traces temporarily removed from a routine.

Synopsis

trace option ?arg arg ...?

Documentation

official documentation

Description

Once a trace is defined on a routine, the trace follows the routine even if it is renamed. If a traced routine is renamed and a new command having the original name is created, the trace remains bound to the renamed command, not the new command.

trace add adds a trace even if it identical to an existing trace. When there are multiple identical traces, trace remove removes only one of them.

Example use cases for trace:

  • Communicate between parts of a GUI and the internal state of the app. (Simplified MVC, observer). In general Communicate between different parts of an app without coupling them strongly.
  • Compute simple constraints for a number of variables ("if this flag is on and that one is on, then no other is allowed to be set", and some such).
  • bind Canvas text items to a variable, effecting dynamic updates
  • Debug - call a proc when a variable is modified (detect setting from wrong routine).
  • Trace works in Itcl Itcl trace but not quite trivially.
  • Clean up upon deallocation of a resource.

Order of Processing

JCW:

proc tellme {id a e op} {
    puts "  $id a=$a e=$e op=$op\
        ax=[info exists ::$a] ex=[info exists ::${a}($e)]"
}

proc do args {
    puts <$args>
    uplevel 1 $args
}

trace add variable a {write unset} {tellme array}
trace add variable a(1) {write unset} {tellme element}

puts [trace info variable a]
puts [trace info variable a(1)]

do set a(0) zero
do set a(1) one
do set a(2) two
do unset a(0)
do unset a(2)
do unset a

# output is:
#
#   {wu {tellme array}}
#   {wu {tellme element}}
#   <set a(0) zero>
#     array a=a e=0 op=w ax=1 ex=1
#   <set a(1) one>
#     array a=a e=1 op=w ax=1 ex=1
#     element a=a e=1 op=w ax=1 ex=1
#   <set a(2) two>
#     array a=a e=2 op=w ax=1 ex=1
#   <unset a(0)>
#     array a=a e=0 op=u ax=1 ex=0
#   <unset a(2)>
#     array a=a e=2 op=u ax=1 ex=0
#   <unset a>
#     array a=a e= op=u ax=0 ex=0
#     element a=a e=1 op=u ax=0 ex=0

Variable Trace Arguments vs Upvar

Be careful how you use the name1 and name2 parameters provided to a variable trace. It's tempting to write a single handler which tracks access to multiple variables thus:

proc traceHandler {name _ op} {  ;# we ignore name2 for a non-array trace
    puts "trace: variable {$name} $op"
}
unset -nocomplain x y
trace add variable x {write} traceHandler
trace add variable y {write} traceHandler
incr x
#  trace: variable {x} write
incr y
#  trace: variable {y} write

So far, so good, but look what happens when the variable is aliased with upvar:

proc loop {_var first limit script} { ;# https://wiki.tcl-lang.org/2025
    upvar 1 $_var var
    for {set var $first} {$var < $limit} {incr var} {
        uplevel 1 $script
    }
}
loop x 0 2 {
  ;# no loop body - we just want to see what happens to x
}
#  trace: variable {var} write
#  trace: variable {var} write
#  trace: variable {var} write

The trace fires as expected ... but the name is now coming from somebody else's code. This is sufficient if you just want to access the variable by upvar or uplevel, but useless for identifying changes to different variables managed by the same trace handler.

Bugs stemming from code not considering this behaviour can lie dormant for a very long time and be mystifying when they arise. Therefore, when the name of the variable matters to the trace handler, it's a good idea (and good practice for other kinds of callbacks) to pass that name explicitly, rather than relying on the arguments supplied by the trace:

proc traceHandler {name} {
    puts "trace: $name"
}
trace add variable x {write} [lambda {varname args} {traceHandler $varname} x]

PYK: I prefer an example that uses apply directly, because its execution namespace can be specified, and in contrast with lambda (at least as currently defined on that page), its uplevel is still the context that triggered the trace:

trace add variable x write [
    list apply [
        list {varname args} {traceHandler $varname} [namespace current]] x]

aspect: updated the lambda page to more prominently feature the Tcllib package which supports a namespace argument.

Local Variable Traces

On comp.lang.tcl, 2004-05, CLN answers Erik Leunissen's question: Erik Leunissen wrote:

> The following passage from the man page to the trace command is
> mystifying to me:
> 
> "If an unset occurs because of a procedure return, then the trace will
> be invoked in the variable context of the procedure being returned to:
> the stack frame of the returning procedure will no longer exist."
> 
> I've read it again and again; I can't imagine how a procedure return
> affects/causes an unset.
> ...


   proc foo { } { 
      set x 1
      trace add variable x unset {some code here}
   }

When foo is invoked, x is created (locally), has a trace associated with
it, then becomes unset as foo exits.

Arrays

For variables that are not arrays the value of name2 is the empty string when the trace is called, but the empty string is a perfectly valid array index (it is also a valid array variable name), so an empty value for name2 doesn't necessarily indicate that the traced variable is scalar. To determine whether a variable is an array, use:

if {[array exists $varname]} {...}

All of the following operations result in the argument values a {} u:

unset a ;# regular var a
array unset a
unset a()

including an empty index string. array exists always returns false for the first two cases, and true for the third (even if the empty index was the only array element). There is no way for the trace to be sure which operation was performed.

Lars H: Hmm... might this be a sign that the format of these parameter lists is not well designed? An alternative would have been to put the operation first and the variable name second, so that there needn't be an index for non-array accesses. Probably too late to change now, though. (Adding a second interface which is just like the current except that it produces parameter lists in a new format is possible, but probably seen as superfluous.)

Arrays and Upvar

A possibly surprising result that is documented for upvar:

If  otherVar  refers  to an element of an array, then variable traces
set for the entire array will not be invoked when myVar is accessed (but
traces on the particular element will still be invoked).

This means that using a trace on an array is not sufficient to capture all the ways elements can be accessed. For instance:

package require lambda
unset -nocomplain a x y
array set a {x 1 y 2}
trace add variable a {array read write unset} [lambda args {puts "::a trace: $args"}]

or easier - without the need for package lambda:

unset -nocomplain a x y
array set a {x 1 y 2}
trace add variable a {array read write unset} {apply {args {puts "::a trace: $args"}}}

Any direct use of a will trigger the trace:

% incr a(x)    ;# existing element
::a trace: a x read
::a trace: a x write
 2
% incr a(z)    ;# new element
::a trace: a z read
::a trace: a z write
 1
% unset a(x)
::a trace: a x unset

so far so good. But throw upvar into the mix:

% upvar 0 a(y) y    ;# existing element
% upvar 0 a(w) w    ;# new element
% incr y
 3
% incr w
 1
% unset y
% unset w

No traces were fired! If you're tracing an array whose elements might be upvared, beware.

What about a "variable create" Trace?

male 2006-01-24: I wanted to trace the creation of an array element and used the write event, but ... the write event is fired after the new array element was already created! What's about a new event like "create"? Since a trace may be created on non-existent variables, this could be useful not only for arrays.


Donald Arseneau: Yes, write traces fire after the variable has already been set, so if you want validation of variables' values, in analogy with Tk's entry validation, then you must maintain shadow copies of the previous values, in order to undo improper settings.

Triggering Traces when using a variable at the C level

On comp.lang.tcl, Kevin Kenny answers someone wanting to link a C variable and a Tcl variable, and have a Tcl proc invoked when the C code modified the variable:

Look in the manual for Tcl_UpdateLinkedVar. The Tcl engine has no way of telling that you've changed the variable in C; if you call Tcl_UpdateLinkedVar, that tells it your value has changed, and it fires the traces.

Simple file I/O in traces:

trace add variable stdout write {puts stdout $stdout ;#}
trace add variable stdin  read {gets stdin  stdin   ;#}

The variables are named like the file handles. Little demo, that waits for input and prints it capitalized:

set stdout [string toupper $stdin]

Managing Traces

Traces are like widgets and images in that they are resources that can be leaked and/or need clean-up. Counter-intuitive results sometimes arise because traces are additive rather than substitutive; a particular trace can fire a whole chain of commands. To wipe the global space clean of traces,

foreach variable [info glob] {
    foreach pair [trace info variable ::$variable] {
        lassign $pair op traced_command
        trace remove variable ::$variable $op $traced_command
    }
}

Swallowed Errors: Command Delete Traces

PYK 2015-04-02:

Errors are ignored in command delete traces:

variable var1 {1 one 2 two}
proc p1 args {}
#intentional bad code here: the commandPrefix doesn't have the right command signature.
trace add command p1 delete [list dict unset var1 1]
rename p1 {}
puts $var1 ;# -> 1 one 2 two

This is the current design, not a bug. Anyone have any info on the rationale?

Traces of Command Executions

step traces

enterstep and leavestep traces fire for all steps, recursively. When this is undesired, the depth of the recursion can be constrained by having the trace procedure look at info level.

Interesting experience with execution traces , comp.lang.tcl, 2003-11-24

Donald Arseneau: Another tricky trap is that errors in traces may give error messages, but no context; the only context is for whatever triggered the trace. Thus, if you ever see Tk error messages like

can't set "xv": invalid command name "oops"
    while executing
"incr xv"

then you should look for a variable trace on the xv variable.


Schnexel: Oh the tricky trace traps! I tried to automaticly update a derivedData array by setting a trace on the parentData array (scenario simplified)... Now I get a surreal result:

set bla "What happened:\n"

namespace eval bbb {

    array set lala [list 1 v1 2 v2 3 v3]
    trace add variable ::bbb::lala {read array} ::bbb::tra
    proc tra args {
        append ::bla "\n (TRACE $args)"
        array unset ::bbb::lala                         ;# also deletes trace (yet the "array" op still fires)
        foreach {n v}  [list 1 trv1 2 trv2 3 trv3] { set ::bbb::lala($n) $v }
    }
}

namespace eval aaa {
    append ::bla "\n\[info exists ::bbb::lala(1)\]==..."; append ::bla ... [info exists ::bbb::lala(1)]
    append ::bla "\n\[info exists ::bbb::lala(1)\]==..."; append ::bla ... [info exists ::bbb::lala(1)]
    append ::bla "\n\$::bbb::lala(1)==...";               append ::bla ... $::bbb::lala(1)
}

puts $bla

which gives the output

What happened:
[info exists ::bbb::lala(1)]==...
 (TRACE ::bbb::lala 1 read)
 (TRACE ::bbb::lala {} array)...0
[info exists ::bbb::lala(1)]==......1
$::bbb::lala(1)==......trv1

So, upon first read access of lala, it does not exist anymore, whilst upon second read it is there. Can anybody make sense of this?

Lars H: Regarding why the "array" op fires: It it fires at the very array unset ::bbb::lala where you comment upon this, i.e., before the foreach, which is consistent with the trace manpage (only read and write traces are disabled inside a read or write trace). But why info exists reports 0 I don't know... Perhaps some caching issue (the variable that was looked up is not the one that is there when the result is returned)? You'll probably need to read the source to find out.

Schnexel: Wrrr... Here´s a simpler example. Array trace is bugged!

array set ::lala [list 1 v1  2 v2]
array set ::lala [list 3 v3  4 v4]
puts "\$::lala==[array get ::lala]"                                   ;# O.K.

trace add variable ::lala read ::tra


proc tra args {
    puts "    (TRACE $args)"
    trace remove variable ::lala read ::tra

    array set ::lala   [list A trvA  B trvB]
    puts "    within trace:  \$::lala==[array get ::lala]" ;# O.K.
}

puts "1st read outside:  \$::lala==[array get ::lala]"      ;# not O.K. !
puts "2nd read outside:  \$::lala==[array get ::lala]"      ;# O.K.

Output:

$::lala==4 v4 1 v1 2 v2 3 v3
reading ::lala
    (TRACE ::lala 4 read)
    within trace:  $::lala==4 v4 A trvA 1 v1 B trvB 2 v2 3 v3
1st read outside:  $::lala==4 v4 1 v1 2 v2 3 v3
2nd read outside:  $::lala==4 v4 A trvA 1 v1 B trvB 2 v2 3 v3

Tracking where procedures were defined

DKF: One of the neat things about Tcl is that you can attach traces to things that in most other languages would be completely impossible to track. Here's how to find out where procedures are defined, by using an execution trace on [proc] itself.

proc recordDefinitionLocation {call code result op} {
    if {$code} return
    set name [uplevel 1 [list namespace which -command [lindex $call 1]]]
    set location [file normalize [uplevel 1 {info script}]]
    puts stderr "defined '$name' in '$location'"
}
trace add execution proc leave recordDefinitionLocation

Trying it out…

% parray tcl_platform
defined '::parray' in '/Library/Frameworks/Tcl.framework/Versions/8.6/Resources/Scripts/parray.tcl'
tcl_platform(byteOrder)     = littleEndian
tcl_platform(machine)       = x86_64
tcl_platform(os)            = Darwin
tcl_platform(osVersion)     = 12.5.0
tcl_platform(pathSeparator) = :
tcl_platform(platform)      = unix
tcl_platform(pointerSize)   = 8
tcl_platform(threaded)      = 1
tcl_platform(user)          = dkf
tcl_platform(wordSize)      = 8

That looks correct for my system.

Proposal: Modify trace command ... enter to Act as a Command Filter

PYK 2013-12-22: The result of a command run as a trace is currently discarded. It could instead be used as the command to actually call. For example, the result of the following script would be 12, not 21

proc add args {
    ::tcl::mathop::+ {*}$args
}

trace add execution add enter {apply {{cmd op} {
    set args [lassign $cmd name]
    foreach arg $args[set args {}] {
        if {$arg % 2 == 0} {
            lappend args $arg
        }
    }
    return [linsert $args 0 $name] 
}}}

add 1 2 3 4 5 6

Strange problem I had with trace and variables in namespaces

SVP 2016-12-22:

An interesting bug I have to fix.

I have a couple of "shapescale" widgets in an application. These are pure tcl, and they act like slider controls but with a non rectangular shape. More importantly they monitor their variable with a trace, so when ever the value changes the trace proc will redraw the widget:

proc _variableWriteTrace {w name1 name2 op} {
    variable Priv

    debugcallinfo traces
    # Array element?
    variable $name1
    if {$name2 ne ""} {
        set name1 "${name1}($name2)"
    }
    # Check the variable exists before we go reading from it
    debugputs traces "  +->Checking if variable $name1 exists ..."
    if {[info exists $name1]} {
        debugputs traces "  +->yes; value = '[set $name1]'"
        set Priv($w,value) [set $name1]
        _redrawSlider $w
    }
}

In theory.

But my problem was that when the data changed, and there were a whole bunch of other variables that changed, and a whole bunch of other traces fired, and whole bunch of other widgets were updated, except for the two shapescale widgets. Well, sometimes they would update. That really was the problem.

Now after much investigation I found the following behaviour. This example works. The variableWriteTrace proc can see the variable and it's value and it redraws the slider:

(System32) 51 % set ::Gap12::gap12_0(config_volume) 7
[::ShapeScale|traces] _variableWriteTrace w='.pr1.ed.pane.pafr.gap12.tree.config_volume' name1='::Gap12::gap12_0' name2='config_volume' op='write'
[::ShapeScale|traces]{  +->Checking if variable ::Gap12::gap12_0(config_volume) exists ...}
[::ShapeScale|traces]{  +->yes; value = '7'}

But then this second example fails. And this is the important one because the proc that changes the value lives in another namespace altogether called "Gap12", part of another code module:

(System32) 52 % namespace eval ::Gap12 { set gap12_0(config_volume) 7 }
[::ShapeScale|traces] _variableWriteTrace w='.pr1.ed.pane.pafr.gap12.tree.config_volume' name1='gap12_0' name2='config_volume' op='write'
[::ShapeScale|traces]{  +->Checking if variable gap12_0(config_volume) exists ...}

So the trace is called, but without the namespace prefix. Hence the variable being referenced doesn't exist, and so the value has changed but no redraw happens and the widget looses synch with the data.

Now the trace info tells me that the trace is there. I can also see a second unrelated trace which monitors for project data changes.

(System32) 53 % trace info variable ::Gap12::gap12_0(config_volume)
{write {::Gap12::_changedDataVariableTrace .pr1.ed.pane.pafr.gap12}} {write {::ShapeScale::_variableWriteTrace .pr1.ed.pane.pafr.gap12.tree.config_volume}}

And this is the code that sets the trace:

trace add variable [set Priv($w,-variable)] write "::ShapeScale::_variableWriteTrace $w"

And parray shows that the variable name is prefixed with the namespace correctly:

::ShapeScale::Priv(.pr1.ed.pane.pafr.gap12.tree.config_volume,-variable)                = ::Gap12::gap12_0(config_volume)

So the problem is that despite the fact that I use the fully qualified name of the variable in the trace add command, trace will still call the handler WITHOUT the namespace prefix when I access the variable within it's own namespace without the namespace qualification.

This seems a little strange to me?