Tcl Part I · Module 5 · Strings
Finding Text: string first and string last
Two subcommands tell you where a piece of text sits inside another. They are simple to use and the interesting part is what they return when there is no match: minus one, which is a perfectly ordinary number that will happily be used as a position. Combined with the previous chapter's rule that an out-of-range position gives an empty string rather than an error, that means an unchecked search produces a wrong answer quietly. This chapter shows that chain happening, and the one-line habit that prevents it.
Foundation12 min readTclPart IStringsSearchingDefensive Checks
Part I · Module 5 · Chapter 5.4 · Finding Text: string first and string last
1. The Two Subcommands
set line "key:value:extra"
puts "first colon -> [string first ":" $line]"
puts "last colon -> [string last ":" $line]"first colon -> 3
last colon -> 9Both return a position, counting from 0 as in Chapter 5.3. string first finds the leftmost occurrence, string last the rightmost.
They search for a literal substring, not a pattern. For patterns you want Chapter 5.5.
2. Which Argument Goes First
This is the inconsistency flagged in Chapter 5.1 §5, and here is where it matters:
string first ":" $line
│ │
│ └── where to look (the haystack)
└─────── what to find (the needle)The thing you are looking for comes first. That is the opposite of string length $text and string range $text 0 5, where the data comes first.
If a search keeps returning -1 for text you can see is there, check this before anything else:
catch {string first} m
puts $mwrong # args: should be "string first needleString haystackString ?startIndex?"Tcl names them for you.
3. The Optional Start Position
A third argument says where to begin:
set line "key:value:extra"
puts "from the beginning -> [string first ":" $line]"
puts "from position 5 -> [string first ":" $line 5]"from the beginning -> 3
from position 5 -> 9That is how you walk through every occurrence — find one, then search again from just past it. A loop doing that properly needs Module 9; for now, note that the capability exists.
4. The Part That Matters: -1
When there is no match:
set line "no colon here"
puts "first colon -> [string first ":" $line]"first colon -> -1-1 is not an error and not an empty value. It is a number, and it will be used as a number by whatever you do next.
Watch what happens when it is not checked:
set line "no colon here"
set i [string first ":" $line]
set key [string range $line 0 $i-1]
puts "i = $i"
puts "key = <$key>"i = -1
key = <>No error anywhere. $i-1 became -2, string range $line 0 -2 is an out-of-range request, and Chapter 5.3 §5 says that gives an empty string.
So the script carries on with an empty key. If that value is written to a file, compared, or used as a dictionary key, the problem surfaces far from the line that caused it.
5. The Standard Split
With the check in place, splitting at the first separator is three lines:
set line "timeout:30"
set i [string first ":" $line]
if {$i < 0} {
error "no colon in line: $line"
}
set key [string range $line 0 $i-1]
set value [string range $line $i+1 end]
puts "key=<$key> value=<$value>"key=<timeout> value=<30>The index arithmetic is Chapter 5.3 §4 — $i-1 and $i+1 written straight into the positions.
Use string first rather than split here when the value may itself contain the separator:
set line "url:http://example.com"
set i [string first ":" $line]
set key [string range $line 0 $i-1]
set value [string range $line $i+1 end]
puts "key=<$key> value=<$value>"key=<url> value=<http://example.com>Splitting on every colon would have broken that value into pieces. Splitting at the first one keeps it whole.
6. string last and File Extensions
set path "archive.tar.gz"
set i [string last "." $path]
puts "base -> [string range $path 0 $i-1]"
puts "extension -> [string range $path $i+1 end]"base -> archive.tar
extension -> gzstring last is the right choice here — string first would have found the dot in .tar and given you archive and tar.gz.
That said, for real paths Tcl has dedicated commands that handle edge cases you have not thought of:
puts "rootname -> [file rootname "archive.tar.gz"]"
puts "extension -> [file extension "archive.tar.gz"]"
puts "tail -> [file tail "/tmp/reports/archive.tar.gz"]"rootname -> archive.tar
extension -> .gz
tail -> archive.tar.gzUse the file commands for paths. Module 12 covers them.
7. When Not to Use These
If the data has a consistent separator and you want all the pieces, split is simpler and is Chapter 5.13:
set csv "alpha,beta,gamma"
puts "fields: [split $csv ","]"fields: alpha beta gammaReach for string first when you want one specific position — the first separator, the last dot — rather than every field.
8. Common Mistakes
Argument order. Needle first. §2.
Using the result without checking for -1. §4. This is the one that costs time.
Using string first for a file extension. string last finds the right dot — or better, use file extension. §6.
Expecting a pattern to work. These search for literal text. Use string match or a regular expression.
Using them to split every field. Use split. §7.
Writing if {$i == -1}. $i < 0 is safer and reads the same.
9. Exercises
Exercise 1 — positions. For set s "a=b=c", find the first and last =. Predict both first.
Exercise 2 — the needle order. Deliberately write string first $line ":" the wrong way round and see what you get. Explain the result.
Exercise 3 — the silent failure. Reproduce §4's unchecked example. Confirm there is no error and that key is empty. Then add the check and confirm it now stops.
Exercise 4 — split at the first separator. Write the three-line split for the line "path:/usr/local/bin". Confirm the value keeps both its slashes and is not cut at any later character.
Exercise 5 — first versus last. For "backup.2024.tar", extract tar using one subcommand and backup using the other. Which did each need, and why?
Exercise 6 — a start position. Using the third argument, find the second : in "a:b:c" without using string last.
10. Remember
string firstandstring lastreturn a position, counting from 0.- The needle comes before the haystack — the opposite of most
stringsubcommands. - A third argument sets where the search starts.
- No match returns
-1, which is a number and will be used as one. - An unchecked
-1gives an empty result, not an error, because out-of-range positions return"". - Check it on the next line:
if {$i < 0} { ... }. - Use
string firstwhen the value may contain the separator;splitwhen you want every field. - For paths, prefer
file rootname,file extensionandfile tail.
Next: matching a shape rather than a literal.
Where this fits
Part of the Tcl curriculum.
