Updated 2011-05-17 09:39:06 by RLE

slebetman: The I love foreach page mentions about ways to iterate over a list at varying rates. A common use of this is parsing command line options or parsing command arguments like Tk does. My usual solution to this is to use a while loop.

First we need something similar to Perl's shift and unshift commands to work on Tcl lists:

shift list - Removes the first element of a list and returns it. The list must be passed by name.
  proc shift {ls} {
    upvar 1 $ls LIST
    if {[llength $LIST]} {
      set ret [lindex $LIST 0]
      set LIST [lreplace $LIST 0 0]
      return $ret
    } else {
      error "Ran out of list elements."
    }
  }

unshift list data - Adds data to the beginning of a list.
  proc unshift {ls data} {
    upvar 1 $ls LIST
    set LIST [concat $data $LIST]
  }

Now that we have shift and unshift it's easy to make while act like foreach. The idiom is:
  while {[llength $mylist]} {
    set var [shift mylist]
    doSomethingWith $var
  }

To consume multiple sets of items from the list each iteration is simply:
  while {[llength $mylist]} {
    set var1 [shift mylist]
    set var2 [shift mylist]
    doSomethingWith $var1 $var2
  }

which behaves identically to:
  foreach {var1 var2} $mylist {
    doSomethingWith $var1 $var2
  }

Finally, to process multiple lists:
  while {[llength $list1] && [llength $list2]} {
    set var1 [shift list1]
    set var2 [shift list2]
    doSomethingWith $var1 $var2
  }

Clearly there's nothing that foreach can do that we can't do with while and shift. The main difference is that foreach continues to process lists until we run out of all elements while the method above generates an error if we try to consume more elements than exists in either list. But that's only because we've written shift to generate an error (see below for discussion of the error raising behavior). If we want to behave exactly like foreach we can instead write shift simply as follows:
  proc shift {ls} {
    upvar 1 $ls LIST
    set ret [lindex $LIST 0]
    set LIST [lreplace $LIST 0 0]
    return $ret
  }

Because we manually consume items from the list, we can vary the number of items consumed per iteration. With unshift we can even put back items on the list if we decide not to consume it in this iteration. The following is an example of using this idiom to process command line arguments:
  set cmdline $argv
  while {[llength $cmdline]} {
    set arg [shift cmdline]
    switch -exact -- $arg {
      "-q" {
        set quiet 1
      }
      "-f" {
        # Specifies output file:
        set outfile [shift cmdline]
      }
      "-D" {
        # If given with numeric argument then set the debug level
        # to that number. Otherwise simply set the debug level to 1:
        set level [shift cmdline]
        if {[string is integer $level]} {
          set debuglevel $level
        } else {
          set debuglevel 1
          unshift cmdline $level
        }
    }
  }

This may be much slower than other alternatives but it is very flexible and much easier to read.

Personally, I'd find it easier to read if you did 'while {[llength $cmdline] > 0} ...'

MG It actually doesn't raise an error, because the while loop stops running the moment one list is empty, which means your [shift] function never sees an empty list, and never gets a chance to raise an error.

slebetman Hope you don't mind but my reply to this is quite long so I moved your comment here.

Actually you're being short sighted. You're thinking of consuming only one word at a time in which case it won't raise an error. In which case you won't need this mechanism. But if you're using this technique then chances are your word consuming requirements are quite more complex than this.

Here's a very simple example of [shift] seeing an empty list:
  set mylist {foo bar bat}
  while {[llength $mylist]} {
    set x [shift mylist]
    set y [shift mylist]
    puts "$x $y"
  }