Controlling iteration in the loop body

mpr 2016-08-19: This proc is a useful little tool for parsing options to a command. iter acts exactly like a foreach loop, but exposes the next command inside the body of the loop, which jumps the iterating variable to the next value in the list and returns that value.

proc iter { var list body } {
    coroutine next apply [subst {{} {
        yield
        foreach x {$list} { yield \$x }
        return -code break
    }}]
    
    while 1 {
        uplevel [list set $var [next]]
        uplevel $body
    }
}

Here is an example.

Say we want to write a command, scrape, which pulls down webpages. scrape has a '-o' option which takes a filename to write the data to, and a '-ssl' option that enables ssl in the http request. A scrape call that pulls down www.foo.com/index.html might look like this

    scrape www.foo.com/index.html -o foo.html -ssl

Here's how we can use iter to parse the options to scrape

proc scrape { addr args } {
    set outfile [lindex [split $addr /] end]
    set usessl  0

    iter op $args {
        switch $op {
            -o {
                set outfile [next]
            }
            -ssl {
                set usessl 1
            }
        }
    }
        
    # do scraping here
}

When the '-o' option is seen, the variable 'outfile' is set to the [next] element in the list, which is the filename passed after -o. In our case 'foo.html'. When that iteration finishes, and iter gets to the top of its loop, it will assign the variable after 'foo.html' to op. In our case this is '-ssl', which will be matched and the variable usessl will be set to 1. Theoretically you can nest loops that keep nexting, but I haven't tried that yet.

-- About a month later... --

Looking over this code now, I think I see how it can be abstracted another level. We can bundle the call to [iter] and switch inside a command, [argparse]. Then we can write the above like this.

argparse $args {
    -o   { set outfile [next] }
    -ssl { set usessl 1 }
}

Nice. So what does the argparse command look like?

proc argparse { options switch_body } {
    uplevel [subst {iter op $options { switch \$op { $switch_body } }}]
}

And now we write scrape with the new abstraction

proc scrape { addr args } {
    set outfile [lindex [split $addr /] end]
    set usessl  0

    argparse $args {
        -o   { set outfile [next] }
        -ssl { set usessl 1 }
    }
        
    # do scraping here
}