Tcl — Reference

Source: https://www.tcl-lang.org/man/tcl/contents.html

Tcl

  • Created: 1988 by John Ousterhout (UC Berkeley)
  • Latest stable: Tcl/Tk 9.0.3 (2025-11-13); the legacy 8.x series (8.6.x) remains widely deployed
  • Paradigms: Imperative, dynamic, command-oriented; object-oriented via TclOO; functional-flavored via lambdas
  • Typing: Dynamic; “everything is a string” with internal dual-typing for numeric performance
  • Memory: Reference-counted, copy-on-write internal object representation; no exposed GC tuning
  • Compilation: Bytecode-compiled on first execution (since 8.0) by an internal compiler that targets the Tcl VM; interpreted execution
  • Primary domains: Test harnesses (DejaGnu, Expect), EDA scripting (Cadence, Synopsys, Xilinx Vivado), GUI prototyping (with Tk), embedded scripting in C/C++ apps, network appliances (Cisco IOS XE)
  • Notable runtimes: Reference Tcl (C), Jacl (JVM, mostly abandoned), Eagle (.NET)
  • Official docs: https://www.tcl-lang.org/man/tcl/contents.html and https://wiki.tcl-lang.org/

At a glance

Tcl (“Tool Command Language”) is a tiny-core, command-driven language whose surface is a single rule: a script is a sequence of commands; a command is a list of whitespace-separated words; the first word names the command, the rest are its arguments. Substitution ($var, [command]) and grouping ("", {}) are the only syntactic features that matter. Everything else — if, proc, while, set, even expr for arithmetic — is just another command. This makes Tcl uniquely embeddable: a host C app exposes its own functionality as new commands, and scripts become indistinguishable from “native” syntax. (tcl-lang.org/about/language.html)

Getting started

Install:

  • Linux/macOS: package manager (apt install tcl tk, brew install tcl-tk)
  • Windows: ActiveTcl distribution (now Magicsplat Tcl/Tk for Windows) or build from source
  • No version manager culture; people install one Tcl per system

Hello world:

#!/usr/bin/env tclsh
puts "Hello, world!"

Project layout: No standard. A typical layout for a pkgIndex.tcl-based package:

mypkg/
  pkgIndex.tcl     ;# loaded by `package require mypkg`
  mypkg.tcl
  tests/all.tcl    ;# tcltest runner

Package/build tool: package require name ?version? is the runtime loader. TEA (Tcl Extension Architecture) for C extensions. Teacup/teapot for ActiveState distribution (legacy). For modern Tcl 9, tclmgr is emerging.

REPL: tclsh (no Tk) or wish (with Tk) gives an interactive prompt with % continuation handling. tkcon is the popular enhanced REPL.

Basics

Types & literals. All values are strings on the surface; internally Tcl maintains a dual Tcl_Obj representation that caches a typed form (int, double, list, dict, bytecode) until a string mutation invalidates it. Numbers in expr: 42, 0xFF, 0o17, 0b1010, 3.14, 1e6. Booleans: true/false/yes/no/on/off/1/0. Lists are space-delimited strings (use list to construct safely with quoting). Dicts are even-length lists treated as key/value pairs (since 8.5).

Variables & scoping.

set x 42                  ;# global or proc-local
set ns::var "hello"       ;# namespace variable
global g                  ;# import global into proc
upvar 1 callerVar local   ;# bind to caller's frame (powerful)
variable v                ;# declare namespace var inside proc

Tcl 9 changed unqualified variable resolution: inside a namespace, an unqualified set now resolves to the current namespace, not the global one. (Tcl 9.0 release notes)

Control flow. All commands:

if {$x > 0} { puts "pos" } elseif {$x < 0} { puts "neg" } else { puts "zero" }
while {$i < 10} { incr i }
for {set i 0} {$i < 10} {incr i} { puts $i }
foreach {k v} $dict { puts "$k=$v" }
switch -regexp -- $s { ^a { ... } ^b { ... } default { ... } }

Brace the condition ({...}) so expr can compile it; quoted conditions force string interpolation per iteration (the classic perf trap).

Functions (proc).

proc greet {name {greeting "Hello"}} {
    return "$greeting, $name!"
}
proc sum {args} { ;# variadic
    set total 0
    foreach n $args { set total [expr {$total + $n}] }
    return $total
}

Strings. string length, string range, string map {a A b B} $s, string match (glob), regexp/regsub (POSIX-ish + extensions), format/scanf-like scan, subst for explicit substitution control.

Collections. list, lappend, lindex, linsert, lreplace, lsort -dictionary -unique, lmap (map over list), lsearch -all -inline. Dicts: dict create, dict get, dict set, dict for {k v} $d {...}, dict update, dict with (binds keys as local variables).

