jpayne@69: # optparse.tcl -- jpayne@69: # jpayne@69: # (private) Option parsing package jpayne@69: # Primarily used internally by the safe:: code. jpayne@69: # jpayne@69: # WARNING: This code will go away in a future release jpayne@69: # of Tcl. It is NOT supported and you should not rely jpayne@69: # on it. If your code does rely on this package you jpayne@69: # may directly incorporate this code into your application. jpayne@69: jpayne@69: package require Tcl 8.5- jpayne@69: # When this version number changes, update the pkgIndex.tcl file jpayne@69: # and the install directory in the Makefiles. jpayne@69: package provide opt 0.4.8 jpayne@69: jpayne@69: namespace eval ::tcl { jpayne@69: jpayne@69: # Exported APIs jpayne@69: namespace export OptKeyRegister OptKeyDelete OptKeyError OptKeyParse \ jpayne@69: OptProc OptProcArgGiven OptParse \ jpayne@69: Lempty Lget \ jpayne@69: Lassign Lvarpop Lvarpop1 Lvarset Lvarincr \ jpayne@69: SetMax SetMin jpayne@69: jpayne@69: jpayne@69: ################# Example of use / 'user documentation' ################### jpayne@69: jpayne@69: proc OptCreateTestProc {} { jpayne@69: jpayne@69: # Defines ::tcl::OptParseTest as a test proc with parsed arguments jpayne@69: # (can't be defined before the code below is loaded (before "OptProc")) jpayne@69: jpayne@69: # Every OptProc give usage information on "procname -help". jpayne@69: # Try "tcl::OptParseTest -help" and "tcl::OptParseTest -a" and jpayne@69: # then other arguments. jpayne@69: # jpayne@69: # example of 'valid' call: jpayne@69: # ::tcl::OptParseTest save -4 -pr 23 -libsok SybTcl\ jpayne@69: # -nostatics false ch1 jpayne@69: OptProc OptParseTest { jpayne@69: {subcommand -choice {save print} "sub command"} jpayne@69: {arg1 3 "some number"} jpayne@69: {-aflag} jpayne@69: {-intflag 7} jpayne@69: {-weirdflag "help string"} jpayne@69: {-noStatics "Not ok to load static packages"} jpayne@69: {-nestedloading1 true "OK to load into nested children"} jpayne@69: {-nestedloading2 -boolean true "OK to load into nested children"} jpayne@69: {-libsOK -choice {Tk SybTcl} jpayne@69: "List of packages that can be loaded"} jpayne@69: {-precision -int 12 "Number of digits of precision"} jpayne@69: {-intval 7 "An integer"} jpayne@69: {-scale -float 1.0 "Scale factor"} jpayne@69: {-zoom 1.0 "Zoom factor"} jpayne@69: {-arbitrary foobar "Arbitrary string"} jpayne@69: {-random -string 12 "Random string"} jpayne@69: {-listval -list {} "List value"} jpayne@69: {-blahflag -blah abc "Funny type"} jpayne@69: {arg2 -boolean "a boolean"} jpayne@69: {arg3 -choice "ch1 ch2"} jpayne@69: {?optarg? -list {} "optional argument"} jpayne@69: } { jpayne@69: foreach v [info locals] { jpayne@69: puts stderr [format "%14s : %s" $v [set $v]] jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: ################### No User serviceable part below ! ############### jpayne@69: jpayne@69: # Array storing the parsed descriptions jpayne@69: variable OptDesc jpayne@69: array set OptDesc {} jpayne@69: # Next potentially free key id (numeric) jpayne@69: variable OptDescN 0 jpayne@69: jpayne@69: # Inside algorithm/mechanism description: jpayne@69: # (not for the faint hearted ;-) jpayne@69: # jpayne@69: # The argument description is parsed into a "program tree" jpayne@69: # It is called a "program" because it is the program used by jpayne@69: # the state machine interpreter that use that program to jpayne@69: # actually parse the arguments at run time. jpayne@69: # jpayne@69: # The general structure of a "program" is jpayne@69: # notation (pseudo bnf like) jpayne@69: # name :== definition defines "name" as being "definition" jpayne@69: # { x y z } means list of x, y, and z jpayne@69: # x* means x repeated 0 or more time jpayne@69: # x+ means "x x*" jpayne@69: # x? means optionally x jpayne@69: # x | y means x or y jpayne@69: # "cccc" means the literal string jpayne@69: # jpayne@69: # program :== { programCounter programStep* } jpayne@69: # jpayne@69: # programStep :== program | singleStep jpayne@69: # jpayne@69: # programCounter :== {"P" integer+ } jpayne@69: # jpayne@69: # singleStep :== { instruction parameters* } jpayne@69: # jpayne@69: # instruction :== single element list jpayne@69: # jpayne@69: # (the difference between singleStep and program is that \ jpayne@69: # llength [lindex $program 0] >= 2 jpayne@69: # while jpayne@69: # llength [lindex $singleStep 0] == 1 jpayne@69: # ) jpayne@69: # jpayne@69: # And for this application: jpayne@69: # jpayne@69: # singleStep :== { instruction varname {hasBeenSet currentValue} type jpayne@69: # typeArgs help } jpayne@69: # instruction :== "flags" | "value" jpayne@69: # type :== knowType | anyword jpayne@69: # knowType :== "string" | "int" | "boolean" | "boolflag" | "float" jpayne@69: # | "choice" jpayne@69: # jpayne@69: # for type "choice" typeArgs is a list of possible choices, the first one jpayne@69: # is the default value. for all other types the typeArgs is the default value jpayne@69: # jpayne@69: # a "boolflag" is the type for a flag whose presence or absence, without jpayne@69: # additional arguments means respectively true or false (default flag type). jpayne@69: # jpayne@69: # programCounter is the index in the list of the currently processed jpayne@69: # programStep (thus starting at 1 (0 is {"P" prgCounterValue}). jpayne@69: # If it is a list it points toward each currently selected programStep. jpayne@69: # (like for "flags", as they are optional, form a set and programStep). jpayne@69: jpayne@69: # Performance/Implementation issues jpayne@69: # --------------------------------- jpayne@69: # We use tcl lists instead of arrays because with tcl8.0 jpayne@69: # they should start to be much faster. jpayne@69: # But this code use a lot of helper procs (like Lvarset) jpayne@69: # which are quite slow and would be helpfully optimized jpayne@69: # for instance by being written in C. Also our struture jpayne@69: # is complex and there is maybe some places where the jpayne@69: # string rep might be calculated at great exense. to be checked. jpayne@69: jpayne@69: # jpayne@69: # Parse a given description and saves it here under the given key jpayne@69: # generate a unused keyid if not given jpayne@69: # jpayne@69: proc ::tcl::OptKeyRegister {desc {key ""}} { jpayne@69: variable OptDesc jpayne@69: variable OptDescN jpayne@69: if {[string equal $key ""]} { jpayne@69: # in case a key given to us as a parameter was a number jpayne@69: while {[info exists OptDesc($OptDescN)]} {incr OptDescN} jpayne@69: set key $OptDescN jpayne@69: incr OptDescN jpayne@69: } jpayne@69: # program counter jpayne@69: set program [list [list "P" 1]] jpayne@69: jpayne@69: # are we processing flags (which makes a single program step) jpayne@69: set inflags 0 jpayne@69: jpayne@69: set state {} jpayne@69: jpayne@69: # flag used to detect that we just have a single (flags set) subprogram. jpayne@69: set empty 1 jpayne@69: jpayne@69: foreach item $desc { jpayne@69: if {$state == "args"} { jpayne@69: # more items after 'args'... jpayne@69: return -code error "'args' special argument must be the last one" jpayne@69: } jpayne@69: set res [OptNormalizeOne $item] jpayne@69: set state [lindex $res 0] jpayne@69: if {$inflags} { jpayne@69: if {$state == "flags"} { jpayne@69: # add to 'subprogram' jpayne@69: lappend flagsprg $res jpayne@69: } else { jpayne@69: # put in the flags jpayne@69: # structure for flag programs items is a list of jpayne@69: # {subprgcounter {prg flag 1} {prg flag 2} {...}} jpayne@69: lappend program $flagsprg jpayne@69: # put the other regular stuff jpayne@69: lappend program $res jpayne@69: set inflags 0 jpayne@69: set empty 0 jpayne@69: } jpayne@69: } else { jpayne@69: if {$state == "flags"} { jpayne@69: set inflags 1 jpayne@69: # sub program counter + first sub program jpayne@69: set flagsprg [list [list "P" 1] $res] jpayne@69: } else { jpayne@69: lappend program $res jpayne@69: set empty 0 jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: if {$inflags} { jpayne@69: if {$empty} { jpayne@69: # We just have the subprogram, optimize and remove jpayne@69: # unneeded level: jpayne@69: set program $flagsprg jpayne@69: } else { jpayne@69: lappend program $flagsprg jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: set OptDesc($key) $program jpayne@69: jpayne@69: return $key jpayne@69: } jpayne@69: jpayne@69: # jpayne@69: # Free the storage for that given key jpayne@69: # jpayne@69: proc ::tcl::OptKeyDelete {key} { jpayne@69: variable OptDesc jpayne@69: unset OptDesc($key) jpayne@69: } jpayne@69: jpayne@69: # Get the parsed description stored under the given key. jpayne@69: proc OptKeyGetDesc {descKey} { jpayne@69: variable OptDesc jpayne@69: if {![info exists OptDesc($descKey)]} { jpayne@69: return -code error "Unknown option description key \"$descKey\"" jpayne@69: } jpayne@69: set OptDesc($descKey) jpayne@69: } jpayne@69: jpayne@69: # Parse entry point for ppl who don't want to register with a key, jpayne@69: # for instance because the description changes dynamically. jpayne@69: # (otherwise one should really use OptKeyRegister once + OptKeyParse jpayne@69: # as it is way faster or simply OptProc which does it all) jpayne@69: # Assign a temporary key, call OptKeyParse and then free the storage jpayne@69: proc ::tcl::OptParse {desc arglist} { jpayne@69: set tempkey [OptKeyRegister $desc] jpayne@69: set ret [catch {uplevel 1 [list ::tcl::OptKeyParse $tempkey $arglist]} res] jpayne@69: OptKeyDelete $tempkey jpayne@69: return -code $ret $res jpayne@69: } jpayne@69: jpayne@69: # Helper function, replacement for proc that both jpayne@69: # register the description under a key which is the name of the proc jpayne@69: # (and thus unique to that code) jpayne@69: # and add a first line to the code to call the OptKeyParse proc jpayne@69: # Stores the list of variables that have been actually given by the user jpayne@69: # (the other will be sets to their default value) jpayne@69: # into local variable named "Args". jpayne@69: proc ::tcl::OptProc {name desc body} { jpayne@69: set namespace [uplevel 1 [list ::namespace current]] jpayne@69: if {[string match "::*" $name] || [string equal $namespace "::"]} { jpayne@69: # absolute name or global namespace, name is the key jpayne@69: set key $name jpayne@69: } else { jpayne@69: # we are relative to some non top level namespace: jpayne@69: set key "${namespace}::${name}" jpayne@69: } jpayne@69: OptKeyRegister $desc $key jpayne@69: uplevel 1 [list ::proc $name args "set Args \[::tcl::OptKeyParse $key \$args\]\n$body"] jpayne@69: return $key jpayne@69: } jpayne@69: # Check that a argument has been given jpayne@69: # assumes that "OptProc" has been used as it will check in "Args" list jpayne@69: proc ::tcl::OptProcArgGiven {argname} { jpayne@69: upvar Args alist jpayne@69: expr {[lsearch $alist $argname] >=0} jpayne@69: } jpayne@69: jpayne@69: ####### jpayne@69: # Programs/Descriptions manipulation jpayne@69: jpayne@69: # Return the instruction word/list of a given step/(sub)program jpayne@69: proc OptInstr {lst} { jpayne@69: lindex $lst 0 jpayne@69: } jpayne@69: # Is a (sub) program or a plain instruction ? jpayne@69: proc OptIsPrg {lst} { jpayne@69: expr {[llength [OptInstr $lst]]>=2} jpayne@69: } jpayne@69: # Is this instruction a program counter or a real instr jpayne@69: proc OptIsCounter {item} { jpayne@69: expr {[lindex $item 0]=="P"} jpayne@69: } jpayne@69: # Current program counter (2nd word of first word) jpayne@69: proc OptGetPrgCounter {lst} { jpayne@69: Lget $lst {0 1} jpayne@69: } jpayne@69: # Current program counter (2nd word of first word) jpayne@69: proc OptSetPrgCounter {lstName newValue} { jpayne@69: upvar $lstName lst jpayne@69: set lst [lreplace $lst 0 0 [concat "P" $newValue]] jpayne@69: } jpayne@69: # returns a list of currently selected items. jpayne@69: proc OptSelection {lst} { jpayne@69: set res {} jpayne@69: foreach idx [lrange [lindex $lst 0] 1 end] { jpayne@69: lappend res [Lget $lst $idx] jpayne@69: } jpayne@69: return $res jpayne@69: } jpayne@69: jpayne@69: # Advance to next description jpayne@69: proc OptNextDesc {descName} { jpayne@69: uplevel 1 [list Lvarincr $descName {0 1}] jpayne@69: } jpayne@69: jpayne@69: # Get the current description, eventually descend jpayne@69: proc OptCurDesc {descriptions} { jpayne@69: lindex $descriptions [OptGetPrgCounter $descriptions] jpayne@69: } jpayne@69: # get the current description, eventually descend jpayne@69: # through sub programs as needed. jpayne@69: proc OptCurDescFinal {descriptions} { jpayne@69: set item [OptCurDesc $descriptions] jpayne@69: # Descend untill we get the actual item and not a sub program jpayne@69: while {[OptIsPrg $item]} { jpayne@69: set item [OptCurDesc $item] jpayne@69: } jpayne@69: return $item jpayne@69: } jpayne@69: # Current final instruction adress jpayne@69: proc OptCurAddr {descriptions {start {}}} { jpayne@69: set adress [OptGetPrgCounter $descriptions] jpayne@69: lappend start $adress jpayne@69: set item [lindex $descriptions $adress] jpayne@69: if {[OptIsPrg $item]} { jpayne@69: return [OptCurAddr $item $start] jpayne@69: } else { jpayne@69: return $start jpayne@69: } jpayne@69: } jpayne@69: # Set the value field of the current instruction jpayne@69: proc OptCurSetValue {descriptionsName value} { jpayne@69: upvar $descriptionsName descriptions jpayne@69: # get the current item full adress jpayne@69: set adress [OptCurAddr $descriptions] jpayne@69: # use the 3th field of the item (see OptValue / OptNewInst) jpayne@69: lappend adress 2 jpayne@69: Lvarset descriptions $adress [list 1 $value] jpayne@69: # ^hasBeenSet flag jpayne@69: } jpayne@69: jpayne@69: # empty state means done/paste the end of the program jpayne@69: proc OptState {item} { jpayne@69: lindex $item 0 jpayne@69: } jpayne@69: jpayne@69: # current state jpayne@69: proc OptCurState {descriptions} { jpayne@69: OptState [OptCurDesc $descriptions] jpayne@69: } jpayne@69: jpayne@69: ####### jpayne@69: # Arguments manipulation jpayne@69: jpayne@69: # Returns the argument that has to be processed now jpayne@69: proc OptCurrentArg {lst} { jpayne@69: lindex $lst 0 jpayne@69: } jpayne@69: # Advance to next argument jpayne@69: proc OptNextArg {argsName} { jpayne@69: uplevel 1 [list Lvarpop1 $argsName] jpayne@69: } jpayne@69: ####### jpayne@69: jpayne@69: jpayne@69: jpayne@69: jpayne@69: jpayne@69: # Loop over all descriptions, calling OptDoOne which will jpayne@69: # eventually eat all the arguments. jpayne@69: proc OptDoAll {descriptionsName argumentsName} { jpayne@69: upvar $descriptionsName descriptions jpayne@69: upvar $argumentsName arguments jpayne@69: # puts "entered DoAll" jpayne@69: # Nb: the places where "state" can be set are tricky to figure jpayne@69: # because DoOne sets the state to flagsValue and return -continue jpayne@69: # when needed... jpayne@69: set state [OptCurState $descriptions] jpayne@69: # We'll exit the loop in "OptDoOne" or when state is empty. jpayne@69: while 1 { jpayne@69: set curitem [OptCurDesc $descriptions] jpayne@69: # Do subprograms if needed, call ourselves on the sub branch jpayne@69: while {[OptIsPrg $curitem]} { jpayne@69: OptDoAll curitem arguments jpayne@69: # puts "done DoAll sub" jpayne@69: # Insert back the results in current tree jpayne@69: Lvarset1nc descriptions [OptGetPrgCounter $descriptions]\ jpayne@69: $curitem jpayne@69: OptNextDesc descriptions jpayne@69: set curitem [OptCurDesc $descriptions] jpayne@69: set state [OptCurState $descriptions] jpayne@69: } jpayne@69: # puts "state = \"$state\" - arguments=($arguments)" jpayne@69: if {[Lempty $state]} { jpayne@69: # Nothing left to do, we are done in this branch: jpayne@69: break jpayne@69: } jpayne@69: # The following statement can make us terminate/continue jpayne@69: # as it use return -code {break, continue, return and error} jpayne@69: # codes jpayne@69: OptDoOne descriptions state arguments jpayne@69: # If we are here, no special return code where issued, jpayne@69: # we'll step to next instruction : jpayne@69: # puts "new state = \"$state\"" jpayne@69: OptNextDesc descriptions jpayne@69: set state [OptCurState $descriptions] jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: # Process one step for the state machine, jpayne@69: # eventually consuming the current argument. jpayne@69: proc OptDoOne {descriptionsName stateName argumentsName} { jpayne@69: upvar $argumentsName arguments jpayne@69: upvar $descriptionsName descriptions jpayne@69: upvar $stateName state jpayne@69: jpayne@69: # the special state/instruction "args" eats all jpayne@69: # the remaining args (if any) jpayne@69: if {($state == "args")} { jpayne@69: if {![Lempty $arguments]} { jpayne@69: # If there is no additional arguments, leave the default value jpayne@69: # in. jpayne@69: OptCurSetValue descriptions $arguments jpayne@69: set arguments {} jpayne@69: } jpayne@69: # puts "breaking out ('args' state: consuming every reminding args)" jpayne@69: return -code break jpayne@69: } jpayne@69: jpayne@69: if {[Lempty $arguments]} { jpayne@69: if {$state == "flags"} { jpayne@69: # no argument and no flags : we're done jpayne@69: # puts "returning to previous (sub)prg (no more args)" jpayne@69: return -code return jpayne@69: } elseif {$state == "optValue"} { jpayne@69: set state next; # not used, for debug only jpayne@69: # go to next state jpayne@69: return jpayne@69: } else { jpayne@69: return -code error [OptMissingValue $descriptions] jpayne@69: } jpayne@69: } else { jpayne@69: set arg [OptCurrentArg $arguments] jpayne@69: } jpayne@69: jpayne@69: switch $state { jpayne@69: flags { jpayne@69: # A non-dash argument terminates the options, as does -- jpayne@69: jpayne@69: # Still a flag ? jpayne@69: if {![OptIsFlag $arg]} { jpayne@69: # don't consume the argument, return to previous prg jpayne@69: return -code return jpayne@69: } jpayne@69: # consume the flag jpayne@69: OptNextArg arguments jpayne@69: if {[string equal "--" $arg]} { jpayne@69: # return from 'flags' state jpayne@69: return -code return jpayne@69: } jpayne@69: jpayne@69: set hits [OptHits descriptions $arg] jpayne@69: if {$hits > 1} { jpayne@69: return -code error [OptAmbigous $descriptions $arg] jpayne@69: } elseif {$hits == 0} { jpayne@69: return -code error [OptFlagUsage $descriptions $arg] jpayne@69: } jpayne@69: set item [OptCurDesc $descriptions] jpayne@69: if {[OptNeedValue $item]} { jpayne@69: # we need a value, next state is jpayne@69: set state flagValue jpayne@69: } else { jpayne@69: OptCurSetValue descriptions 1 jpayne@69: } jpayne@69: # continue jpayne@69: return -code continue jpayne@69: } jpayne@69: flagValue - jpayne@69: value { jpayne@69: set item [OptCurDesc $descriptions] jpayne@69: # Test the values against their required type jpayne@69: if {[catch {OptCheckType $arg\ jpayne@69: [OptType $item] [OptTypeArgs $item]} val]} { jpayne@69: return -code error [OptBadValue $item $arg $val] jpayne@69: } jpayne@69: # consume the value jpayne@69: OptNextArg arguments jpayne@69: # set the value jpayne@69: OptCurSetValue descriptions $val jpayne@69: # go to next state jpayne@69: if {$state == "flagValue"} { jpayne@69: set state flags jpayne@69: return -code continue jpayne@69: } else { jpayne@69: set state next; # not used, for debug only jpayne@69: return ; # will go on next step jpayne@69: } jpayne@69: } jpayne@69: optValue { jpayne@69: set item [OptCurDesc $descriptions] jpayne@69: # Test the values against their required type jpayne@69: if {![catch {OptCheckType $arg\ jpayne@69: [OptType $item] [OptTypeArgs $item]} val]} { jpayne@69: # right type, so : jpayne@69: # consume the value jpayne@69: OptNextArg arguments jpayne@69: # set the value jpayne@69: OptCurSetValue descriptions $val jpayne@69: } jpayne@69: # go to next state jpayne@69: set state next; # not used, for debug only jpayne@69: return ; # will go on next step jpayne@69: } jpayne@69: } jpayne@69: # If we reach this point: an unknown jpayne@69: # state as been entered ! jpayne@69: return -code error "Bug! unknown state in DoOne \"$state\"\ jpayne@69: (prg counter [OptGetPrgCounter $descriptions]:\ jpayne@69: [OptCurDesc $descriptions])" jpayne@69: } jpayne@69: jpayne@69: # Parse the options given the key to previously registered description jpayne@69: # and arguments list jpayne@69: proc ::tcl::OptKeyParse {descKey arglist} { jpayne@69: jpayne@69: set desc [OptKeyGetDesc $descKey] jpayne@69: jpayne@69: # make sure -help always give usage jpayne@69: if {[string equal -nocase "-help" $arglist]} { jpayne@69: return -code error [OptError "Usage information:" $desc 1] jpayne@69: } jpayne@69: jpayne@69: OptDoAll desc arglist jpayne@69: jpayne@69: if {![Lempty $arglist]} { jpayne@69: return -code error [OptTooManyArgs $desc $arglist] jpayne@69: } jpayne@69: jpayne@69: # Analyse the result jpayne@69: # Walk through the tree: jpayne@69: OptTreeVars $desc "#[expr {[info level]-1}]" jpayne@69: } jpayne@69: jpayne@69: # determine string length for nice tabulated output jpayne@69: proc OptTreeVars {desc level {vnamesLst {}}} { jpayne@69: foreach item $desc { jpayne@69: if {[OptIsCounter $item]} continue jpayne@69: if {[OptIsPrg $item]} { jpayne@69: set vnamesLst [OptTreeVars $item $level $vnamesLst] jpayne@69: } else { jpayne@69: set vname [OptVarName $item] jpayne@69: upvar $level $vname var jpayne@69: if {[OptHasBeenSet $item]} { jpayne@69: # puts "adding $vname" jpayne@69: # lets use the input name for the returned list jpayne@69: # it is more usefull, for instance you can check that jpayne@69: # no flags at all was given with expr jpayne@69: # {![string match "*-*" $Args]} jpayne@69: lappend vnamesLst [OptName $item] jpayne@69: set var [OptValue $item] jpayne@69: } else { jpayne@69: set var [OptDefaultValue $item] jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: return $vnamesLst jpayne@69: } jpayne@69: jpayne@69: jpayne@69: # Check the type of a value jpayne@69: # and emit an error if arg is not of the correct type jpayne@69: # otherwise returns the canonical value of that arg (ie 0/1 for booleans) jpayne@69: proc ::tcl::OptCheckType {arg type {typeArgs ""}} { jpayne@69: # puts "checking '$arg' against '$type' ($typeArgs)" jpayne@69: jpayne@69: # only types "any", "choice", and numbers can have leading "-" jpayne@69: jpayne@69: switch -exact -- $type { jpayne@69: int { jpayne@69: if {![string is integer -strict $arg]} { jpayne@69: error "not an integer" jpayne@69: } jpayne@69: return $arg jpayne@69: } jpayne@69: float { jpayne@69: return [expr {double($arg)}] jpayne@69: } jpayne@69: script - jpayne@69: list { jpayne@69: # if llength fail : malformed list jpayne@69: if {[llength $arg]==0 && [OptIsFlag $arg]} { jpayne@69: error "no values with leading -" jpayne@69: } jpayne@69: return $arg jpayne@69: } jpayne@69: boolean { jpayne@69: if {![string is boolean -strict $arg]} { jpayne@69: error "non canonic boolean" jpayne@69: } jpayne@69: # convert true/false because expr/if is broken with "!,... jpayne@69: return [expr {$arg ? 1 : 0}] jpayne@69: } jpayne@69: choice { jpayne@69: if {$arg ni $typeArgs} { jpayne@69: error "invalid choice" jpayne@69: } jpayne@69: return $arg jpayne@69: } jpayne@69: any { jpayne@69: return $arg jpayne@69: } jpayne@69: string - jpayne@69: default { jpayne@69: if {[OptIsFlag $arg]} { jpayne@69: error "no values with leading -" jpayne@69: } jpayne@69: return $arg jpayne@69: } jpayne@69: } jpayne@69: return neverReached jpayne@69: } jpayne@69: jpayne@69: # internal utilities jpayne@69: jpayne@69: # returns the number of flags matching the given arg jpayne@69: # sets the (local) prg counter to the list of matches jpayne@69: proc OptHits {descName arg} { jpayne@69: upvar $descName desc jpayne@69: set hits 0 jpayne@69: set hitems {} jpayne@69: set i 1 jpayne@69: jpayne@69: set larg [string tolower $arg] jpayne@69: set len [string length $larg] jpayne@69: set last [expr {$len-1}] jpayne@69: jpayne@69: foreach item [lrange $desc 1 end] { jpayne@69: set flag [OptName $item] jpayne@69: # lets try to match case insensitively jpayne@69: # (string length ought to be cheap) jpayne@69: set lflag [string tolower $flag] jpayne@69: if {$len == [string length $lflag]} { jpayne@69: if {[string equal $larg $lflag]} { jpayne@69: # Exact match case jpayne@69: OptSetPrgCounter desc $i jpayne@69: return 1 jpayne@69: } jpayne@69: } elseif {[string equal $larg [string range $lflag 0 $last]]} { jpayne@69: lappend hitems $i jpayne@69: incr hits jpayne@69: } jpayne@69: incr i jpayne@69: } jpayne@69: if {$hits} { jpayne@69: OptSetPrgCounter desc $hitems jpayne@69: } jpayne@69: return $hits jpayne@69: } jpayne@69: jpayne@69: # Extract fields from the list structure: jpayne@69: jpayne@69: proc OptName {item} { jpayne@69: lindex $item 1 jpayne@69: } jpayne@69: proc OptHasBeenSet {item} { jpayne@69: Lget $item {2 0} jpayne@69: } jpayne@69: proc OptValue {item} { jpayne@69: Lget $item {2 1} jpayne@69: } jpayne@69: jpayne@69: proc OptIsFlag {name} { jpayne@69: string match "-*" $name jpayne@69: } jpayne@69: proc OptIsOpt {name} { jpayne@69: string match {\?*} $name jpayne@69: } jpayne@69: proc OptVarName {item} { jpayne@69: set name [OptName $item] jpayne@69: if {[OptIsFlag $name]} { jpayne@69: return [string range $name 1 end] jpayne@69: } elseif {[OptIsOpt $name]} { jpayne@69: return [string trim $name "?"] jpayne@69: } else { jpayne@69: return $name jpayne@69: } jpayne@69: } jpayne@69: proc OptType {item} { jpayne@69: lindex $item 3 jpayne@69: } jpayne@69: proc OptTypeArgs {item} { jpayne@69: lindex $item 4 jpayne@69: } jpayne@69: proc OptHelp {item} { jpayne@69: lindex $item 5 jpayne@69: } jpayne@69: proc OptNeedValue {item} { jpayne@69: expr {![string equal [OptType $item] boolflag]} jpayne@69: } jpayne@69: proc OptDefaultValue {item} { jpayne@69: set val [OptTypeArgs $item] jpayne@69: switch -exact -- [OptType $item] { jpayne@69: choice {return [lindex $val 0]} jpayne@69: boolean - jpayne@69: boolflag { jpayne@69: # convert back false/true to 0/1 because expr !$bool jpayne@69: # is broken.. jpayne@69: if {$val} { jpayne@69: return 1 jpayne@69: } else { jpayne@69: return 0 jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: return $val jpayne@69: } jpayne@69: jpayne@69: # Description format error helper jpayne@69: proc OptOptUsage {item {what ""}} { jpayne@69: return -code error "invalid description format$what: $item\n\ jpayne@69: should be a list of {varname|-flagname ?-type? ?defaultvalue?\ jpayne@69: ?helpstring?}" jpayne@69: } jpayne@69: jpayne@69: jpayne@69: # Generate a canonical form single instruction jpayne@69: proc OptNewInst {state varname type typeArgs help} { jpayne@69: list $state $varname [list 0 {}] $type $typeArgs $help jpayne@69: # ^ ^ jpayne@69: # | | jpayne@69: # hasBeenSet=+ +=currentValue jpayne@69: } jpayne@69: jpayne@69: # Translate one item to canonical form jpayne@69: proc OptNormalizeOne {item} { jpayne@69: set lg [Lassign $item varname arg1 arg2 arg3] jpayne@69: # puts "called optnormalizeone '$item' v=($varname), lg=$lg" jpayne@69: set isflag [OptIsFlag $varname] jpayne@69: set isopt [OptIsOpt $varname] jpayne@69: if {$isflag} { jpayne@69: set state "flags" jpayne@69: } elseif {$isopt} { jpayne@69: set state "optValue" jpayne@69: } elseif {![string equal $varname "args"]} { jpayne@69: set state "value" jpayne@69: } else { jpayne@69: set state "args" jpayne@69: } jpayne@69: jpayne@69: # apply 'smart' 'fuzzy' logic to try to make jpayne@69: # description writer's life easy, and our's difficult : jpayne@69: # let's guess the missing arguments :-) jpayne@69: jpayne@69: switch $lg { jpayne@69: 1 { jpayne@69: if {$isflag} { jpayne@69: return [OptNewInst $state $varname boolflag false ""] jpayne@69: } else { jpayne@69: return [OptNewInst $state $varname any "" ""] jpayne@69: } jpayne@69: } jpayne@69: 2 { jpayne@69: # varname default jpayne@69: # varname help jpayne@69: set type [OptGuessType $arg1] jpayne@69: if {[string equal $type "string"]} { jpayne@69: if {$isflag} { jpayne@69: set type boolflag jpayne@69: set def false jpayne@69: } else { jpayne@69: set type any jpayne@69: set def "" jpayne@69: } jpayne@69: set help $arg1 jpayne@69: } else { jpayne@69: set help "" jpayne@69: set def $arg1 jpayne@69: } jpayne@69: return [OptNewInst $state $varname $type $def $help] jpayne@69: } jpayne@69: 3 { jpayne@69: # varname type value jpayne@69: # varname value comment jpayne@69: jpayne@69: if {[regexp {^-(.+)$} $arg1 x type]} { jpayne@69: # flags/optValue as they are optional, need a "value", jpayne@69: # on the contrary, for a variable (non optional), jpayne@69: # default value is pointless, 'cept for choices : jpayne@69: if {$isflag || $isopt || ($type == "choice")} { jpayne@69: return [OptNewInst $state $varname $type $arg2 ""] jpayne@69: } else { jpayne@69: return [OptNewInst $state $varname $type "" $arg2] jpayne@69: } jpayne@69: } else { jpayne@69: return [OptNewInst $state $varname\ jpayne@69: [OptGuessType $arg1] $arg1 $arg2] jpayne@69: } jpayne@69: } jpayne@69: 4 { jpayne@69: if {[regexp {^-(.+)$} $arg1 x type]} { jpayne@69: return [OptNewInst $state $varname $type $arg2 $arg3] jpayne@69: } else { jpayne@69: return -code error [OptOptUsage $item] jpayne@69: } jpayne@69: } jpayne@69: default { jpayne@69: return -code error [OptOptUsage $item] jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: # Auto magic lazy type determination jpayne@69: proc OptGuessType {arg} { jpayne@69: if { $arg == "true" || $arg == "false" } { jpayne@69: return boolean jpayne@69: } jpayne@69: if {[string is integer -strict $arg]} { jpayne@69: return int jpayne@69: } jpayne@69: if {[string is double -strict $arg]} { jpayne@69: return float jpayne@69: } jpayne@69: return string jpayne@69: } jpayne@69: jpayne@69: # Error messages front ends jpayne@69: jpayne@69: proc OptAmbigous {desc arg} { jpayne@69: OptError "ambigous option \"$arg\", choose from:" [OptSelection $desc] jpayne@69: } jpayne@69: proc OptFlagUsage {desc arg} { jpayne@69: OptError "bad flag \"$arg\", must be one of" $desc jpayne@69: } jpayne@69: proc OptTooManyArgs {desc arguments} { jpayne@69: OptError "too many arguments (unexpected argument(s): $arguments),\ jpayne@69: usage:"\ jpayne@69: $desc 1 jpayne@69: } jpayne@69: proc OptParamType {item} { jpayne@69: if {[OptIsFlag $item]} { jpayne@69: return "flag" jpayne@69: } else { jpayne@69: return "parameter" jpayne@69: } jpayne@69: } jpayne@69: proc OptBadValue {item arg {err {}}} { jpayne@69: # puts "bad val err = \"$err\"" jpayne@69: OptError "bad value \"$arg\" for [OptParamType $item]"\ jpayne@69: [list $item] jpayne@69: } jpayne@69: proc OptMissingValue {descriptions} { jpayne@69: # set item [OptCurDescFinal $descriptions] jpayne@69: set item [OptCurDesc $descriptions] jpayne@69: OptError "no value given for [OptParamType $item] \"[OptName $item]\"\ jpayne@69: (use -help for full usage) :"\ jpayne@69: [list $item] jpayne@69: } jpayne@69: jpayne@69: proc ::tcl::OptKeyError {prefix descKey {header 0}} { jpayne@69: OptError $prefix [OptKeyGetDesc $descKey] $header jpayne@69: } jpayne@69: jpayne@69: # determine string length for nice tabulated output jpayne@69: proc OptLengths {desc nlName tlName dlName} { jpayne@69: upvar $nlName nl jpayne@69: upvar $tlName tl jpayne@69: upvar $dlName dl jpayne@69: foreach item $desc { jpayne@69: if {[OptIsCounter $item]} continue jpayne@69: if {[OptIsPrg $item]} { jpayne@69: OptLengths $item nl tl dl jpayne@69: } else { jpayne@69: SetMax nl [string length [OptName $item]] jpayne@69: SetMax tl [string length [OptType $item]] jpayne@69: set dv [OptTypeArgs $item] jpayne@69: if {[OptState $item] != "header"} { jpayne@69: set dv "($dv)" jpayne@69: } jpayne@69: set l [string length $dv] jpayne@69: # limit the space allocated to potentially big "choices" jpayne@69: if {([OptType $item] != "choice") || ($l<=12)} { jpayne@69: SetMax dl $l jpayne@69: } else { jpayne@69: if {![info exists dl]} { jpayne@69: set dl 0 jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: } jpayne@69: # output the tree jpayne@69: proc OptTree {desc nl tl dl} { jpayne@69: set res "" jpayne@69: foreach item $desc { jpayne@69: if {[OptIsCounter $item]} continue jpayne@69: if {[OptIsPrg $item]} { jpayne@69: append res [OptTree $item $nl $tl $dl] jpayne@69: } else { jpayne@69: set dv [OptTypeArgs $item] jpayne@69: if {[OptState $item] != "header"} { jpayne@69: set dv "($dv)" jpayne@69: } jpayne@69: append res [string trimright [format "\n %-*s %-*s %-*s %s" \ jpayne@69: $nl [OptName $item] $tl [OptType $item] \ jpayne@69: $dl $dv [OptHelp $item]]] jpayne@69: } jpayne@69: } jpayne@69: return $res jpayne@69: } jpayne@69: jpayne@69: # Give nice usage string jpayne@69: proc ::tcl::OptError {prefix desc {header 0}} { jpayne@69: # determine length jpayne@69: if {$header} { jpayne@69: # add faked instruction jpayne@69: set h [list [OptNewInst header Var/FlagName Type Value Help]] jpayne@69: lappend h [OptNewInst header ------------ ---- ----- ----] jpayne@69: lappend h [OptNewInst header {(-help} "" "" {gives this help)}] jpayne@69: set desc [concat $h $desc] jpayne@69: } jpayne@69: OptLengths $desc nl tl dl jpayne@69: # actually output jpayne@69: return "$prefix[OptTree $desc $nl $tl $dl]" jpayne@69: } jpayne@69: jpayne@69: jpayne@69: ################ General Utility functions ####################### jpayne@69: jpayne@69: # jpayne@69: # List utility functions jpayne@69: # Naming convention: jpayne@69: # "Lvarxxx" take the list VARiable name as argument jpayne@69: # "Lxxxx" take the list value as argument jpayne@69: # (which is not costly with Tcl8 objects system jpayne@69: # as it's still a reference and not a copy of the values) jpayne@69: # jpayne@69: jpayne@69: # Is that list empty ? jpayne@69: proc ::tcl::Lempty {list} { jpayne@69: expr {[llength $list]==0} jpayne@69: } jpayne@69: jpayne@69: # Gets the value of one leaf of a lists tree jpayne@69: proc ::tcl::Lget {list indexLst} { jpayne@69: if {[llength $indexLst] <= 1} { jpayne@69: return [lindex $list $indexLst] jpayne@69: } jpayne@69: Lget [lindex $list [lindex $indexLst 0]] [lrange $indexLst 1 end] jpayne@69: } jpayne@69: # Sets the value of one leaf of a lists tree jpayne@69: # (we use the version that does not create the elements because jpayne@69: # it would be even slower... needs to be written in C !) jpayne@69: # (nb: there is a non trivial recursive problem with indexes 0, jpayne@69: # which appear because there is no difference between a list jpayne@69: # of 1 element and 1 element alone : [list "a"] == "a" while jpayne@69: # it should be {a} and [listp a] should be 0 while [listp {a b}] would be 1 jpayne@69: # and [listp "a b"] maybe 0. listp does not exist either...) jpayne@69: proc ::tcl::Lvarset {listName indexLst newValue} { jpayne@69: upvar $listName list jpayne@69: if {[llength $indexLst] <= 1} { jpayne@69: Lvarset1nc list $indexLst $newValue jpayne@69: } else { jpayne@69: set idx [lindex $indexLst 0] jpayne@69: set targetList [lindex $list $idx] jpayne@69: # reduce refcount on targetList (not really usefull now, jpayne@69: # could be with optimizing compiler) jpayne@69: # Lvarset1 list $idx {} jpayne@69: # recursively replace in targetList jpayne@69: Lvarset targetList [lrange $indexLst 1 end] $newValue jpayne@69: # put updated sub list back in the tree jpayne@69: Lvarset1nc list $idx $targetList jpayne@69: } jpayne@69: } jpayne@69: # Set one cell to a value, eventually create all the needed elements jpayne@69: # (on level-1 of lists) jpayne@69: variable emptyList {} jpayne@69: proc ::tcl::Lvarset1 {listName index newValue} { jpayne@69: upvar $listName list jpayne@69: if {$index < 0} {return -code error "invalid negative index"} jpayne@69: set lg [llength $list] jpayne@69: if {$index >= $lg} { jpayne@69: variable emptyList jpayne@69: for {set i $lg} {$i<$index} {incr i} { jpayne@69: lappend list $emptyList jpayne@69: } jpayne@69: lappend list $newValue jpayne@69: } else { jpayne@69: set list [lreplace $list $index $index $newValue] jpayne@69: } jpayne@69: } jpayne@69: # same as Lvarset1 but no bound checking / creation jpayne@69: proc ::tcl::Lvarset1nc {listName index newValue} { jpayne@69: upvar $listName list jpayne@69: set list [lreplace $list $index $index $newValue] jpayne@69: } jpayne@69: # Increments the value of one leaf of a lists tree jpayne@69: # (which must exists) jpayne@69: proc ::tcl::Lvarincr {listName indexLst {howMuch 1}} { jpayne@69: upvar $listName list jpayne@69: if {[llength $indexLst] <= 1} { jpayne@69: Lvarincr1 list $indexLst $howMuch jpayne@69: } else { jpayne@69: set idx [lindex $indexLst 0] jpayne@69: set targetList [lindex $list $idx] jpayne@69: # reduce refcount on targetList jpayne@69: Lvarset1nc list $idx {} jpayne@69: # recursively replace in targetList jpayne@69: Lvarincr targetList [lrange $indexLst 1 end] $howMuch jpayne@69: # put updated sub list back in the tree jpayne@69: Lvarset1nc list $idx $targetList jpayne@69: } jpayne@69: } jpayne@69: # Increments the value of one cell of a list jpayne@69: proc ::tcl::Lvarincr1 {listName index {howMuch 1}} { jpayne@69: upvar $listName list jpayne@69: set newValue [expr {[lindex $list $index]+$howMuch}] jpayne@69: set list [lreplace $list $index $index $newValue] jpayne@69: return $newValue jpayne@69: } jpayne@69: # Removes the first element of a list jpayne@69: # and returns the new list value jpayne@69: proc ::tcl::Lvarpop1 {listName} { jpayne@69: upvar $listName list jpayne@69: set list [lrange $list 1 end] jpayne@69: } jpayne@69: # Same but returns the removed element jpayne@69: # (Like the tclX version) jpayne@69: proc ::tcl::Lvarpop {listName} { jpayne@69: upvar $listName list jpayne@69: set el [lindex $list 0] jpayne@69: set list [lrange $list 1 end] jpayne@69: return $el jpayne@69: } jpayne@69: # Assign list elements to variables and return the length of the list jpayne@69: proc ::tcl::Lassign {list args} { jpayne@69: # faster than direct blown foreach (which does not byte compile) jpayne@69: set i 0 jpayne@69: set lg [llength $list] jpayne@69: foreach vname $args { jpayne@69: if {$i>=$lg} break jpayne@69: uplevel 1 [list ::set $vname [lindex $list $i]] jpayne@69: incr i jpayne@69: } jpayne@69: return $lg jpayne@69: } jpayne@69: jpayne@69: # Misc utilities jpayne@69: jpayne@69: # Set the varname to value if value is greater than varname's current value jpayne@69: # or if varname is undefined jpayne@69: proc ::tcl::SetMax {varname value} { jpayne@69: upvar 1 $varname var jpayne@69: if {![info exists var] || $value > $var} { jpayne@69: set var $value jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: # Set the varname to value if value is smaller than varname's current value jpayne@69: # or if varname is undefined jpayne@69: proc ::tcl::SetMin {varname value} { jpayne@69: upvar 1 $varname var jpayne@69: if {![info exists var] || $value < $var} { jpayne@69: set var $value jpayne@69: } jpayne@69: } jpayne@69: jpayne@69: jpayne@69: # everything loaded fine, lets create the test proc: jpayne@69: # OptCreateTestProc jpayne@69: # Don't need the create temp proc anymore: jpayne@69: # rename OptCreateTestProc {} jpayne@69: }