Published Jan 22nd, 2026 · Updated Apr 12th, 2026 · wyatthoho
In Tcl, the distinction between "...", [...], and {...} initially felt
like a guessing game to me. For a long time, I found myself memorizing syntax
case-by-case and struggling with unexpected expr errors without truly
understanding the underlying logic. This article documents my journey to
demystify the Tcl interpreter by breaking down substitution rules and the
double evaluation trap, transforming that initial confusion into a clearer
mental model.
A Tcl script is a string containing one or more commands. Semicolons and newlines are command separators. Below is a Tcl script containing multiple commands:
# Using semicolons to separate commands
set x 1; set y 2; set z 3
# Using newlines to separate commands
set a 10
set b 20
set c 30
A command is composed of words. The first word is used to
locate a command procedure to carry out the command, then
all of the words of the command are passed to the command
procedure. If a hash character # appears as the first character
of a command, then this command is treated as a comment.
set msg "Hello, World!"; # Set the value of a variable
list Apple Banana Cherry; # Create a list with any number of arguments
A word is defined as:
"..."{...}When a word is enclosed in double quotes or braces, special characters such as semicolons, newlines, and whitespace are treated as literal characters and included in the word.
puts Hello, World!; # can not find channel named "Hello,"
puts "Hello, World!"; # Hello, World!
puts {Hello, World!}; # Hello, World!
The primary difference between these two grouping mechanisms is how substitutions are processed:
| Substitution Type | Double Quotes "..." |
Braces {...} |
|---|---|---|
| Command [] | Supported | Ignored |
| Variable $ | Supported | Ignored |
| Backslash \ | Supported | Ignored* |
*Note: Inside braces, backslashes are generally ignored, with one major exception: a backslash followed by a newline is replaced by a single space to allow for multi-line formatting without breaking the string.
There is another special syntax with braces called argument expansion. If a
word begins with {*}, the rest of the word is substituted as usual and then
parsed as a list. Each element of that list is then added to a command as a
separate argument. Example:
set profile {name Alice age 30}
dict create {*}$profile
The specific details of substitutions are described below.
Before carrying out the command, the Tcl interpreter breaks the command into words and performs substitutions.
If a word contains an section enclosed by open/close bracket [...]
then Tcl performs command substitution. The Tcl interpreter process
the characters as a Tcl script. The script may contain any number
of commands. The result of the script is substituted into the word
in place of the brackets and all of the characters between them.
set fruit [lindex "apple banana cherry" 0]; # fruit = "apple"
If a word contains a dollar sign $, then Tcl performs variable substitution.
That is, the dollar sign and the following characters are replaced in the word
by the value of a variable.
set name wyatt
puts $name
# Output: wyatt
However, if using the same approach to try to get the output wyatthoho, an
error will occur:
puts $namehoho
# can't read "namehoho": no such variable
There is another form of variable substitution to overcome this difficulty.
That is using ${name}.
puts ${name}hoho
# Output: wyatthoho
If a backslash \ appears within a word, then backslash
substitution occurs. The backslash is used to escape special
characters, such as:
\\: Literal backslash \\$: Literal dollar sign $\[: Literal open bracket [\]: Literal close bracket ]\": Literal double quote "The backslash is also used to insert whitespace characters, such as:
\n: Newline character\t: Tab character\<newline>whiteSpace: A single space characterA Tcl expression consists of a combination of operands, operators, parentheses, and commas. White space may be used between operands, operators, parentheses, and commas.
Operands may be specified in any of the following ways:
$...[...]"..."{...}Common operators include +, -, *, /, **, ==, !=, eq, ne,
in, ni, and others. The specific meaning of these operators is not the
focus here, so no further explanation is provided.
The expr command concatenates its arguments (ignoring white spaces between
them) as a Tcl expression, evaluates the result, and returns the value. Here’s
an example:
expr 1 + 1 + 1
# Output: 3
However, let’s try this:
expr 100 in "100 200 300"
# Error!!
Why did it fail? This example illustrates an extremely important behavior of expr,
according to tcl-lang:
… expressions are substituted twice: once by the Tcl parser and once by the expr command.
Applied to our case, the Tcl parser performs substitution on "100 200 300"
first (stripping the quotes). Consequently, the expr command receives five
distinct arguments: 100, in, 100, 200, and 300. Because expr
expects a single expression or a valid operator between these values, this
results in a “missing operator” error.
So, how do we write the expression correctly?
expr {100 in "100 200 300"}
# Output: 1
In this instance, the Tcl parser treats the braced string
{100 in "100 200 300"} as a single literal word, as braces prevent all
internal substitutions. Consequently, the expr command receives the entire
string as one argument. It then performs its own internal evaluation, correctly
identifying the three distinct components-the value, the operator, and the
list-and processes the expression without error.
The tcl-lang provides clear guidance on writing conventions:
… the command parser may already have performed one round of substitution before the expression processor was called. … it is usually best to enclose expressions in braces to prevent the command parser from performing substitutions on the contents.
Additionally:
Enclose expressions in braces for the best speed and the smallest storage requirements. This allows the Tcl bytecode compiler to generate the best code.
Unfortunately, simply placing the calculation you need within braces will not solve all problems. Substitution timing must be considered carefully.
set a 3
set b {$a + 2}
expr $b*4
# Output: 11
Without braces, a double substitution occurs. First, the Tcl parser replaces
$b with the literal string $a + 2, passing the expression $a + 2*4 to the
expr command. Subsequently, expr performs its own internal substitution,
replacing $a with 3. This results in the calculation 3 + 2*4, yielding a
final result of 11.
set a 3
set b {$a + 2}
expr {$b * 4}
# Error!!
With braces, only one level of substitution occurs. The Tcl parser passes the
literal string $b * 4 directly to the expr command. During evaluation,
expr performs a single substitution, replacing $b with its value: the
string $a + 2. This results in the expression $a + 2 * 4. However, because
expr has already completed its substitution phase, it does not re-scan the
result for further variables. Consequently, it attempts to multiply the literal
string $a + 2 by 4, which triggers a numeric syntax error.
set a 3
set b {$a + 2}
expr {[expr $b] * 4}
# Output: 20
When using braces, the Tcl parser passes the literal string [expr $b] * 4
directly to the expr command. During evaluation, the expr engine identifies
the command substitution [...], which pauses the current calculation to
trigger a nested command execution. According to tcl-lang on command
substitution:
If command substitution occurs then the nested command is processed entirely by the recursive call to the Tcl interpreter; no substitutions are performed before making the recursive call and no additional substitutions are performed on the result of the nested script.
Thus, expr $b is executed as an independent sub-command, returning the
value 5. The outer expr then substitutes this result into the original string,
simplifying the expression to 5 * 4. This produces the expected result of 20.
Expressions within if and for statements behave the same way as expr
arguments; however, the specifics of that evaluation are outside the scope of
this article.
After systematically organizing these syntax details, the myths that once confused me are now clear. Although the rules are intricate, I believe there are a few basic principles to keep in mind.
"..." is a word that allows substitution.{...} is a word that prevents substitution (except for backslash-newline).[...] invokes the Tcl interpreter recursively to process characters as a Tcl script.expr command.These principles form the basis of the more complex mechanics in Tcl.