Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
basic.tcl
set s "hello world"

puts "toupper -> [string toupper $s]"
puts "tolower -> [string tolower "HELLO WORLD"]"
puts "totitle -> [string totitle $s]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
toupper -> HELLO WORLD
tolower -> hello world
totitle -> Hello world

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
totitle.tcl
puts [string totitle "hello world"]
puts [string totitle "HELLO WORLD"]
puts [string totitle "hELLO wORLD"]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Hello world
Hello world
Hello world

string 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:

Azvya Education Pvt. Ltd.VLSI Mentor
totitle-good.tcl
puts [string totitle "ERROR"]
puts [string totitle "timeout"]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Error
Timeout

3. Converting Part of a String

All three accept an optional range, using the index rules from Chapter 5.3:

Azvya Education Pvt. Ltd.VLSI Mentor
range.tcl
set s "hello world"

puts [string toupper $s 0 4]
puts [string toupper $s end-4 end]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
HELLO world
hello WORLD

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
everywhere.tcl
set mode "FAST"

if {[string equal -nocase $mode "fast"]} { puts "quick path" }
if {[string equal -nocase $mode "slow"]} { puts "slow path" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
quick path

Or convert once, then compare plainly:

Azvya Education Pvt. Ltd.VLSI Mentor
once.tcl
set mode [string tolower "FAST"]

if {$mode eq "fast"} { puts "quick path" }
if {$mode eq "slow"} { puts "slow path" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
quick path

Both 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:

Azvya Education Pvt. Ltd.VLSI Mentor
boundary.tcl
set raw_key "  TIMEOUT  "

set key [string tolower [string trim $raw_key]]

puts "normalised: <$key>"
puts "matches:    [expr {$key eq "timeout"}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
normalised: <timeout>
matches:    1

Trim 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.

Azvya Education Pvt. Ltd.VLSI Mentor
sorting.tcl
puts "mixed:      [lsort {banana Apple cherry}]"
puts "normalised: [lsort {banana apple cherry}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mixed:      Apple banana cherry
normalised: apple banana cherry

In 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 toupper and string tolower do what their names say.
  • string totitle capitalises 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 eq comparisons.
  • 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.