TCL Wat
At FlightAware, I read and write TCL every day. This means I've
run into more than a few edge cases in the language, and have even
tracked down a few bugs in
TCLlib. Here's a collection of
the weirdest things I've encountered.
Empty string is a boolean superposition
In TCL, an empty string is regarded as both true and false, due to the language's dynamic typing and the way it handles truthy/falsy values. This can lead to unexpected behavior in conditionals:
if {""} {
puts "This is true"
} elseif {!""} {
puts "This is false"
} else {
puts "This is neither true nor false"
}
Output:
This is neither true nor false
Why is this the way it is? TCL treats empty strings as falsy in
boolean contexts, but the empty string itself is still a valid
string value. The negation
!"" creates a boolean true, but
the empty string in the
if condition is also falsy.
It's... confusing.
Scalars are converted to dictionary keys
When using dict set command to
set a value into a dictionary, it can't automatically convert a
scalar variable to a dictionary. You have to explicitly create a
dictionary first:
set my_dict [dict create]
dict set my_dict key value
This is actually reasonable behavior - TCL doesn't want to assume you meant to create a dict when you might have just made a typo. But it's still annoying when you forget.
Upvar and pass-by-name
upvar is a command that
introduces a new variable in the current context which is an alias
for a variable in a caller's context. TCL uses pass-by-name for
its argument passing, meaning the names of variables are passed,
rather than their values:
proc increment {varName} {
upvar $varName var
incr var
}
proc swap {a b} {
upvar $a aVar $b bVar
set temp $aVar
set aVar $bVar
set bVar $temp
}
This is actually pretty cool once you get used to it, but it's definitely not what you'd expect coming from other languages.
Insane string matching
Some TCL commands, like
string is, allow matching on
shortened or abbreviated strings. While this can lead to concise
code, it may also introduce unintended and interesting bugs:
string is true true
# => 1
string is tru true
# => 1
string is tr true
# => 1
string is t true
# => 1
string is fa 0
# => 1
This has no practical benefit and serves only to create bugs. Why would a reasonable person ever want to check if a string "is tr" in production code?
Split nested list, get flat list
When using split on a nested
list, the result is a list with members representing the escaped
syntax of the nested list(s):
set a {aa bb {cc dd}}
split $a " "
# => {aa bb \{cc dd\}}
Notice that the braces of the nested list
{cc dd} have been escaped,
resulting in a string that looks like a dict, but is not. This is
because under the hood,
split operates on the string
representation, not the list structure. This has been the source
of a number of real bugs at FlightAware.
Lower-case numbers
The string tolower command
attempts to convert a string's characters to lowercase. When
encountering mixed or non-alphabetic input, the results may be
unexpected:
set number_string "42A_B8C"
string tolower $number_string
# => 42a_b8c
Even though the numbers remain the same, the non-alphabetic characters have been modified. This is technically correct behavior, but it's weird that you can "lowercase" a number.
Dict loops don't garbage-collect
When using dict with inside a
loop, it overrides variables, but it doesn't automatically unset
variables from previous iterations that have no new value:
set data {
item1 {var1 value1}
item2 {var2 value2}
}
foreach item {item1 item2} {
dict with data $item {
puts "Item: $item, var1: $var1, var2: $var2"
}
}
Output:
Item: item1, var1: value1, var2: value2
Item: item2, var1: value1, var2: value2
Notice that var1 is still set to
value1 in the second iteration.
The values were not garbage collected at the end of the
dict with body. This has been
another real source of bugs.
Comments are not comments
In TCL, comments are technically commands that do nothing, the
# command, which is a no-op, to
create comments:
# This is a TCL comment
This means that comments can be placed anywhere a TCL command can go, but also that they are part of the evaluation process, albeit with no effect. It's... different.
Uplevel is OP
The uplevel command is used to
execute a script in a different scope or level. This can be useful
in creating macros or DSLs (Domain Specific Languages) in TCL:
proc print_doubled {script} {
uplevel "set x [expr {$script * 2}] ; puts \$x"
}
print_doubled {2 + 3}
# => 10
This is actually pretty powerful, but it's also a great way to write code that nobody can understand or debug.
Lack of static typing
TCL is a dynamically typed language, which means variables don't have a fixed type. A variable can change its type during runtime:
set my_var 42
puts "Variable type: [string is integer $my_var]"
# => Variable type: 1
set my_var "hello"
puts "Variable type: [string is integer $my_var]"
# => Variable type: 0
This can lead to flexibility in development, but may also cause unintended consequences or unexpected behavior. Especially when dealing with JSON or other structured data formats. On the surface, this behavior is expected because "everything is a string". But it causes bugs when you understand TCL well enough to know that nothing is a string at the fundamental level.
Subst is not a separate compilation step
In TCL, the subst command is
used to perform variable and command substitutions within strings.
This can be handy in some situations, but it's important to note
that subst is executed at
runtime:
set x 10
set y "\$x + 5"
puts "Before substitution: $y"
# => Before substitution: $x + 5
puts "After substitution: [subst $y]"
# => After substitution: 10 + 5
This may be surprising if you're expecting TCL to act like a Lisp, and means that you might miss syntax errors until the code is actually executed.
Two ways of catching errors
TCL provides error handling mechanisms via the
catch,
try, and
error commands:
if {[catch {some_command} result]} {
puts "Error encountered: $result"
}
try {
some_command
} on error {errorMsg} {
puts "Error encountered: $errorMsg"
}
The catch command can be used to
capture any errors or exceptions that occur within a script.
try and
error offer more sophisticated
error handling, but they're still not as clean as exceptions in
other languages.
One particularly tricky aspect is that these error handling
mechanisms will also set the
errorCode global variable, which
can be both useful and a curse to debugging. When a TCL library
function is the source of the error, this global can leak error
state between different parts of your code, making it hard to
track down where errors are actually coming from. It's especially
problematic when functions use error handling as a way of checking
for dictionary key existence.
So there you have it - a collection of TCL weirdness that I encounter on a daily basis at FlightAware. Some of these are actually reasonable design decisions when you understand the philosophy behind TCL, but others are just... wat.