Intermediate

Type system depth. No declared types. Internal representation shimmering: a value used as an integer caches an int rep; coerce it to a list and the int rep is dropped, then back to int costs reparsing. The cost matters in tight loops; expr {…} braces avoid the round-trip. Tcl 9 brought 64-bit-clean strings: values >2 GiB are now legal, removing a long-standing limit.

Modules / namespaces.

namespace eval util {
    namespace export greet
    proc greet {n} { return "Hi $n" }
}
namespace import util::greet

Namespaces also support ensembles — a single command that dispatches to subcommands by name, the foundation of dict, string, file, chan. Build your own: namespace ensemble create -map {add ::myadd sub ::mysub}.

Error handling.

try {
    set fp [open foo.txt r]
} on error {msg opts} {
    puts "failed: $msg"
} finally {
    catch {close $fp}
}

Older idiom is catch {expr {1/0}} result. Errors carry an option dict (since 8.5) with -errorcode, -errorinfo, -level. return -code error -errorcode {ARITH DIVZERO} "div by zero" lets you raise structured errors.

Concurrency. Single-threaded by default. The Thread extension (bundled) gives true OS threads with shared-nothing model and message passing via thread::send. Each thread has its own interpreter. Combine with tsv::* for shared variables. The coroutine command (since 8.6) gives stackful coroutines for cooperative concurrency without OS threads.

I/O. open, close, read, gets, puts, chan configure -buffering none -translation binary -encoding utf-8. fileevent for non-blocking event-driven I/O integrated with the event loop (vwait). socket -server for TCP servers in a few lines.

Stdlib highlights. tcllib is the de facto standard library (HTTP, JSON, MIME, math, crypto, BWidget). tcltest for unit testing. pkg_mkIndex to generate pkgIndex.tcl. parray for pretty-printing arrays. info for introspection (info commands, info procs, info body, info args). clock for date/time (handles tz, ISO 8601).

Advanced

Memory. Reference-counted Tcl_Obj everywhere; no GC pause concerns but cycles in dicts-of-dicts can leak (avoided by Tcl’s tree-shaped data). string is checks may shimmer; cache results.

Concurrency deep dive. With Thread: each thread has an independent interp, no shared interp state. thread::create -joinable, thread::send, thread::wait. Pool pattern via thread::pool::create. Avoid sharing Tcl_Objs across threads — use thread::send to marshal. Tcl 9 added epoll/kqueue notifiers for high-FD-count event loops on Linux/BSD.

FFI / C extension. Three paths: (1) write a C extension using Tcl’s stable C API (Tcl_CreateObjCommand, Tcl_GetStringFromObj, etc.) — the canonical embed/extend story. (2) critcl package — embed C inline in Tcl scripts, JIT-compiled. (3) Ffidl / tcl-cffi — call shared libs without writing C glue. Tcl was designed to be embedded in C (Ousterhout’s original goal): Tcl_CreateInterp, Tcl_Eval, Tcl_CreateObjCommand is a 3-line embed.

Reflection. info commands ::*, info procs, info args proc, info body proc, info default, info level/info frame for stack inspection, info exists var. The trace command attaches read/write/unset/array hooks to variables and enter/leave/enterstep/leavestep hooks to commands — used for AOP, debuggers, mocking.

Performance tools. tcl::unsupported::disassemble proc <name> dumps the bytecode for a proc — invaluable for spotting expr quoting bugs that defeat compilation. time {…} 1000 for microbenchmarks. tcl::unsupported::representation $value shows the current internal rep (“pure string”, “int”, “list of N”, etc.). The profile package in tcllib provides call-graph profiling.

