Tcl Part I · Module 13 · Regular Expressions
When a Regex Is the Right Tool, and When It Is Not
This module comes twelfth rather than second, and that is deliberate. By now you have string match for shapes of names, string first and string range for a known delimiter, split for separated fields, scan for fixed columns, and the file path commands for anything that is a path. Most of what people reach for a regular expression to do is covered by one of those, more readably and with fewer ways to be subtly wrong. So before learning the syntax, it is worth seeing the same problems solved every way — including the ones where the regex genuinely is the right answer, because those exist too and this chapter does not pretend otherwise.
Intermediate11 min readTclPart IRegexDesignString handling
Part I · Module 13 · Chapter 13.1 · When a Regex Is the Right Tool, and When It Is Not
1. The rule
The simplest tool that cannot be wrong wins.
Not simply the shortest. A tool that is shorter and gives the wrong answer on inputs you have not thought of is not simpler — it has moved the complexity somewhere you cannot see it.
The rest of the chapter applies that to four problems.
2. Does this name end in .csv?
Three tools, same answer:
set name "summary.csv"
puts "string match : [string match "*.csv" $name]"
puts "regexp : [regexp {\.csv$} $name]"
puts "file extension : [expr {[file extension $name] eq ".csv"}]"string match : 1
regexp : 1
file extension : 1And on a name designed to catch a careless test:
set tricky "notes.csv.bak"
puts "string match : [string match "*.csv" $tricky]"
puts "regexp : [regexp {\.csv$} $tricky]"
puts "file extension : [expr {[file extension $tricky] eq ".csv"}]"string match : 0
regexp : 0
file extension : 0All three are correct. So pick on readability, and string match "*.csv" is the one a reader understands without parsing anything — with file extension arguably better still, because it says why rather than how.
3. Splitting key = value
set line "timeout = 300"split alone is not enough:
puts "split : '[split $line "="]'"split : '{timeout } { 300}'The spaces are still there. split divides, it does not tidy — the behaviour from Chapter 5.13. You would need a string trim on each part, at which point you are writing three commands.
string first plus string range works and is explicit:
set i [string first "=" $line]
set k [string trim [string range $line 0 [expr {$i - 1}]]]
set v [string trim [string range $line [expr {$i + 1}] end]]
puts "string first : '$k' / '$v'"string first : 'timeout' / '300'scan is shorter:
scan $line "%s = %s" k v
puts "scan : '$k' / '$v'"scan : 'timeout' / '300'regexp is also shorter, and says the most about the shape:
regexp {^\s*(\w+)\s*=\s*(.+?)\s*$} $line -> k v
puts "regexp : '$k' / '$v'"regexp : 'timeout' / '300'All four agree. For this input, scan is the clearest short answer and string first is the clearest explicit one. The regex earns its place only once the format stops being this regular — if the separator might be = or :, or the key might contain a dot, the pattern absorbs that and the others start growing conditionals.
4. Fixed columns
set row "clk in 1"
scan $row "%s %s %d" n d w
puts "scan : $n / $d / $w"
regexp {^(\S+)\s+(\S+)\s+(\d+)$} $row -> n d w
puts "regexp : $n / $d / $w"scan : clk / in / 1
regexp : clk / in / 1Identical results, and scan is half the length with no pattern to misread. For whitespace-separated columns, scan wins outright — that is what it is for (Chapter 5.12).
The regex version does carry one advantage: it validates. scan will happily read clk in abc and leave w unset or wrong, whereas the pattern's (\d+) simply fails to match. If the input is trusted, use scan; if it is not, the pattern is doing a second job.
5. Where a regex genuinely wins
Now the honest case for the tool this module is about. A log line with a structured prefix and free-form text after it:
set entry "WARN 2026-01-05 clock skew on domain two"
regexp {^(\w+)\s+(\d{4}-\d{2}-\d{2})\s+(.*)$} $entry -> level date message
puts "level : $level"
puts "date : $date"
puts "message : $message"level : WARN
date : 2026-01-05
message : clock skew on domain twoscan cannot do this:
scan $entry "%s %s %s" a b c
puts "scan gives : '$a' / '$b' / '$c'"scan gives : 'WARN' / '2026-01-05' / 'clock'%s stops at whitespace, so the message is truncated to its first word. You could reach for string first twice and slice, and by the time you have done that the pattern is both shorter and more obviously correct.
Two things make this the regex case:
- A field whose shape matters —
\d{4}-\d{2}-\d{2}asserts the date looks like a date, which no other tool here checks. - A trailing field that may contain anything, including the separators the other tools split on.
6. A decision table
Chapter 13.10 closes the module with a fuller version. The short form:
| The problem | Reach for |
|---|---|
| A shape of name — prefix, suffix, wildcard | string match |
| Anything about a path | the file commands |
| A known single delimiter | string first + string range |
| Separated fields, consistent | split |
| Fixed whitespace columns | scan |
| A substring at a known place | string range |
| Validating a field's shape | regexp |
| Optional or alternative structure | regexp |
| A trailing field containing anything | regexp |
7. The costs a regex carries
Three, so that "use the simplest tool" has teeth.
It is write-only unless you are careful. {^(\w+)\s*=\s*(.+?)\s*$} takes a reader thirty seconds; string first "=" takes none.
It fails silently. A pattern that does not match returns 0 and leaves your capture variables untouched — which Chapter 13.9 is entirely about. scan has the same issue; string first returns -1, which is at least a value you can check.
The pattern must survive Tcl's own parsing. A regex written in double quotes is mangled before the regex engine ever sees it — Chapter 13.5 shows a pattern that silently matches the wrong thing because of it.
8. A worked comparison
One file, parsed twice — once with the simplest tool per field, once entirely with regexes:
set lines {
"# settings"
"timeout = 300"
"mode = fast"
""
"WARN 2026-01-05 disk nearly full"
}
puts "simple tools:"
foreach line $lines {
set t [string trim $line]
if {$t eq "" || [string match "#*" $t]} continue
if {[string first "=" $t] >= 0} {
set i [string first "=" $t]
puts " setting: [string trim [string range $t 0 [expr {$i-1}]]] = [string trim [string range $t [expr {$i+1}] end]]"
} else {
puts " other : $t"
}
}
puts "regex for the irregular line:"
foreach line $lines {
set t [string trim $line]
if {[regexp {^(\w+)\s+(\d{4}-\d{2}-\d{2})\s+(.*)$} $t -> lvl date msg]} {
puts " $lvl on $date: $msg"
}
}simple tools:
setting: timeout = 300
setting: mode = fast
other : WARN 2026-01-05 disk nearly full
regex for the irregular line:
WARN on 2026-01-05: disk nearly fullThe settings were handled by string match and string first, which is all they needed. The log line needed a pattern, and got one. Mixing the two is not inconsistency — it is each field getting the tool that fits it.
9. Exercises
Exercise 1. State the rule this chapter opens with, and say why "simplest" alone is not enough.
Exercise 2. Give three ways to test whether $name ends in .csv, and say which you would use.
Exercise 3. Why is split $line "=" not sufficient for timeout = 300?
Exercise 4. Why does scan $entry "%s %s %s" truncate the message in section 5?
Exercise 5. Name two properties of an input that make a regex the right tool.
Exercise 6. Someone writes regexp {^abc} $s. What simpler command answers the same question, and what can it not get wrong?
Answers
1. The simplest tool that cannot be wrong wins. Simplicity alone is not enough because a shorter tool that gives wrong answers on untested inputs has only hidden its complexity — you pay for it later, somewhere harder to find.
2. string match "*.csv" $name, regexp {\.csv$} $name, and [file extension $name] eq ".csv". The last is best for a path, because it is about paths rather than about strings, and string match is the clearest of the two string answers.
3. Because split divides but does not trim, so the parts come back as {timeout } and { 300} with their spaces. Making it usable needs a string trim on each part, at which point simpler tools are no longer simpler.
4. Because %s reads up to the next whitespace, so it captures only clock — the first word of a message that contains spaces. A regex can end with (.*)$ and take the rest of the line whole.
5. A field whose shape has to be validated rather than just located, and a trailing field that may contain the characters other tools would split on. More generally: genuine irregularity — optional fields or alternative separators.
6. string match "abc*" $s. It has no backslash escapes, no anchors and no greediness, so none of those can be got wrong — and it does not need to survive Tcl's quoting rules in the way a pattern does.
10. Remember
- The simplest tool that cannot be wrong wins — not simply the shortest.
- For paths, use the
filecommands; for shapes of names,string match; for a known delimiter,string firstandstring range; for fixed columns,scan. splitdivides but does not trim, so it rarely suffices alone forkey = value.scan's%sstops at whitespace, so it cannot capture a trailing free-form field.- A regex earns its place on irregularity: a field whose shape must be validated, optional or alternative structure, or a trailing field containing anything.
- Regexes cost you readability, silent failure, and surviving Tcl's own quoting.
- A pattern with no classes, quantifiers or groups probably wanted to be a glob.
- Mixing tools within one parser is correct, not inconsistent.
Where this fits
Part of the Tcl curriculum.
