Tcl Part I · Module 8 · Conditions and Control Flow
switch: Multi-Way Branching
When a chain of elseif branches all compare the same variable against a different constant, there is a command that says exactly that and reads as a table instead of a staircase. switch is that command, and it is mostly as simple as it looks. Two things about it are not: the choice between writing the cases inside one pair of braces or as loose arguments changes which branch runs, not just how it looks; and a line starting with a hash inside those braces is not a comment, which produces one of the most confusing error messages in Tcl. Both are in here.
Foundation11 min readTclPart IControl flowswitchDispatch
Part I · Module 8 · Chapter 8.4 · switch: Multi-Way Branching
1. The shape
set mode full
switch -- $mode {
fast { set runs 1 }
full { set runs 10 }
deep { set runs 100 }
default { set runs 0 }
}
puts "runs = $runs"runs = 10One value, a list of pattern–body pairs, and the first matching body runs. Compare it with the elseif chain it replaces:
if {$mode eq "fast"} {
set runs 1
} elseif {$mode eq "full"} {
set runs 10
} elseif {$mode eq "deep"} {
set runs 100
} else {
set runs 0
}Same behaviour. The switch version states once that everything is comparing $mode, and the cases line up as a table you can read down.
2. Only one body runs
Unlike C, Tcl's switch does not fall through from one case into the next by accident. Exactly one body runs — the first that matches:
switch -- b {
a { puts "ran a" }
b { puts "ran b" }
c { puts "ran c" }
}ran bThere is a deliberate way to share one body between several patterns, and it is explicit. Chapter 8.6 covers it.
3. No match is not an error
If nothing matches and there is no default, switch quietly does nothing:
switch -- zzz {
a { puts "ran a" }
b { puts "ran b" }
}
puts "still here"still hereThat silence is the single most common source of "my script did nothing and said nothing". Supplying a default is how you decide what should happen instead — again, Chapter 8.6.
4. switch returns a value
Like if, switch is a command and returns the result of the body that ran:
set runs [switch -- full {
fast { expr 1 }
full { expr 10 }
default { expr 0 }
}]
puts $runs10Which makes the table form read very well for a pure lookup:
proc runs_for {mode} {
return [switch -- $mode {
fast { expr 1 }
full { expr 10 }
deep { expr 100 }
default { expr 0 }
}]
}
foreach m {fast full deep other} {
puts [format "%-6s %d" $m [runs_for $m]]
}fast 1
full 10
deep 100
other 0When nothing matches and there is no default, the result is the empty string.
5. The braced body list, and why it matters
There are two ways to write the cases. Inside one pair of braces:
switch -- $mode {
fast { ... }
full { ... }
}Or as loose arguments:
switch -- $mode fast { ... } full { ... }They are not equivalent. The braced form is one word, so nothing inside it is substituted before switch sees it — the rule from Chapter 2.8. The loose form is several words, each substituted as the line is parsed.
Watch which arm runs:
set pat fast
puts [switch -- fast {
$pat { expr {"matched-the-variable"} }
fast { expr {"matched-the-literal"} }
}]matched-the-literalputs [switch -- fast $pat { expr {"matched-the-variable"} } fast { expr {"matched-the-literal"} }]matched-the-variableIn the braced form the first pattern is the three literal characters $pat, which does not match fast, so the second arm wins. In the loose form $pat was replaced with fast before switch ran, so the first arm wins.
6. Bodies are scripts, not expressions
Each body is an ordinary script, so it can be as long as you like and can contain anything:
set level warn
set problems 0
switch -- $level {
error {
incr problems
puts "ERROR seen"
}
warn {
incr problems
puts "warning seen"
puts "continuing anyway"
}
info {
puts "nothing to do"
}
}
puts "problems = $problems"warning seen
continuing anyway
problems = 17. A # inside a switch is not a comment
This is the trap. Comments in Tcl are only recognised where a command is expected (Chapter 1.8), and the inside of a braced body list is not a command position — it is a flat list of patterns and bodies.
switch -- a {
# this looks like a comment
a { puts "hit" }
default { puts "miss" }
}invalid command name "comment"To see why, read that brace block the way Tcl does — as a list of ten words, paired off:
| Pair | Pattern | Body |
|---|---|---|
| 1 | # | this |
| 2 | looks | like |
| 3 | a | comment |
| 4 | a | puts "hit" |
| 5 | default | puts "miss" |
The words of your comment became patterns and bodies. The value being switched on is a, and the first pattern equal to a is pair 3, whose body is the single word comment — so Tcl ran comment as a command. Your intended puts "hit" sat one pair further down and was never reached.
Change the comment's wording and the bug changes shape. With an odd number of words in the block, Tcl notices something is wrong and tells you exactly what it suspects:
switch -- a {
# pick the right branch
a { puts "hit" }
default { puts "miss" }
}extra switch pattern with no body, this may be due to a comment incorrectly placed outside of a switch body - see the "switch" documentationThat message exists specifically because this mistake is so common. You only get it when the word count happens to be odd — which is why the first version, with its silent mis-pairing, is the more dangerous of the two.
8. A worked example
Dispatching on a report type, with each body doing real work:
proc summarise {kind rows} {
switch -- $kind {
count {
return "[llength $rows] rows"
}
first {
if {[llength $rows] == 0} { return "no rows" }
return "first: [lindex $rows 0]"
}
widths {
set out {}
foreach r $rows { lappend out [string length $r] }
return "widths: $out"
}
default {
return "unknown report kind: $kind"
}
}
}
set rows {clk reset data_in}
foreach k {count first widths bogus} {
puts [format "%-7s %s" $k [summarise $k $rows]]
}count 3 rows
first first: clk
widths widths: 3 5 7
bogus unknown report kind: bogusFour cases, each self-contained, and a default that names the problem rather than returning nothing.
9. Exercises
Exercise 1. Rewrite this as a switch:
if {$c eq "r"} { set n red } elseif {$c eq "g"} { set n green } elseif {$c eq "b"} { set n blue } else { set n unknown }Exercise 2. What does this print, and why is that dangerous?
switch -- zzz { a { puts A } b { puts B } }
puts doneExercise 3. Explain why these two give different answers:
set p fast
switch -- fast { $p {puts VAR} fast {puts LIT} }
switch -- fast $p {puts VAR} fast {puts LIT}Exercise 4. What is wrong with this, and what is the fix?
switch -- $mode {
# fast is the default
fast { set n 1 }
full { set n 10 }
}Exercise 5. Write a switch that returns a value, and show what it returns when nothing matches and there is no default.
Exercise 6. Give one case where an elseif chain is better than a switch, and say what makes it so.
Answers
1.
switch -- $c {
r { set n red }
g { set n green }
b { set n blue }
default { set n unknown }
}2. It prints only done. Nothing matched, there is no default, and switch does nothing silently — so a typo'd or unexpected value produces no output and no error at all.
3. The first is a braced body list, so it is a single word and $p is never substituted — the pattern is the literal text $p, which does not match fast, so LIT runs. The second is loose arguments, so $p is substituted to fast while the line is parsed and the first arm matches, printing VAR.
4. The # line is not a comment. Inside a braced body list Tcl is reading patterns and bodies, so #, fast, is, the and default all become patterns or bodies and the pairing shifts. Move the comment above the switch:
# fast is the default
switch -- $mode {
fast { set n 1 }
full { set n 10 }
}5.
set r [switch -- zzz {
a { expr 1 }
b { expr 2 }
}]
puts "'[set r]'"''The empty string.
6. When the branches test different things, or test ranges rather than equality — if {$n < 0} ... elseif {$n < 10} ... cannot be written as a switch on one value. switch is for one value against many constants or patterns; elseif is for genuinely different questions.
10. Remember
switch -- $value { pattern body ... }runs the first matching body, and only that one.- Always write
--so a value beginning with a dash is not mistaken for an option. - No match and no
defaultdoes nothing, silently — the commonest cause of a script that produces no output and no error. switchreturns the body's value, or the empty string when nothing ran.- Use the braced body list. It keeps patterns literal; the loose-argument form substitutes them first and can run a different arm.
- Bodies are full scripts, not expressions.
- A
#line inside a braced body list is not a comment — its words become patterns and bodies and one gets executed. Put comments above theswitchor inside a body. - Use
switchfor one value against many constants; useelseifwhen the branches ask different questions.
Where this fits
Part of the Tcl curriculum.
