Tcl Part I · Module 13 · Regular Expressions
The Useful Switches: -all, -inline, -line, -nocase
So far regexp has found one match and reported it through variables. Four switches change that in ways you will want often: one finds every match rather than stopping at the first, one hands results back as a list instead of assigning them, one makes the anchors mean line boundaries so a multi-line string can be searched line by line, and one ignores case. They combine, and the combination of the first two has a result shape that catches people out the first time — so this chapter spends real time on exactly what comes back.
Intermediate11 min readTclPart IRegexSwitchesParsing
Part I · Module 13 · Chapter 13.6 · The Useful Switches: -all, -inline, -line, -nocase
1. -all counts; it does not collect
The first surprise. -all finds every match, and on its own it returns how many:
set text "alpha 1.5 beta 2.25 gamma 30"
puts [regexp -all -- {\d+\.\d+} $text]2Two matches — and no way to see what they were. That is useful for counting and almost never what you wanted.
2. -inline returns the matches
puts [regexp -all -inline -- {\d+\.\d+} $text]1.5 2.25-inline says "return the result instead of assigning it to variables". With -all you get every match as a list, which is the form you will use most.
Without -all, -inline returns just the first match — as a list containing the whole match followed by each group:
puts [regexp -inline {(\w+) (\S+)} $text]{alpha 1.5} alpha 1.5Three elements: the whole match alpha 1.5, then group 1 alpha, then group 2 1.5. That is the same information regexp ... -> a b would have put in variables, handed back as a value instead.
3. Patterns starting with a dash
Chapter 13.2 §6 showed that a pattern beginning with - is read as a switch. With switches present it is easy to forget:
regexp -all -inline {-?\d+\.\d+} $textbad switch "-?\d+\.\d+": must be -all, -about, -indices, -inline, -expanded, -line, -linestop, -lineanchor, -nocase, -start, or --puts [regexp -all -inline -- {-?\d+\.\d+} "a 1.5 b -2.25"]1.5 -2.25Write -- before the pattern whenever you use switches. It costs nothing when unnecessary and prevents this entirely.
4. -all -inline with capture groups
Here is the result shape to understand before you rely on it. With groups, each match contributes the whole match and its groups, all flattened into one list:
set log "INFO ok\nERROR bad thing\nINFO fine\nERROR worse"
set found [regexp -line -all -inline {^ERROR (.*)$} $log]
puts "list : $found"
puts "elements : [llength $found]"list : {ERROR bad thing} {bad thing} {ERROR worse} worse
elements : 4Two matches, two elements each: the whole line, then the captured message. They are not grouped per match — it is one flat list.
So walk it with foreach taking as many variables per pass as the pattern has groups plus one — Chapter 9.3's grouping:
foreach {whole message} $found {
puts " message: '$message'"
} message: 'bad thing'
message: 'worse'5. -line makes the anchors mean line boundaries
By default ^ and $ mean the start and end of the whole string, even when it contains newlines:
set log "INFO ok\nERROR bad thing\nINFO fine\nERROR worse"
puts "without -line : '[regexp -all -inline {^ERROR (.*)$} $log]'"
puts "with -line : '[regexp -line -all -inline {^ERROR (.*)$} $log]'"without -line : ''
with -line : '{ERROR bad thing} {bad thing} {ERROR worse} worse'Without -line the pattern asks for a string that begins with ERROR and ends after the message — and this string begins with INFO, so there is no match at all. With -line, each line is a candidate.
This is what you want whenever you have read a file with read (Chapter 12.3) and want to search it line by line without splitting it first.
regexp -line {^ERROR (.*)$} $log -> first
puts "first error : $first"first error : bad thing6. -nocase
set text "alpha 1.5 beta 2.25 gamma 30"
puts "plain : [regexp {ALPHA} $text]"
puts "-nocase : [regexp -nocase {ALPHA} $text]"plain : 0
-nocase : 1It applies to the whole pattern. For case-insensitivity in one part only, write the alternatives into the pattern — [Ee]rror — which is clearer than making the entire thing insensitive.
7. -indices, for positions rather than text
regexp -indices {\d+} "ab123cd" where
puts "match occupies characters $where"
lassign $where start end
puts "which is '[string range "ab123cd" $start $end]'"match occupies characters 2 4
which is '123'-indices replaces each captured string with a two-element list of start and end offsets. It is what you need when you intend to modify the original rather than just read from it — though for most rewriting, Chapter 13.7 is the better tool.
8. A reference
| Switch | Effect |
|---|---|
-all | find every match; on its own, returns the count |
-inline | return results instead of assigning to variables |
-all -inline | every match (and its groups) as one flat list |
-line | ^ and $ match at line boundaries |
-nocase | ignore case for the whole pattern |
-indices | report offsets instead of text |
-- | end the switches; always use it before the pattern |
9. A worked example
Extracting every timestamped error from a log held as one string:
set log "2026-01-05 10:00:01 INFO starting up
2026-01-05 10:00:04 ERROR cannot open input.csv
2026-01-05 10:00:04 WARN retrying
2026-01-05 10:00:09 ERROR giving up after 2 attempts
2026-01-05 10:00:09 INFO finished"
set pattern {^(\S+ \S+) ERROR (.*)$}
set hits [regexp -line -all -inline -- $pattern $log]
puts "[expr {[llength $hits] / 3}] error(s):"
foreach {whole when message} $hits {
puts [format " %s %s" $when $message]
}
set counts [dict create]
set levels [regexp -line -all -inline -- {^\S+ \S+ (INFO|WARN|ERROR)} $log]
foreach {whole lvl} $levels {
dict incr counts $lvl
}
puts "by level : $counts"2 error(s):
2026-01-05 10:00:04 cannot open input.csv
2026-01-05 10:00:09 giving up after 2 attempts
by level : INFO 2 ERROR 2 WARN 1The / 3 is the stride from section 4: one pattern with two groups gives three elements per match. The second pass counts levels with a one-group pattern, so its stride is two.
10. Exercises
Exercise 1. What does regexp -all -- {\d+} $text return?
Exercise 2. What does adding -inline change?
Exercise 3. For a pattern with two capture groups and three matches, how many elements does -all -inline return?
Exercise 4. Why does regexp -all -inline {^ERROR (.*)$} $multiline return nothing without -line?
Exercise 5. Why should -- appear before the pattern whenever switches are used?
Exercise 6. Write a one-liner returning every integer in $text as a list.
Answers
1. The number of matches — a count, not the matches themselves.
2. It returns the matches as a list rather than assigning anything to variables. With -all you get every match; without it, the first match plus its groups.
3. Nine — three elements per match (the whole match plus two groups), times three matches, flattened into a single list.
4. Because without -line the anchors mean the start and end of the entire string. The string begins with something other than ERROR, so the pattern cannot match anywhere. -line makes ^ and $ apply at each line boundary.
5. Because a pattern beginning with - is otherwise read as another switch, giving bad switch "...": must be -all, -about, .... -- ends the switch list and costs nothing when it was not needed.
6.
set text "a 12 b 345 c 6"
puts [regexp -all -inline -- {\d+} $text]12 345 611. Remember
-allon its own returns a count, not the matches.-inlinereturns results instead of assigning them, so they compose with every list command.-all -inlinegives one flat list — the whole match plus each group, for every match.- Count the groups to get the
foreachstride right: groups plus one element per match. -linemakes^and$match at line boundaries; without it they mean the ends of the whole string.-nocaseapplies to the entire pattern — use[Ee]for one part only.-indicesreports offsets rather than text.- Write
--before the pattern whenever you use switches.
Where this fits
Part of the Tcl curriculum.
