Tcl Part I · Module 6 · Lists
lsearch: Finding an Element
Finding something in a list is a common need and this command covers it thoroughly — exact matching, patterns, regular expressions, returning positions or values, all matches or just the first. It also contains two traps that produce wrong answers rather than errors. One is that it does not do what you probably assume by default. The other is a consequence of returning an index, and it is the reason the membership operators from Module 4 exist.
Foundation13 min readTclPart IListsSearching
Part I · Module 6 · Chapter 6.9 · lsearch
1. It Returns a Position
set names {clk reset data tx}
puts "reset -> [lsearch -exact $names reset]"
puts "bogus -> [lsearch -exact $names bogus]"reset -> 1
bogus -> -1A position when found, -1 when not — the same convention as Chapter 5.4.
2. The Default Is Not Exact
This is the first trap, and it is easy to miss because it usually does not matter:
set names {clk data_top}
puts "default with a pattern -> [lsearch $names "*_top"]"
puts "-exact with the same -> [lsearch -exact $names "*_top"]"default with a pattern -> 1
-exact with the same -> -1The default mode is glob matching, not exact. With ordinary values containing no *, ? or [, the two behave identically, so the difference stays hidden.
It stops being hidden the moment the value you are searching for contains one of those characters, because that value is used as the pattern:
set l [list axb ayb a*b]
puts "list: $l"
puts "default -> index [lsearch $l "a*b"], which is <[lindex $l [lsearch $l "a*b"]]>"
puts "-exact -> index [lsearch -exact $l "a*b"], which is <[lindex $l [lsearch -exact $l "a*b"]]>"list: axb ayb a*b
default -> index 0, which is <axb>
-exact -> index 2, which is <a*b>You asked for the element a*b and the default gave you axb — because a*b was treated as a pattern, and axb matches it.
It gets worse with -all:
set l [list axb ayb a*b]
puts "default -> [lsearch -all $l "a*b"]"
puts "-exact -> [lsearch -all -exact $l "a*b"]"default -> 0 1 2
-exact -> 2Every element matched.
3. The Matching Modes
set names {clk reset data_top rx_top tx}
puts "-exact -> [lsearch -exact $names reset]"
puts "-glob -> [lsearch -glob $names "*_top"]"
puts "-regexp -> [lsearch -regexp $names {^r}]"-exact -> 1
-glob -> 2
-regexp -> 1-glob uses the patterns from Chapter 5.5. -regexp uses regular expressions, which are Module 13.
For numeric lists, -integer and -real compare as numbers rather than text:
puts "-exact 20 -> [lsearch -exact {10 20 30} 20]"
puts "-integer 20 -> [lsearch -integer {10 20 30} 20]"-exact 20 -> 1
-integer 20 -> 1They agree here. They would not if the list held 20.0 and you searched for 20 — the text differs and the number does not, which is Chapter 4.3 again.
4. Returning Values Instead of Indices
set names {clk reset data_top rx_top tx}
puts "-all -> [lsearch -all -glob $names "*_top"]"
puts "-all -inline -> [lsearch -all -inline -glob $names "*_top"]"
puts "-inline (first)-> [lsearch -inline -glob $names "*_top"]"
puts "-all -inline -not -> [lsearch -all -inline -not -glob $names "*_top"]"-all -> 2 3
-all -inline -> data_top rx_top
-inline (first)-> data_top
-all -inline -not -> clk reset txFour useful combinations:
-all— every match, as indices.-inline— the matching value instead of its index.-all -inline— all matching values. This is Tcl's filter.-not— invert, so-all -inline -notkeeps everything that does not match.
lsearch -all -inline is the idiomatic way to filter a list, and it reads better than a loop:
set files {report.txt data.csv notes.txt archive.tar}
set text_files [lsearch -all -inline -glob $files "*.txt"]
puts "text files: $text_files"
puts "count: [llength $text_files]"text files: report.txt notes.txt
count: 25. The Index-Zero Trap
Now the second trap, and it is a real bug:
set names {clk reset}
set i [lsearch -exact $names clk]
puts "index of clk: $i"
if {$i} {
puts "found (wrong reasoning)"
} else {
puts "NOT found — but it IS there, at index 0"
}index of clk: 0
NOT found — but it IS there, at index 0clk is in the list, at position 0. And 0 is false — Chapter 3.6 §3.
So if {[lsearch ...]} reports "not found" for the first element of every list, and correctly for everything else. A test that works for four of five elements is a test that passes review.
The fix:
set names {clk reset}
set i [lsearch -exact $names clk]
if {$i >= 0} {
puts "found at $i"
}found at 06. Searching a Table
With -index, the search looks at one column of a nested list:
set table {{clk 1} {reset 1} {payload 64}}
set i [lsearch -exact -index 0 $table "reset"]
puts "row index: $i"
puts "row: [lindex $table $i]"row index: 1
row: reset 1That is how you look a record up by its key field. Chapter 6.11 covers the structure, and Module 7's dictionaries are usually the better answer once lookups get frequent.
7. Removing a Found Element
Combining with Chapter 6.7 §3:
set queue {alpha beta gamma}
set i [lsearch -exact $queue beta]
if {$i >= 0} {
set queue [lreplace $queue $i $i]
}
puts $queuealpha gammaThe >= 0 check is not optional. Without it, a missing value gives -1 and lreplace $queue -1 -1 does something you did not ask for.
8. Common Mistakes
Omitting -exact. The default is glob. §2.
Using the result directly in a condition. Index 0 is falsy. §5.
Writing != -1 and getting it right, then writing == 1 somewhere else. Use >= 0 consistently.
Using lsearch when you only need yes or no. Use in. §5.
Forgetting -inline and then indexing manually. -all -inline gives you the values.
Using a -1 result as a position. §7.
9. Exercises
Exercise 1 — found and not found. Search a four-element list for a value that is present and one that is not. Print both results.
Exercise 2 — the default mode. Build a list containing the literal value a*b. Search for axb with and without -exact. Explain the difference.
Exercise 3 — the zero trap. Write the buggy if {[lsearch ...]} test and show it failing for the first element. Then fix it two ways — with >= 0 and with in.
Exercise 4 — filter. From a list of eight filenames, produce just the .log ones using -all -inline -glob, and then just the non-.log ones.
Exercise 5 — indices versus values. For the same search, print the indices and the values. When would you want each?
Exercise 6 — look up a row. Given a three-row table of name and width, find the row whose name is payload and print its width.
10. Remember
lsearchreturns a position, or-1when not found.- The default mode is glob, not exact. Pass
-exactfor literal values, always. - Modes:
-exact,-glob,-regexp, plus-integerand-realfor numbers. -allgives every match;-inlinegives values instead of indices.-all -inlineis Tcl's filter, and-notinverts it.- Index 0 is a valid match and is also false. Never use the result directly in a condition — write
>= 0. - Use
inwhen you only need to know whether something is present. -index Nsearches one column of a nested list.- Check
>= 0before using the result as a position.
Next: putting a list in order.
Where this fits
Part of the Tcl curriculum.
