Tcl Part I · Module 5 · Strings
Case Conversion
Three subcommands change the case of text, and the mechanism holds no surprises. What this chapter is really about is a habit: when case does not matter to your program, convert values once as they arrive rather than remembering to ignore case at every comparison. That one decision removes a whole family of bugs where a check was written correctly in four places and carelessly in the fifth. There is also one genuine trap, in the subcommand whose name suggests it does something it does not.
Foundation9 min readTclPart IStringsCaseNormalisation
Part I · Module 5 · Chapter 5.7 · Case Conversion
1. The Three Subcommands
set s "hello world"
puts "toupper -> [string toupper $s]"
puts "tolower -> [string tolower "HELLO WORLD"]"
puts "totitle -> [string totitle $s]"toupper -> HELLO WORLD
tolower -> hello world
totitle -> Hello worldTwo of those are exactly what you expect. The third is the trap.
2. totitle Is Not Title Case
Look at that output again: Hello world, not Hello World.
puts [string totitle "hello world"]
puts [string totitle "HELLO WORLD"]
puts [string totitle "hELLO wORLD"]Hello world
Hello world
Hello worldstring totitle capitalises the first character of the string and lowercases everything else. It does not capitalise each word.
Where totitle is genuinely right is a single word, or the first word of a sentence:
puts [string totitle "ERROR"]
puts [string totitle "timeout"]Error
Timeout3. Converting Part of a String
All three accept an optional range, using the index rules from Chapter 5.3:
set s "hello world"
puts [string toupper $s 0 4]
puts [string toupper $s end-4 end]HELLO world
hello WORLDThis is rarely what you want and is worth knowing exists, mostly so that an unexpected third argument in someone else's code makes sense.
4. The Habit: Normalise Once
Here is the chapter's actual content.
When case does not matter to your program, you have two choices. Compare case-insensitively everywhere:
set mode "FAST"
if {[string equal -nocase $mode "fast"]} { puts "quick path" }
if {[string equal -nocase $mode "slow"]} { puts "slow path" }quick pathOr convert once, then compare plainly:
set mode [string tolower "FAST"]
if {$mode eq "fast"} { puts "quick path" }
if {$mode eq "slow"} { puts "slow path" }quick pathBoth work. The second is better for a reason that only shows up later: in the first version, every future comparison has to remember -nocase. Add a third branch six months from now, forget the option, and that branch silently never matches.
In the second version there is nothing to remember. The value was made lowercase on the way in, and eq is correct everywhere after that.
The rule:
Convert at the boundary — where the value enters your script — and use plain comparisons thereafter.
5. Where the Boundary Is
"On the way in" means wherever a value first arrives from outside:
set raw_key " TIMEOUT "
set key [string tolower [string trim $raw_key]]
puts "normalised: <$key>"
puts "matches: [expr {$key eq "timeout"}]"normalised: <timeout>
matches: 1Trim and lowercase together, once, and everything downstream is simple. Trimming is Chapter 5.8, the next chapter, and the two are almost always done as a pair.
6. Case and Sorting
A related consequence, from Chapter 5.6 §7: uppercase sorts before lowercase, because character codes decide the order.
puts "mixed: [lsort {banana Apple cherry}]"
puts "normalised: [lsort {banana apple cherry}]"mixed: Apple banana cherry
normalised: apple banana cherryIn the first list Apple came first because A has a lower code than b, not because of alphabetical order as a person would mean it.
lsort -nocase handles this directly, and Module 6 covers it. Normalising first also works.
7. Common Mistakes
Expecting totitle to capitalise every word. §2.
Scattering -nocase through a script. Normalise once. §4.
Normalising the value you display. §5.
Forgetting that sorting is case-sensitive too. §6.
Converting repeatedly in a loop. If a value is compared many times, convert it once before the loop, not inside it.
8. Exercises
Exercise 1 — the three. Apply all three subcommands to "mIxEd CaSe TeXt" and predict each result before running.
Exercise 2 — the totitle trap. Predict string totitle "annual report 2024". Then write down what you would have to do to capitalise each word.
Exercise 3 — normalise once. Take the raw value " Verbose " and produce a key suitable for comparison. Then show the comparison succeeding against "verbose".
Exercise 4 — the forgotten option. Write three -nocase comparisons and deliberately omit the option from one. Confirm that branch fails. Then rewrite the whole thing with normalisation and confirm the bug cannot be reproduced.
Exercise 5 — display versus match. Write a script that greets a user by the name they typed while matching their role case-insensitively. Keep both values.
Exercise 6 — sorting. Sort {delta Alpha charlie Bravo} as-is, then after normalising. Explain the difference in one sentence.
9. Remember
string toupperandstring tolowerdo what their names say.string totitlecapitalises only the first character of the whole string and lowercases the rest. It is not per-word title case.- There is no built-in per-word capitalisation. Split, convert, join.
- All three accept an optional index range.
- Normalise once at the boundary, then use plain
eqcomparisons. - That beats scattering
-nocase, because there is nothing left to forget. - Normalise the value you compare, not the one you display.
- Sorting is case-sensitive too — uppercase sorts first.
Next: the other half of normalising an incoming value, and a trap that looks like it works.
Where this fits
Part of the Tcl curriculum.