God mode

  • Bytecode disassembly: tcl::unsupported::disassemble proc myproc (or script {script body}) shows the compiled Tcl VM ops. Use it to verify expr {…} is compiled instead of re-parsed each iteration; a properly braced expr emits INST_* ops, an unbraced one emits a string-substitute + reparse.
  • uplevel / upvar: uplevel 1 {script} runs script in caller’s frame; upvar 1 callerName local binds caller’s variable. This is how Tcl implements foreach-like commands in user code — it’s macro-equivalent power without macros.
  • trace everything: trace add variable x write [list myhook] fires myhook on every write. trace add execution proc enter/leave/enterstep/leavestep instruments calls — the tkcon debugger and tcltest mocks lean on this.
  • Namespace ensembles: Build extensible “subcommand dispatch” types that look like built-ins (mything add 1 2, mything del foo) via namespace ensemble create -map. Combine with unknown handlers for dynamic subcommands.
  • Custom commands in C: Tcl_CreateObjCommand(interp, "mycmd", MyCmdProc, clientData, deleteProc). Your C function receives Tcl_Obj *const objv[] and returns TCL_OK/TCL_ERROR. Performance is C-native; dispatch overhead is one hash lookup + indirect call.
  • Threads + thread::send: Marshal a script to another thread’s interpreter, optionally synchronously. thread::send -async $tid {puts hi}. Combine with tsv::set for shared kv.
  • TclOO (since 8.6): Real classes with oo::class create, method, constructor, destructor, superclass, plus filters (interceptors that run before/after every method call) and mixins (per-instance behavior injection). The model is more powerful than Java’s — filters give pre/post hooks for free.
  • Critcl: critcl::cproc fast_add {int a int b} int { return a + b; } compiles inline C on the fly; subsequent runs use the cached .so.
  • Expect: Built on Tcl, automates terminal interactions (spawn ssh host, expect "Password:", send "$pw\r") — still the canonical tool for scripting interactive CLIs.
  • Embedded Tcl in a C app: Tcl_CreateInterp() → Tcl_Eval(interp, script). Cisco IOS XE ships Tcl as a built-in CLI scripting environment this way.
  • Errors with structure: try ... on error {msg opts} { dict get $opts -errorcode } lets you dispatch on a structured error code list (ARITH DIVZERO) instead of regex-matching the message.

Idioms & style

  • Naming: lowerCamelCase for procs and vars by convention (not enforced). Namespaces in lower::case. Constants as ALL_CAPS by tradition only.
  • Brace your exprs. expr {$x + 1} compiles to bytecode; expr "$x + 1" re-parses every call. The single most impactful idiom.
  • Quote with list, not "". When building command lines for eval, uplevel, after, etc., always construct with list so quoting is exact: eval [list set $varname $value].
  • if {[catch {…} err]} {…} is the older error pattern; try…on error… is preferred in Tcl 8.6+.
  • No semicolons; newlines end commands. Use ; only to put two commands on one line.
  • Formatter/linter: Frink (style checker, oldest), nagelfar (lint + light static analysis). No de facto formatter.
  • Comments are commands. A # is only a comment when it appears where a command is expected. set x 1 # nope, this is an arg is a parse-time bug; use ;# this is fine after a command.

Ecosystem

  • GUI: Tk is Tcl’s sibling; package require Tk and you have native widgets on Win/Mac/Linux. Bundled. (Also wrapped from Python as tkinter, Perl, Ruby.)
  • Test: tcltest (bundled), tcltest2, plus nagelfar for static checks.
  • Web: Wapp, Rivet (mod_rivet for Apache), tclhttpd. Niche.
  • Database: tdbc (uniform DB layer, tdbc::sqlite3, tdbc::postgres, tdbc::mysql, tdbc::odbc).
  • Docs: doctools (in tcllib).
  • Notable users: AOL (web stack, original sponsor of Tcl 8.x), Cisco (IOS XE Tcl scripting), every major EDA vendor (Synopsys Design Compiler, Cadence Innovus, Xilinx Vivado, Altera Quartus all script in Tcl), DejaGnu (GCC/GDB test harness), Expect (terminal automation).

Gotchas

  • Unbraced expr is parsed as a string and reparsed every call — orders-of-magnitude slowdown. Always expr {…}.
  • Comments inside {…} braces at command position aren’t comments — they’re errors at the next pass when the body is evaluated. Put them at the start of a line preceded by ; or use #… preceded by a newline inside a proc body.
  • Tcl 9 broke unqualified namespace var lookup. Pre-9, an unqualified set x inside a namespace fell back to the global; in 9, it stays in the current namespace. (Tcl 9 notes)
  • Tcl 9 disabled tilde expansion in pathnames. open ~/foo.txt no longer works; use file normalize ~/foo.txt or $env(HOME)/foo.txt.
  • Tcl 9 makes I/O encoding errors fatal by default (was silent replacement in 8.x). Audit binary I/O paths and add -encoding binary -translation binary.
  • tcl_precision no longer affects formatting in Tcl 9.
  • List vs string is contextual. set s "a b"; lindex $s 0 returns a. But set s "a b" (two spaces) and lindex still returns a — the string-to-list parse normalizes whitespace, so list ops on “string” data lose information silently. Use list to construct.
  • info exists doesn’t fire variable read traces; set does. Important for testing trace-instrumented vars.
  • Empty string is a valid list of length 0, but also a valid string, also a valid number 0 in some expr contexts. Type confusion is real.
  • Threads need separate interp init. package require X in one thread doesn’t load it in another.

Citations