Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string match   : 1
regexp         : 1
file extension : 1

And on a name designed to catch a careless test:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string match   : 0
regexp         : 0
file extension : 0

All 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set line "timeout = 300"

split alone is not enough:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts "split : '[split $line "="]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
string first : 'timeout' / '300'

scan is shorter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scan $line "%s = %s" k v
puts "scan : '$k' / '$v'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scan : 'timeout' / '300'

regexp is also shorter, and says the most about the shape:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
regexp {^\s*(\w+)\s*=\s*(.+?)\s*$} $line -> k v
puts "regexp : '$k' / '$v'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scan   : clk / in / 1
regexp : clk / in / 1

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
level   : WARN
date    : 2026-01-05
message : clock skew on domain two

scan cannot do this:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scan $entry "%s %s %s" a b c
puts "scan gives : '$a' / '$b' / '$c'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 problemReach for
A shape of name — prefix, suffix, wildcardstring match
Anything about a paththe file commands
A known single delimiterstring first + string range
Separated fields, consistentsplit
Fixed whitespace columnsscan
A substring at a known placestring range
Validating a field's shaperegexp
Optional or alternative structureregexp
A trailing field containing anythingregexp

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 full

The 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 file commands; for shapes of names, string match; for a known delimiter, string first and string range; for fixed columns, scan.
  • split divides but does not trim, so it rarely suffices alone for key = value.
  • scan's %s stops 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.