Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
basic.tcl
set line "key:value:extra"

puts "first colon -> [string first ":" $line]"
puts "last colon  -> [string last ":" $line]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first colon -> 3
last colon  -> 9

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
usage.tcl
catch {string first} m
puts $m
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrong # 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:

Azvya Education Pvt. Ltd.VLSI Mentor
start.tcl
set line "key:value:extra"

puts "from the beginning -> [string first ":" $line]"
puts "from position 5    -> [string first ":" $line 5]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from the beginning -> 3
from position 5    -> 9

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

Azvya Education Pvt. Ltd.VLSI Mentor
no-match.tcl
set line "no colon here"

puts "first colon -> [string first ":" $line]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
unchecked.tcl
set line "no colon here"

set i [string first ":" $line]
set key [string range $line 0 $i-1]

puts "i   = $i"
puts "key = <$key>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
split-at-first.tcl
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>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
value-has-colon.tcl
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>"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
extension.tcl
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]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
base      -> archive.tar
extension -> gz

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

Azvya Education Pvt. Ltd.VLSI Mentor
file-commands.tcl
puts "rootname  -> [file rootname "archive.tar.gz"]"
puts "extension -> [file extension "archive.tar.gz"]"
puts "tail      -> [file tail "/tmp/reports/archive.tar.gz"]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rootname  -> archive.tar
extension -> .gz
tail      -> archive.tar.gz

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

Azvya Education Pvt. Ltd.VLSI Mentor
use-split.tcl
set csv "alpha,beta,gamma"

puts "fields: [split $csv ","]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fields: alpha beta gamma

Reach 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 first and string last return a position, counting from 0.
  • The needle comes before the haystack — the opposite of most string subcommands.
  • A third argument sets where the search starts.
  • No match returns -1, which is a number and will be used as one.
  • An unchecked -1 gives an empty result, not an error, because out-of-range positions return "".
  • Check it on the next line: if {$i < 0} { ... }.
  • Use string first when the value may contain the separator; split when you want every field.
  • For paths, prefer file rootname, file extension and file tail.

Next: matching a shape rather than a literal.

Where this fits

Part of the Tcl curriculum.