Options and Arguments

The "official" or "maintained" version of the code below is at tcl-hacks in modules/options-0.tm, accompanied by a TIP#288-capable version of arguments in modules/tip288-0.tm.

Supporting options like the core commands do is a common itch for Tcl'ers, as evidenced by the number of pages in Category Argument Processing! Here's a version I found recently in an old code folder, dusted off and have adopted as my go-to. The main features are that its use should be obvious, good error messages are generated and you don't end up writing a proc wrapper that takes two or more multi-line arguments.

Here's a demo:

proc what {args} {
    options {{-loud} {-colour red green blue black} {-count 5}}
    arguments {this {that {}} args}
    foreach name [info locals] {
        puts "$name = [set $name]"
    }
}

Most of that should be obvious, so I'll just show off how it handles errors:

  % what
  wrong # args: should be "what this ?that? ?args ...?"
  % what -co la
  Ambiguous option -co: maybe one of -colour -count
  % what -colour greenish
  Bad colour "greenish": must be one of red green blue black

And the module, which is hopefully short enough to act as its own documentation (and encouragement to extend!).

# commented sections are questionable support for validation of arguments (not opts - conflicts with multi-value form).

namespace eval options {

    proc options {args} {
        while {[llength $args] > 1} {
            if {[string match [lindex $args 0]* -arrayvariable]} {
                set args [lassign $args _ value]
                set name -arrayvariable
                set options($name) $value
            } else {
                error "Unknown option \"[lindex $args 0]\": must be one of -arrayvariable"
            }
        }
        if {[info exists options(-arrayvariable)]} {
            set upset [format {apply {{name value} {
                    uplevel 2 [list set %s(-$name) $value]
            }}} $value]
        } else {
            set upset [format {apply {{name value} {
                    uplevel 2 [list set $name $value]
            }}}]
        }
        # parse optspec
        set spec [lindex $args 0]
        foreach optspec $spec {
            set name [lindex $optspec 0]
            switch [llength $optspec] {
                1 {
                    dict set opts $name type 0 ;# flag
                    {*}$upset [string range $name 1 end] 0
                    #dict set opts $name value 0
                } 
                2 {
                    dict set opts $name type 1 ;# arbitrary value
                    dict set opts $name default [lindex $optspec 1]
                    {*}$upset [string range $name 1 end] [lindex $optspec 1]
                    #dict set opts $name value [lindex $optspec 1]
                }
                default {
                    dict set opts $name type 2 ;# choice
                    dict set opts $name default [lindex $optspec 1]
                    dict set opts $name values [lrange $optspec 1 end]
                    {*}$upset [string range $name 1 end] [lindex $optspec 1]
                }
            }
        }
        # get caller's args
        upvar 1 args argv
        for {set i 0} {$i<[llength $argv]} {} {
            set arg [lindex $argv $i]
            if {![string match -* $arg]} {
                break
            }
            incr i
            if {$arg eq "--"} {
                break
            }
            set candidates [dict filter $opts key $arg*]
            switch [dict size $candidates] {
                0 {
                    return -code error -level 2 "Unknown option $arg: must be one of [dict keys $opts]"
                }
                1 {
                    dict for {name spec} $candidates {break}
                    set name [string range $name 1 end]
                    dict with spec {} ;# look out
                    if {$type==0} {
                        set value 1
                    } else {
                        if {[llength $argv]<($i+1)} {
                            return -code error -level 2 "Option $name requires a value"
                        }
                        set value [lindex $argv $i]
                        if {$type==2} {
                            set is [lsearch -all -glob $values $value*]
                            switch [llength $is] {
                                1 {
                                    set value [lindex $values $is]
                                }
                                0 {
                                    return -code error -level 2 "Bad $name \"$value\": must be one of $values"
                                }
                                default {
                                    return -code error -level 2 "Ambiguous $name \"$value\": could be any of [lmap i $is {lindex $values $i}]"
                                }
                            }
                        }
                        incr i
                    }
                    {*}$upset $name $value
                }
                default {
                    return -code error -level 2 "Ambiguous option $arg: maybe one of [dict keys $candidates]"
                }
            }
        }
        set argv [lrange $argv $i end]
    }

    proc formatArgspec {argspec} {
        foreach arg $argspec {
            if {[llength $arg]>1} {
                lappend res "?[lindex $arg 0]?"
            } elseif {$arg eq "args"} {
                lappend res "?args ...?"
            } else {
                lappend res $arg
            }
        }
        return [join $res " "]
    }

    proc arguments {argspec} {
        upvar 1 args argv
        for {set i 0} {$i<[llength $argv]} {incr i} {
            if {$i >= [llength $argspec]} {
                return -code error -level 2 "wrong # args: should be \"[lindex [info level -1] 0] [formatArgspec $argspec]\""
            }
            set name [lindex $argspec $i 0]
            if {$name eq "args"} {
                uplevel 1 [list set args [lrange $argv $i end]]
                return
            }
            set value [lindex $argv $i]
#            set test [lindex $argspec $i 2]
#            if {$test != ""} {
#                set valid [uplevel 1 $test $value]
#                if {!$value} {
#                    return -code error -level 2 "Invalid $name \"$value\", must be $test"
#                }
#            }
            uplevel 1 [list set $name $value]
        }
        # defaults:
        for {} {$i < [llength $argspec]} {incr i} {
            set as [lindex $argspec $i]
            if {[llength $as]==1} {
                if {$as ne "args"} {
                    return -code error -level 2 "wrong # args: should be \"[lindex [info level -1] 0] [formatArgspec $argspec]\""
                }
                upvar 1 args args
                set args [lrange $argv $i end]
                return
            }
            lassign $as name value
#            set test [lindex $argspec $i 2]
#            if {$test != ""} {
#                set valid [uplevel 1 $test $value]
#                if {!$value} {
#                    return -code error -level 2 "Invalid $name \"$value\", must be $test"
#                }
#            }
            uplevel 1 [list set $name $value]
        }
    }

    namespace export options arguments
}
namespace import options::*

aspect 2015-03-16 development notes: kap raised a question about gathering options in an array, which make me think that having options return a dict of the variables it has set might be a good idea. The current return value of $args isn't particularly useful, and wasn't designed.

Note I have a TIP#288 implementation in my local version of arguments, which will make it into a chiselapp repo once I can find a spare tuit.