Tcl Part I · Module 13 · Regular Expressions
regsub: Substituting by Pattern
Matching finds things; substituting changes them. regsub takes a pattern, a subject, a replacement and the name of a variable to put the result in — and the detail worth fixing in your head immediately is that last clause, because the command's return value is not the new string. It is a count of how many substitutions were made. That is the opposite shape to regexp, which returns the thing you usually want, and mixing the two up produces code that assigns a number where text belongs.
Intermediate11 min readTclPart IRegexregsubText processing
Part I · Module 13 · Chapter 13.7 · regsub: Substituting by Pattern
1. The shape
regsub {\s+} "a b c" "_" result
puts "result : $result"result : a_b cFour arguments: pattern, subject, replacement, and the name of a variable for the result. Only the first run of spaces was replaced, because regsub stops after one substitution by default.
-all replaces every match:
regsub -all {\s+} "a b c" " " squeezed
puts "squeezed : '$squeezed'"squeezed : 'a b c'Collapsing runs of whitespace is probably the single most common use.
2. The return value is a count
set n [regsub -all {\s+} "a b c" " " squeezed]
puts "returned : $n"
puts "result : '$squeezed'"returned : 2
result : 'a b c'Two substitutions were made. The result went into squeezed; the count came back from the command.
3. Omitting the variable returns the result
Tcl offers the form people usually expect — leave out the variable name and the new string comes back:
puts [regsub -all {\d} "a1b2" "X"]aXbXThat is often the clearest spelling, and it composes:
set line " padded text here "
puts "'[string trim [regsub -all {\s+} $line " "]]'"'padded text here'4. A failed substitution still writes the variable
Unlike regexp, which leaves capture variables untouched on failure (Chapter 13.4 §5), regsub always produces a result:
set out "PREVIOUS"
set n [regsub {zzz} "abc" "X" out]
puts "returned : $n"
puts "out : '$out'"returned : 0
out : 'abc'Zero substitutions, and out holds the unchanged input rather than its old value. That is the sensible behaviour — "the result of replacing nothing" is the original — and it means you never have to guard a read the way you must with regexp.
5. & is the whole match
In the replacement, & stands for whatever the pattern matched:
puts [regsub {\d+} "abc123" {[&]}]
puts [regsub -all {\w+} "one two" {<&>}]abc[123]
<one> <two>To put a literal ampersand in the replacement, escape it:
puts [regsub {\d+} "abc123" {\&}]abc&6. Backreferences reuse the groups
\1, \2 and so on stand for the capture groups, numbered as in Chapter 13.4 §7:
puts [regsub {(\w+)@(\w+)} "user@host" {\2:\1}]host:userThe two halves were swapped. A more practical version — reformatting a date:
set dates "2026-01-05 and 2025-12-31"
puts [regsub -all {(\d{4})-(\d{2})-(\d{2})} $dates {\3/\2/\1}]05/01/2026 and 31/12/20257. What regsub is genuinely good at
Normalising whitespace:
puts "'[string trim [regsub -all {\s+} " a b " " "]]'"'a b'Stripping comments:
foreach line {"width = 32 # the bus width" "mode = fast" "# whole line"} {
set clean [string trim [regsub {\s*#.*$} $line ""]]
puts " '[set line]' -> '$clean'"
} 'width = 32 # the bus width' -> 'width = 32'
'mode = fast' -> 'mode = fast'
'# whole line' -> ''Counting as a side effect, which is the one time the return value is what you want:
set text "a1 b2 c3 d"
set n [regsub -all {\d} $text "" stripped]
puts "removed $n digit(s), leaving '$stripped'"removed 3 digit(s), leaving 'a b c d'8. A worked example
Cleaning up a configuration file in one pass:
proc tidy {line} {
# strip a trailing comment
set line [regsub {\s*#.*$} $line ""]
# collapse internal whitespace
set line [regsub -all {\s+} $line " "]
# normalise spacing around the equals sign
set line [regsub {\s*=\s*} $line " = "]
return [string trim $line]
}
set lines {
"width=32"
" mode = fast # the run mode"
"depth = 16"
"# a whole-line comment"
" "
}
foreach l $lines {
set t [tidy $l]
if {$t eq ""} {
puts [format " %-32s (dropped)" "'$l'"]
} else {
puts [format " %-32s -> '%s'" "'$l'" $t]
}
} 'width=32' -> 'width = 32'
' mode = fast # the run mode' -> 'mode = fast'
'depth = 16' -> 'depth = 16'
'# a whole-line comment' (dropped)
' ' (dropped)Three substitutions in sequence, each doing one job, each using the no-variable form so the result flows into the next. That is easier to read — and to change — than one pattern trying to do all three.
9. Exercises
Exercise 1. What are regsub's four arguments, and what does it return?
Exercise 2. What does set x [regsub -all {\s+} $line " " out] put in x?
Exercise 3. How do you get the rewritten string back directly?
Exercise 4. After a substitution that matched nothing, what does the result variable hold?
Exercise 5. What do & and \1 mean in a replacement, and how do you write a literal &?
Exercise 6. Write a one-liner collapsing all whitespace runs in $s to single spaces and trimming the ends.
Answers
1. Pattern, subject, replacement, and the name of a variable for the result. It returns the number of substitutions made, not the new string.
2. The count. The rewritten text goes into out.
3. Omit the variable name: regsub -all {\s+} $line " " returns the new string directly.
4. The unchanged input. regsub always writes a result, which is why — unlike regexp — you never need to check the return value before reading it.
5. & is the whole match and \1, \2 … are the capture groups. A literal ampersand is \&. Brace the replacement so Tcl does not eat the backslashes.
6.
set s " a b c "
puts "'[string trim [regsub -all {\s+} $s " "]]'"'a b c'10. Remember
regsub ?switches? pattern subject replacement ?varName?— and it returns a count, not the result.- That is the opposite of
regexp, which returns what you want and writes what you asked for. - Omit the variable name to get the string back directly — usually the clearest form.
- A substitution that matched nothing still writes the result variable, with the unchanged input. No guard needed.
-allreplaces every match; without it only the first.&is the whole match,\1and friends are the groups, and\&is a literal ampersand.- Brace the replacement as well as the pattern —
\1in quotes arrives as1. - Several small substitutions in sequence read better than one pattern doing everything.
Where this fits
Part of the Tcl curriculum.
