In my first software developer role, I used C++. That was expected, given the job was in finance.
What was unexpected was the second language we used. A language I had never heard of.
I’m betting most people reading this haven’t heard of it either. Or at least, haven’t written any code in it.
Despite the fact that it’s installed on macOS by default. And on some Linux distributions, too.
That language is TCL, or Tool Command Language. To this day, it’s the weirdest programming language I’ve ever learned.
It’s also inspired one of my favorite pieces of software, Redis, and its forks like Valkey.
But what makes it so weird? One property sets it apart from everything else.
In TCL, everything is a string.
Everything is a String
When I say everything in TCL is a string, I mean literally everything.
This includes what you might expect, like data types. But it also includes things you wouldn’t think of.
Complex data structures. Even the syntax of the language itself.
It’s without a doubt the weirdest language I ever learned.
Hello, World! in TCL
Before we dive deep, let’s start with a classic “Hello, World!” First, you’ll need the TCL runtime.
If you’re on macOS, it’s already there. But it’s likely an older version.
I recommend installing the latest version with Homebrew.
brew install tcl-tk
This gives you the tclsh command.
You can use it to execute TCL scripts or as a REPL (Read-Eval-Print Loop).
Let’s try the REPL first.
Use the puts keyword with the string you want to print.
puts "Hello, World!"
Simple enough.
Now, let’s write that in a script file. The REPL is great for experimenting, but not for complex code.
Create a new file named hello.tcl.
.
└── hello.tcl
Inside, let’s write a script to say hello to a specific name.
We’ll create a variable using the set command.
It takes two arguments: the variable’s name and its value.
# hello.tcl
set name "Dreams"
puts "Hello, $name"
[!TIP] Does that
setcommand look familiar? The creator of Redis, Salvatore Sanfilippo (antirez), took heavy inspiration from TCL. The first version of Redis was even written in TCL!
To reference the variable, we use the dollar sign syntax, $name.
TCL automatically substitutes this variable in strings defined with double quotes.
Now, run the script from your terminal.
tclsh hello.tcl
The output will be: Hello, Dreams.
Strings: Braces vs. Quotes
There are two ways to define strings in TCL. This distinction is critical.
Let’s say you want to print the literal text $name, not its value.
To do that, you define the string using braces {} instead of quotes "".
--- a/hello.tcl
+++ b/hello.tcl
@@ -1,2 +1,2 @@
set name "Dreams"
-puts "Hello, $name"
+puts {Hello, $name}
Now, running the code prints Hello, $name.
- Quotation Marks
"": Create strings where variables and commands are substituted. - Braces
{}: Create literal strings where no substitution occurs.
This is vital because, as I said, everything is a string. Including the language’s own constructs.
Code is Just a String
Let’s create a new file to see what this means.
I’m setting a variable called code to a string.
set code {puts "Hello from a string"}
If I print this variable, it’s just a string. But because this is TCL, it’s also valid, executable code.
We can execute it with the eval command.
set code {puts "Hello from a string"}
eval $code
This evaluates the string and runs the code within it.
The output is Hello from a string.
It gets weirder.
The eval command itself is also a string.
This means we can store the command we want to use in a variable.
set command "eval"
set code {puts "Hello from a string"}
$command $code
Executing this works just like calling eval directly.
This makes the language incredibly dynamic and opens up strange possibilities.
Expressions are Also Strings
This “stringly-typed” nature is especially wild with numbers and expressions. TCL looked at JavaScript’s type coercion and said, “Hold my beer.”
Here’s a simple string trying to add two numbers.
set my_expression "10 + 5"
To execute this, we don’t use eval.
We use the expr command, which resolves mathematical and boolean expressions.
I’m wrapping the command in square brackets [].
This tells TCL to resolve this command first and return its result, similar to parentheses in Lisp.
puts [expr "10 + 5"]
This prints the result: 15.
The string expression has been resolved.
Functions are Strings, Too
Defining a function in TCL uses the proc command (short for procedure).
You provide a string for the arguments and a string for the function body.
proc greet {name} {
puts "Hello, $name"
}
The function body is just a string that gets evaluated when the procedure is called. To call it, just reference its name.
greet "World"
You’ll notice I used braces for the argument list and the body. This is idiomatic TCL.
It makes the code look a lot like C’s syntax. It also prevents variables from being substituted too early.
No Statements, Only Commands
In most languages, you have statements like if for branching logic.
TCL is different.
It has if, but it’s a command, not a statement.
It works in a similar way, though.
The if command accepts two arguments:
- A string containing a boolean expression.
- A string of code to evaluate if the expression is true.
graph TD
A[if command] --> B{Evaluates Expression String};
B -- "True (non-zero)" --> C[Executes Body String];
B -- "False (zero)" --> D[Ends];
C --> D;
Let’s check if a number is greater than zero.
set num 10
if {$num > 0} {
puts "The number is positive."
}
[!NOTE] In TCL, boolean
trueis represented by any non-zero number string, andfalseis represented by a zero string.
Because $num is 10, the expression is true, and the message is printed.
If we change num to 0, nothing is printed.
The same principle applies to loops, like the while command.
It takes a boolean expression string and a loop body string.
set num 0
while {$num < 5} {
incr num 1
puts $num
}
This code increments num and prints its value until it reaches 5.
Using braces here is critical.
If you use quotes on the condition, the value of $num gets substituted once at the beginning.
# This creates an infinite loop!
set num 0
while "$num < 5" {
incr num 1
puts $num
}
The condition becomes "0 < 5", which is always true.
The loop never ends.
By using braces, we let the while command re-evaluate the expression string in every iteration, keeping the condition dynamic.
Data Structures as Structured Strings
How does TCL handle lists and dictionaries? You guessed it: they’re just strings.
A list in TCL is a string where elements are separated by spaces.
set my_list {apple banana cherry}
TCL provides commands to work with these structured strings:
llength: Returns the number of elements.lindex: Accesses an element by its index.lappend: Appends an element to the end.linsert: Inserts an element at a specific index.lrange: Performs list slicing.
You can iterate over a list with the foreach command.
set my_list {apple banana cherry}
foreach fruit $my_list {
puts "I like to eat a $fruit"
}
But what if an element needs to contain a space?
You can use quotes for that element or use the list command to build the list dynamically.
# Statically defined list with a space
set my_list {apple "banana split" cherry}
# Dynamically created
set item1 "apple"
set item2 "banana split"
set item3 "cherry"
set my_list [list $item1 $item2 $item3]
This creates a sublist within the string, allowing you to access elements as if it were a 2D array.
Dictionaries follow the same pattern. They are key-value data structures represented as a structured string. Key-value pairs are separated by spaces.
set saiyan {name Goku power_level 9001}
Commands for dictionaries are prefixed with dict:
dict get: Access a value by its key.dict set: Set or update a key’s value.
set saiyan {name Goku power_level 9001}
# Get a value
puts [dict get $saiyan name] ;# Outputs: Goku
# Set a value
dict set saiyan power_level "Over 9000"
puts $saiyan ;# Outputs: name Goku power_level {Over 9000}
Explicit Scoping
In most languages, scope is implied. A variable in a function is local. In TCL, scope is explicit.
Consider this script:
set x 10
proc print_x {} {
puts $x
}
print_x
Running this code produces an error. The print_x procedure can’t see the global variable x.
To make it work, you must explicitly mark x as global inside the procedure’s scope.
set x 10
proc print_x {} {
global x
puts $x
}
print_x ;# Now it works!
This explicitness also applies to pass-by-reference using the upvar command.
It binds a variable from a calling scope to a local variable.
But the weirdest scoping command is uplevel.
It allows a procedure to climb the call stack and execute code in a different scope.
sequenceDiagram
participant O as outer()
participant I as inner()
O->>O: set x = 42
O->>I: calls inner()
I->>O: uplevel 1 { puts $x }
Note right of I: Executes `puts $x`<br>in outer's scope
O-->>I: returns result
I-->>O: returns
Here, inner climbs one level up the call stack (uplevel 1) and executes puts $x from within outer’s scope.
proc inner {} {
uplevel 1 { puts "Value of x from outer scope: $x" }
}
proc outer {} {
set x 42
inner
}
outer ;# Outputs: Value of x from outer scope: 42
Why would this exist? It enables powerful metaprogramming, creating DSLs, and building your own control flow commands.
Where TCL Shines
Because the language and its data are one and the same, TCL is an excellent embedded scripting language. It’s similar to the role Lua plays in game engines or Neovim.
It has default bindings for C and C++, and can be used with any language supporting a Foreign Function Interface (FFI), like Rust or Zig.
The most common integration, however, is in Python’s tkinter package.
tkinter is a wrapper around Tk, a GUI framework that is almost always paired with TCL.
For a long time, TCL/Tk was the easiest way to build cross-platform GUI applications. And it’s still genuinely simple today.
Here’s a simple counter app in just 26 lines of TCL/Tk.
# A simple counter GUI in TCL/Tk
package require Tk
# Global variable for the counter
set count 0
# UI Elements
label .label -textvariable count -font {Helvetica 24}
button .inc -text "Increment" -command {incr count}
button .reset -text "Reset" -command {set count 0}
# Layout
pack .label .inc .reset -pady 10
# To run this, save it as counter.tcl and execute: wish counter.tcl
This is the main benefit of Tk: building simple, native-looking UIs trivially.
TCL is a fascinating, if strange, language. It’s quirky, but its unique design makes it powerful in its niche. It’s a language I’m glad I got to learn.