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.