Looping over the contents of a text widget

Bryan Oakley writes:

This is something I've kept in my back pocket for a while. It hasn't gone through rigorous testing but seems to work well enough for my needs.

Usage:

forText pathName ?-elide? ?-regexp? ?-nocase? ?-exact? pattern start end script

The -elide, -regexp, -nocase and -exact options are passed directly to the text widget's search command.

This command will loop over all of the text in the widget specified by pathName. Each time a range of characters matches pattern the marks matchStart and matchEnd will be set and the specified script will be run.

Example:

   # this example adds the tag 'highlight' to all occurrences
   # of text inside <>
   pack [text .t] -side top -fill both -expand y 
   .t tag configure highlight -foreground red
   <insert text into widget>
   forText .t -regexp {***:<.*?>} 1.0 end {
      .t tag add highlight matchStart matchEnd
   }

   proc forText {w args} {

      # initialize search command; we may add to it, depending on the
      # arguments passed in...
      set searchCommand [list $w search -count count]

      # Poor man's switch detection
      set i 0
      while {[string match {-*} [set arg [lindex $args $i]]]} {

         if {[string match $arg* -regexp]} {
            lappend searchCommand -regexp
            incr i
         } elseif {[string match $arg* -elide]} {
            lappend searchCommand  -elide
            incr i
         } elseif {[string match $arg* -nocase]} {
            lappend searchCommand  -nocase
            incr i
         } elseif {[string match $arg* -exact]} {
            lappend searchCommand  -exact
            incr i
         } elseif {[string compare $arg --] == 0} {
            incr i
            break
         } else {
            return -code error "bad switch \"$arg\": must be\
              --, -elide, -exact, -nocase or -regexp"
         }
      }

      # parse remaining arguments, and finish building search command
      foreach {pattern start end script} [lrange $args $i end] {break}
      lappend searchCommand $pattern matchEnd searchLimit

      # make sure these are of the canonical form
      set start [$w index $start]
      set end [$w index $end]

      # place marks in the text to keep track of where we've been
      # and where we're going
      $w mark set matchStart $start
      $w mark set matchEnd $start
      $w mark set searchLimit $end

      # default gravity is right, but we're setting it here just to
      # be pedantic. It's critical that matchStart and matchEnd have
      # left and right gravity, respectively, so that any text inserted
      # by the caller duing the search won't normally (*) cause an infinite
      # loop. 
      # (*) If the script inserts text after the matchEnd mark, and the
      # text that was added matches the pattern, madness will ensue.
      $w mark gravity searchLimit right
      $w mark gravity matchStart left
      $w mark gravity matchEnd right

      # finally, the part that does useful work. Keep running the search
      # command until we don't find anything else. Each time we find 
      # something, adjust the marks and execute the script
      while {1} {
         set cmd $searchCommand
         set index [eval $searchCommand]
         if {[string length $index] == 0} break

         $w mark set matchStart $index
         $w mark set matchEnd  [$w index "$index + $count c"]

         uplevel $script
      }
   }