[AMG]: [[argparse]] is a feature-heavy argument parser. Documentation and test suite to come. For now, look at the big comment at the start of the implementation. **Usage summary** Parses an argument list according to a definition list. The result may be stored into caller variables or returned as a dict. The [[argparse]] command accepts the following switches: &| `-inline` | Return the result dict rather than setting caller variables |& &| `-exact` | Require exact switch name matches, and do not accept prefixes |& &| `-mixed` | Allow switches to appear after parameters |& &| `-long` | Recognize "`--switch`" long option alternative syntax |& &| `-equalarg` | Recognize "`-switch=arg`" inline argument alternative syntax |& &| `-normalize` | Normalize switch syntax in pass-through result keys |& &| `-reciprocal` | Every element's `-require` constraints are reciprocal |& &| `-level` ''LEVEL'' | Every `-upvar` element's [[upvar]] level; defaults to 1 |& &| `-template` ''TMP'' | Transform default element names using a substitution template |& &| `-pass` ''KEY'' | Pass unrecognized elements through to a result key |& &| `-keep` | Do not unset omitted element variables; conflicts with `-inline` |& &| `-boolean` | Treat switches as having `-boolean` wherever possible |& &| `-validate` ''DEF'' | Define named validation expressions to be used by elements |& &| `-enum` ''DEF'' | Define named enumeration lists to be used by elements |& &| `--` | Force next argument to be interpreted as the definition list |& After the above switches comes the definition list argument, then finally the optional argument list argument. If the argument list is omitted, it is taken from the caller's args variable. Each element of the definition list is itself a list containing a unique, non-empty name element consisting of alphanumerics, underscores, and minus (not as the first character), then zero or more of the following switches: &| `-switch` | Element is a switch; conflicts with `-parameter` |& &| `-parameter` | Element is a parameter; conflicts with `-switch` |& &| `-alias` ''ALIAS'' | Alias name; requires `-switch` |& &| `-ignore` | Element is omitted from result; conflicts with `-key` and `-pass` |& &| `-key` ''KEY'' | Override key name; not affected by `-template` |& &| `-pass` ''KEY'' | Pass through to result key; not affected by `-template` |& &| `-default` ''VAL'' | Value if omitted; conflicts with `-required` and `-keep` |& &| `-keep` | Do not unset if omitted; requires `-optional`; conflicts `-inline` |& &| `-value` ''VAL'' | Value if present; requires `-switch`; conflicts with `-argument` |& &| `-boolean` | Equivalent to "`-default 0 -value 1`" |& &| `-argument` | Value is next argument following switch; requires `-switch` |& &| `-optional` | Switch value is optional, or parameter is optional |& &| `-required` | Switch is required, or stop `-catchall` from implying `-optional` |& &| `-catchall` | Value is list of all otherwise unassigned arguments |& &| `-upvar` | Links caller variable; conflicts with `-inline` and `-catchall` |& &| `-level` ''LEVEL'' | This element's [[upvar]] level; requires `-upvar` |& &| `-standalone` | If element is present, ignore `-required`, `-require`, and `-forbid` |& &| `-require` ''LIST'' | If element is present, other elements that must be present |& &| `-forbid` ''LIST'' | If element is present, other elements that must not be present |& &| `-imply` ''LIST'' | If element is present, extra switch arguments; requires `-switch` |& &| `-reciprocal` | This element's `-require` is reciprocal; requires `-require` |& &| `-validate` ''DEF'' | Name of validation expression, or inline validation definition |& &| `-enum` ''DEF'' | Name of enumeration list, or inline enumeration definition |& As a special case, a definition list element may be the single character "`#`", which will cause the following element to be completely ignored. This may be used to place comments directly within the definition list. If neither `-switch` nor `-parameter` are used, a shorthand form is permitted. If the name is preceded by "`-`", it is a switch; otherwise, it is a parameter. An alias may be written after "`-`", then followed by "`|`" and the switch name. The element name may be followed by any number of flag characters: &| "`=`" | Same as `-argument`; only valid for switches |& &| "`?`" | Same as `-optional` |& &| "`!`" | Same as `-required` |& &| "`*`" | Same as `-catchall` |& &| "`^`" | Same as `-upvar` |& For more information, see the long header comment preceding the code [https://wiki.tcl-lang.org/page/argparse#340f463033e0fd5ddeabb922df4d4f1b5747494d0f5ed9894f13b6e13ca831f5]. **Examples** <>basic example ====== proc ex1 {args} { puts [argparse -inline { {-debug -default 0 -value 1} {-increment= -default 1} {-from= -default 1} {-to= -default 10} }] } % ex1 -debug debug 1 increment 1 from 1 to 10 % ex1 -from 1.0 -to 100.0 -increment 0.1 from 1.0 to 100.0 increment 0.1 debug 0 ====== <> <>another basic example ====== proc cmdline {args} { puts [argparse -inline -long -equalarg { {-debug -value true} {-h|help} {-type= -required} {-i|in=} {outfile -required} }] } % cmdline --type mp3 xyzzy type mp3 outfile xyzzy % cmdline --type flac --in waltz.flac missing required parameter: outfile % cmdline --type flac --in waltz.flac waltz.ogg type flac in waltz.flac outfile waltz.ogg % cmdline --type flac --in waltz.flac waltz.ogg type flac in waltz.flac outfile waltz.ogg % cmdline --type flac --in=waltz.flac waltz.ogg type flac in waltz.flac outfile waltz.ogg ====== <> <>lsort ====== proc lsort_ {args} { puts [argparse -inline { {-ascii -key sort -value text -default text} {-dictionary -key sort -value dictionary} {-integer -key sort -value integer} {-real -key sort -value real} {-command= -forbid {ascii dictionary integer real}} {-increasing -key order -value increasing -default increasing} {-decreasing -key order -value decreasing} -indices -index= -stride= -nocase -unique list }] } % lsort_ -increasing -real {2.0 1.0} order increasing sort real list {2.0 1.0} ====== <> <>lsearch ====== proc lsearch_ {args} { puts [argparse -inline { {-exact -key match -value exact} {-glob -key match -value glob -default glob} {-regexp -key match -value regexp} {-sorted -forbid {glob regexp}} -all -inline -not -start= {-ascii -key format -value text -default text} {-dictionary -key format -value dictionary} {-integer -key format -value integer} {-nocase -key format -value nocase} {-real -key format -value real} {-increasing -key order -value increasing -require sorted -default increasing} {-decreasing -key order -value decreasing -require sorted} {-bisect -imply -sorted -forbid {all not}} -index= {-subindices -require index} list pattern }] } % lsearch_ -inline -start 1 -exact -bisect {a b c} b inline {} start 1 match exact bisect {} sorted {} list {a b c} pattern b ====== <> <>example with a catchall and optional arguments ====== proc dummy {args} { puts [argparse -inline {a? b c* d e?}] } # note the interaction between the optional arguments and the catchall argument. # the optional arguments are assigned first before the catchall argument. % dummy 1 2 b 1 d 2 c {} % dummy 1 2 3 a 1 b 2 d 3 c {} % dummy 1 2 3 4 a 1 b 2 d 3 e 4 c {} % dummy 1 2 3 4 5 a 1 b 2 c 3 d 4 e 5 % dummy 1 2 3 4 5 6 a 1 b 2 c {3 4} d 5 e 6 ====== <> One feature not shown by the examples is setting or linking variables. I'm just using the -inline mode for display purposes. **Ideas** ***Enabling -boolean by default*** I'm using -boolean often enough now that I want it to be enabled by default for every compatible switch, as if -boolean were passed to the [[argparse]] command as a global configuration switch. To make this work, I would have to remove -keep and instead accept -unset as its converse. ***Automatic command usage error message*** At least when not given an explicit argument list and instead relying on the caller's args variable, I'd like for this command to generate the usage error message for the procedure that's calling it. To make this work, it will need to check the name and argument list of its calling procedure. This won't give the right result in every case though; for example, the caller might be applying logic of its own before calling [[argparse]]. Therefore, additional switches may be needed to enable, disable, or fine-tune the help text. Basically, [[argparse]] would take the place of [Tcl_WrongNumArgs]() which otherwise would never be called when the proc's parameter list is "args". ***Help text generation*** A -help or -usage switch could be added to the element definition syntax to supply usage information for each switch or parameter. Using this text, nicely-formatted help text can be produced, suitable for display by a command-line program. There's more to it than this though. Some overall help text would need to be supplied to describe the behavior of the program at a higher level, as opposed to simply describing each argument. Plus there's the question of knowing how and when to display this text. ***Header comment parsing*** (Suggested by [tjk] via email.) Like [docstring] or [Ruff!], [[argparse]] could use [info body] to fetch the implementation of its calling procedure then parse an extended header comment at the beginning. From this header comment it could extract not only the list of switches and parameters but also description text explaining their use, as well as an overall synopsis of the procedure. Naturally, this can't be the only mode of operation for [[argparse]]. One idea is that it could do this if both its definition and argument list arguments are omitted. My inclination is to say this creates ambiguity if [[argparse]] is supplied with overall switches such as -inline or -exact: ====== argparse -inline -exact ====== Here, -inline is a perfectly good definition list, and -exact is a perfectly good argument list. But they also would work just as well as overall switches if [[argparse]] is updated to allow the definition and argument lists to be omitted in favor of looking at the header comment and the [args] variable. At present, [[argparse]] resolves the ambiguity by treating the first argument it doesn't recognize as a known switch as the definition list, and if that's not good enough, -- may be used to force it. This heuristic should be good enough, so I think we're okay. I could also make an explicit switch to tell [[argparse]] to get its configuration from the header comment, but I'm disinclined to do this since I don't have an analogous switch to tell it to get its argument list from the args variable. It should be able to autodetect both situations just fine. I have some work ahead of me defining a nice, human-readable comment format that also doubles as an input to [[argparse]]. However, there is a performance pitfall that needs to be addressed. If there is no definition list argument, where would a high-performance [[argparse]] C implementation cache the parsed definition list data? The obvious solution is to piggyback on the [Tcl_Obj] containing the script body. The script body itself is not where the bytecodes are stored, and it has a pure string type, so it is a reasonable candidate to cache data derived from that self-same string. But there is another trap, one I did not expect. Every time I call [[info body]], it returns a new Tcl_Obj, as shown by [tcl::unsupported::representation] reporting a different object pointer each time. This spells trouble. I need a way to associate metadata with procs, and if I can't just shove it in the script body's [intrep], then things get messy. I probably can maintain a mapping from Tcl_CmdInfo pointers to my cached parsed definition lists, but I will also have to set the Tcl_CmdDeleteProc to a cleanup routine to deallocate my cache. I am not sure if Tcl_CmdDeleteProc even works for procs. But if it does, I will also need to worry about chaining to the original Tcl_CmdDeleteProc if one came predefined. Plus if another extension comes along and tries to do the same thing I'm doing, I had better hope it doesn't assume it's the only one using Tcl_CmdDeleteProc. I will also have to worry about [apply] [lambda]s, namespace commands, and ensemble commands too. See [info body] for details on how to get the current body even when called from a lambda or a namespace command. Though, at this point it's more important to get the name than the body. For the case of lambdas, as much as I'd like to cache the parsed definition list in the lambda body, attempting to treat the overall lambda as a list in order to get its body will likely shimmer away the bytecode representation. I will have to investigate further to see if there is a way to bypass this limitation and directly access the text of the lambda body. This is a problem even for simply reading the body in order to parse the header comment, so there could be constant recompilation even when not trying to cache the parsed definition list. ***Test suite*** Yup, definitely need to write one. There are a lot of complicated features and corner cases to exercise. I have a partial test suite for [[argparse]], though it only covers parameters and not switches. I'll have to update it for new syntax, expanded capability, and the features I didn't test in the first place. ***C API*** I plan to rewrite this code in C and provide a stubs-enabled C API, then create a script binding for same. I doubt bytecode optimization will provide any benefit if it's already all rolled up in a single function. Though, more likely it will be several functions: one to parse the element definition list, another to parse an argument list using a pre-parsed element definition list. One thing that will be needed alongside the script binding is a new internal representation type to cache the parsed element definition list. This should provide a major performance boost by avoiding the need to repeatedly parse and validate the element definition list. ***Support switch clustering*** Elsewhere on this page, [bll] mentioned switch clustering, e.g. "`[ls] -lA`". I can add support for this, though it will preclude using a single `-` for switch names longer than a single character. Instead, only the `-long` form (`--`) would be allowed for longer names. **Bugs** In updating an existing application to use this code, I found and fixed a few bugs and feature gaps, but it's quite possible more issues remain. Please report anything you find right here. **References** [Command Option Parsing]: Has more references and links to discussion [extending the notation of proc args]: Backward-compatible proposal to incorporate similar functionality into the [proc] parameter list [parse_args]: Very similar functionality to [[argparse]], implemented in C [https://wiki.tcl-lang.org/page/dispatch#f295cc474c2cd9ae047b9eaf124c94d134bc153bfd4f32b9c14b9459953c9bdd%|%dispatch%|%]: [switch]-like command that uses [[argparse]] to let each script accept arguments **Discussion** <>Discussion ***Use of argv*** [bll] 2019-2-28: I just noticed that argparse is modifying argv. Is there a reason for this? It seems to me that argv should not be touched. If the user wants argv modified, e.g. with ''mycmd -p1 -p2=stuff -- -pb1 -pb2'', and the user wants argv to end up with ''-pb1 -pb2'', then this could be an option to argparse. [AMG]: [[argparse]] has a local variable named argv that it modifies, but this is not tied to any caller or global variables that may have the same name. To make [[argparse]] modify a caller variable named argv, use argv as a key or pass name and don't use -inline, or use a -template the somehow permutes to argv (e.g. -template argv(%)). For example: ====== argparse -equalarg {-p1 -p2= argv*} $argv ====== ***Arguments in many places*** [bll] 2018-8-23: Arguments can appear in many places: The command line, in an environment variable, global configuration files, user configuration files. (1) The argument processing should have the flexibility of doing: ====== # overly simplistic pseudo-code argparse $dataFromGlobalConfig argparse $dataFromLocalConfig argparse $::env(MYPROG_ENV) argparse $::argv ====== and end up with a single set of options. [AMG]: These all would work if you simply inserted the element definition as the initial argument. For command line parsing, consider using `-long`, `-equalarg`, and `-mixed` to more closely resemble the switch syntax supported by most common Unix-like commands. ***Counting duplicate arguments*** [bll]: (2) I would always like to see some sort of ability to supply duplicate arguments that increment a value. e.g. -v -v -v is often seen on a command line to increase a verbosity level. [AMG]: At present, the only support for duplicate arguments is via `-pass`, then the caller can do secondary parsing of the pass-through variable or dict value. ====== argparse {{-v -pass v}} set v [llength $v] ====== I've considered adding special support for this usage, but the above isn't so bad. ***Option aliases*** [bll]: I'm just looking at some option code I wrote (not for Tcl) and what else would be nice. (3) Option aliases, I think you can support already with: `-D -imply debug` or `-D -require debug`. [AMG]: There's `-alias` or its `|` shorthand. [[`argparse {-D|debug}`]] will recognize `-D`, `-d`, `-de`, `-deb`, `-debu`, `-debug`. ([[`argparse {-D|debug -define}`]] will not recognize `-d` or `-de` as ways of writing `-debug`.) Try this in combination with `-pass` and `-normalize`, by the way. [bll]: I hate prefixes and would always use `-exact`. Ok, figured out the syntax. I find it rather confusing. `-D= -alias debug` specifies that debug is an alias of it, not that `-D` is an alias of `-debug`. This is backwards in my mind. I think I would use: `-D= -hasalias debug` or `-debug -alias D`. Well, this one will drive me crazy if I need it. ====== % set args [list -D=5] -D=5 % argparse -equalarg -exact -template apopts(%) {{-debug=} {-D= -alias debug}} % parray apopts apopts(D) = 5 ====== [AMG]: I'm not sure I understand the complaint here. You say you would prefer "`-debug -alias D`" but that is exactly what is expected and supported. Saying "`-D -alias debug`" is indeed backwards. As you point out, the key will default to D rather than debug. The way it works is the first element is the switch or parameter name, then subsequent elements modify the definition. I recommend the shorthand: "`-D|debug=`" means the same as "`-debug= -alias D`". The longest possible form is "`debug -switch -alias D -argument`". But use the shorthand. Shorthand is good. The long form exists solely to regularize the internals. The previous version of this code only supported shorthand, and it meant the internals were searching strings for single-character flags, plus had no room for expansion. But obviously the long form is too verbose for typical use. Thus I allow both syntaxes. Why do I list the alias first in the shorthand? Because the alias is almost always a single character, and this results in a neater display. Personal preference. Sorry if this contributes to confusion. [bll]: Our minds work differently. I read `-alias` as is-an-alias-of. [AMG]: Here, have a real-life example, modified a bit from the production form: ====== argparse -mixed { {-s|slocs -key fileMode -value slocs} {-t|total -key fileMode -value total} {-d|density -key fileMode -value density} {-l|language -key fileMode -value language} {-f|filename -key fileMode -value filename -default filename} {-r|reverse -key fileReverse} {-o|omit -key fileMode -value omit} {-S|summarySlocs -key summaryMode -value slocs} {-T|summaryTotal -key summaryMode -value total} {-D|summaryDensity -key summaryMode -value density} {-L|summaryLanguage -key summaryMode -value language -default language} {-C|summaryCount -key summaryMode -value count} {-R|summaryReverse -key summaryReverse} {-O|summaryOmit -key summaryMode -value omit} -n|noRecurse -x|exclude= -g|debug argv* } $argv ====== Hopefully this should clarify how aliases are intended to be written. Basically, don't use `-alias`, rather use the shorthand. ***Clustering*** [bll]: (4) Possibly a legacy mode, where `-ab` = `-a -b` instead of `-a b`. I don't know if this is necessary. [AMG]: I believe you're talking about switch clustering, which is a nice feature for compactness and command line parsing, but it collides very badly with long options. The existence of switch clustering is the whole reason we have `--switch` long option syntax. I could create a `-cluster` switch which would imply `-long` and make single `-` support only single-character unique prefixes (presumably from aliases) but allow multiple per argument. I don't know what you mean by `-a b`. In context I would guess "the switch whose name is the two-character sequence `ab`" but it's not clear. [bll]: If there is no clustering, `-ab` is the same as `-a=b` or `-a b` (old unix style). I believe the programming community is moving away from switch clustering. I would definitely lump it into the legacy category. I cannot recall if the community is moving away from the `-ab` no-space syntax (I think so). [AMG]: Ah, I completely forgot about having the argument immediately follow the single-character switch name. I'm not keen on supporting that, but it does turn up in a lot of places. Example: `-I/usr/local/include`. ***Ignoring options*** [bll]: (5) Be able to specify an option that is just ignored. [AMG]: Use `-ignore`: [[`argparse {{-foo -ignore} -bar}`]] will parse both `-foo` and `-bar` but will only set the variable `bar`. ***Switch and parameter arrays*** [bll] 2018-8-23: In [extending the notation of proc args] an example is given where the options are stored in local variables: e.g. ${-start}. I do not like this at all. I would very much prefer to specify and access an array: $apopts(-start). Then I can check the options from other procedures in my program. [AMG]: There are numerous ways to fine-tune where the switch and parameter values are stored. `argparse -template apopts(%) {-start= -exact}`: Switches go in apopts array, having keys "start" and "exact" `argparse -template apopts(-%) {-start= -exact}`: Switches go in apopts array, having keys "-start" and "-exact" `argparse {{-start= -key apopts(-start)} {-exact -key apopts(-exact)}}`: Switches go in apopts array, having keys "-start" and "-exact" `set apopts [[argparse -inline {-start= -exact}]]`: Switches go in apopts dict, having keys "start" and "exact" While arrays and dicts work, they do have the drawback of not being compatible with `-upvar` because Tcl does not allow for an array element or dict value to be a link to another variable. Saying `-template apopts(-%)` is cheating because it prefixes all array element names with `-`, both switches and parameters. But if you're only using switches, and you really want that `-` in there, it's fine. There is not the ability to set a different template for switches than for parameters, but you are able to individually set the `-key` of each element. [bll]: With my simple option parser I use, I often specify some parameters as `-parameter `, so that's not a big issue. I sort of like the `-` in front, but that's not an issue. ***Auto defaulting of simple switches*** [bll]: RFE: I would like an option to auto-default simple switches. If the switch is not specified, a false value would always be returned. ====== # hypothetical example % set args [list] % unset -nocomplain apopts % argparse -template apopts(%) -simple false {-test1 -stride=} % parray aptopts apopts(test1) = false ====== If you add `-boolean` as stated from [parse_args], this would not be needed. [AMG]: Yeah, `-boolean` is probably what you want since you're only addressing half the issue. The "default -default" is for the array element (or whatever) to not even be created in the first place, but the "default -value" (i.e. the value used for a switch lacking an argument) is empty string. It would be very strange to have the choices be false and empty string. I think I would have `-boolean` be permitted both as a switch modifying individual switches and as a switch applying to the entire [[argparse]] command. In the latter case it would change the default `-default` and `-value` to 0 and 1 for switches that lack `-argument` (or `-optional`, `-required`, and `-catchall`, all of which imply `-argument` when used with `-switch`; also note that these all have shorthand syntaxes you're more likely to use). [AMG]: `-boolean` is now implemented, both overall (as a leading argument to [[argparse]]) and per-switch (in the same way as other modifier switches). It works the same as `-default 0 -value 1`. If you use `-boolean` overall, it will automatically apply to every element for which it would be legal to specify it. That is, it overall `-boolean` applies to every element that: * is a switch (uses `-switch` or starts with "`-`") * does not accept an argument (no `-argument`, does not end with "`=`" or "`!`") * does not link to a caller variable (no `-upvar`, does not end with "`^`") * does not have a default value (no `-default`) * does not specify a value if set (no `-value`) * is not required (no `-required`) Oh yes, I made another change worth mentioning in the context of your example. You do [[unset -nocomplain apopts]], but that is no longer necessary because variables for omitted elements are now automatically unset. To get the old behavior back, use the `-keep` switch, which may either be specified overall or for an individual element (which, unlike `-boolean`, is legal for parameters as well as switches). Rewriting your example: ====== % argparse -template apopts(%) -boolean {-test1 -stride=} {} % parray aptopts apopts(test1) = 0 ====== However, I do not support changing the way false and true are expressed. I chose to go with their canonical representations, being 0 and 1. This will make no difference when actually testing logic values and will only impact display, which is typically a development/debug task only rather than something visible to an application's end user. ***Easy testing if a parameter was specified*** [bll]: Another RFE: I also have in mine a very simple way of testing whether a switch was specified at all, so instead of many `[[info exists opts(-weburl)]]`, I simply do `if { $opts(-weburl) } ...` and access the argument with `$opts(-weburl.arg)`. This could be reversed: ====== # hypothetical example and I may have wrong syntax set args {} puts [argparse -inline -specifyexists -stride=] stride.exists false set args -stride=5 puts [argparse -inline -specifyexists -stride=] stride.exists true stride 5 ====== [AMG]: So you're suggesting having separate keys to track existence and value. I don't have a problem with [[info exists]] and frequently use the presence or absence of a variable to signal a boolean, particularly if there's additional detail attached to a "true" condition, most especially if there is no value I can reserve to indicate "false". But if you prefer another style then that can be accommodated. This is interesting in combination with "`-optional`" switches (i.e. tri-state switches that can either be omitted altogether, be supplied as the final argument and have no argument of their own, or be present and given an argument). Currently, such switches are either omitted from the result, have empty string as their value, or have a single-element dict mapping from empty string to the real value. With `-inline` this becomes a bit more natural: %| Script | Return value |% &| `argparse -inline {-foo?} {}` | `{}` |& &| `argparse -inline {-foo?} {-foo}` | `foo {}` |& &| `argparse -inline {-foo?} {-foo bar}` | `foo {{} bar}` |& This allows: ====== set opt [argparse -inline {-foo?}] dict exists $opt foo ;# Checks if -foo was specified dict exists $opt foo {} ;# Checks if -foo was specified and given a value dict get $opt foo {} ;# Returns the value of -foo or throws an error if not given one ====== With your proposal, the above would instead be: %| Script | Return value |% &| `argparse -inline -specifyexists {-foo?} {}` | `foo 0` |& &| `argparse -inline -specifyexists {-foo?} {-foo}` | `foo 1` |& &| `argparse -inline -specifyexists {-foo?} {-foo bar}` | `foo 1 foo.arg bar` |& ====== set opt [argparse -inline {-foo?}] dict get $opt foo ;# Checks if -foo was specified dict exists $opt foo.arg ;# Checks if -foo was specified and given a value dict get $opt foo.arg ;# Returns the value of -foo or throws an error if not given one ====== [bll]: I have to think about this. Whereas the utility is nice, I don't want to create more code mess to support something that might only be used by one person. Let me roll this around in the back of my head for a few days. ***Mixing - and --*** [bll]: Now this surprised me: ====== cmdline -type flac --in waltz.flac waltz.ogg ====== I have never seen -option and --option mixed on the command line before. Slightly unusual. If you do end up supporting options with no space, e.g. `-I/usr/include/local`, this will probably go away. [AMG]: Having both in the same command line is admittedly weird, but rejecting it would be arbitrary and useless. Possibly the command line was built up in parts coming from different places. Without `-long`, both would have to be `-type` and `-in`. With `-long`, the above is valid, though the expectation is that the user would pick one style and stick with it. With `-cluster` (doesn't exist yet), the caller would have to use `--type` and `--in`, or single-character aliases/prefixes, e.g. `-t` and `-i`. Related: I'm considering adding an `-appendarg` switch to go along with `-equalarg` to allow the argument to be directly appended to single-character switch names. This would make `-I/usr/local/include` possible. ***Biased testimonial*** [AMG]: Take it for what it's worth, since I'm just talking about my own code, but lately I found that having this functionality (a more limited predecessor version) has transformed the way I program in Tcl. I wish I had written this code years ago, but I've only had it for a month or two. I'm now free to make more flexible procedures that take many arguments, no longer having to worry about the complexity of argument parsing or the nightmare of long argument lists. I don't have to make contorted syntaxes that always put the "`args`" parameter at the end if an alternative would be more natural (e.g. a list of switches up at the front) since [[argparse]] lets optional, defaulted, and catchall arguments appear anywhere. In addition to defaulted arguments, I have optional arguments for which I don't need to worry about picking some default value that will never show up in normal usage; just check [[[info exists]]] to see if they were passed or not. It's now much easier to link to caller variables: just tack `^` on the end of the switch or parameter name. I'm now even using [[argparse]] for variadic data structures. [[dict]] would have sufficed for that purpose, except [[dict]] is a more rigid format. Because of how much I'm using it these days for professional work, I'm definitely interested in making this code faster. [bll]: My apologies for not reading everything thoroughly. Though the clarifications help. I am looking forward to seeing it as a package and in C form. I definitely agree with you. I made a very simple option parser for my application, and it has helped a lot with making the code clearer and like you I wish I had written it a little sooner. Unfortunately, everybody has their own way of parsing their command lines, and unless every possible ability is supported, it is hard for an option/argument parser to gain traction. [AMG]: Agreed. I feel it's a losing proposition no matter what. If the parser is too simplistic, it won't be useful enough to even bother publishing. If it supports too much stuff, it's too daunting and people either don't read the whole documentation or give up without trying. And if it's somewhere in the middle, it's still not flexible enough for many cases since there are so many possible syntaxes in the wild. Thus, I'm pretty well forced to implement the kitchen sink and accept the consequences of bloat. Because it's not merely my code that's bloated; it's the problem space that's bloated, and I'm just dealing with it. One thing I think will be helpful is a gentle introduction, one I have utterly failed to provide. Accompanying my code I wrote a very long yet dense comment giving a complete reference but not real examples. Then I dumped this code on the wiki and didn't spend much additional time writing tutorial material. All I did was write a few complex examples showing [[[lsort]]] and [[[lsearch]]]. And now I'm afraid to add more material to this page because it's so long already. If I port this to C, it will be accompanied by both an introduction and a reference. <> **Compatibility** This code is written for Tcl 8.6 and newer. If you want to use this with Tcl 8.5, you will need: * [lmap forward compatibility] * [string cat] forward compatibility * [Forward-compatible tcl::prefix] * [Forward-compatible try and throw] For Tcl 8.4, you will need: * [Forward-compatible dict] * [https://wiki.tcl.tk/1530#pagetocaa2ba245%|%Forward-compatible lassign] * [[[eval]]] instead of [{*}] (invasive change) **Code** <>Code ***argparse.tcl*** ====== package require Tcl 8.6 package provide argparse 0.3 # argparse -- # Parses an argument list according to a definition list. The result may be # stored into caller variables or returned as a dict. # # The [argparse] command accepts the following switches: # # -inline Return the result dict rather than setting caller variables # -exact Require exact switch name matches, and do not accept prefixes # -mixed Allow switches to appear after parameters # -long Recognize "--switch" long option alternative syntax # -equalarg Recognize "-switch=arg" inline argument alternative syntax # -normalize Normalize switch syntax in pass-through result keys # -reciprocal Every element's -require constraints are reciprocal # -level LEVEL Every -upvar element's [upvar] level; defaults to 1 # -template TMP Transform default element names using a substitution template # -pass KEY Pass unrecognized elements through to a result key # -keep Do not unset omitted element variables; conflicts with -inline # -boolean Treat switches as having -boolean wherever possible # -validate DEF Define named validation expressions to be used by elements # -enum DEF Define named enumeration lists to be used by elements # -- Force next argument to be interpreted as the definition list # # After the above switches comes the definition list argument, then finally the # optional argument list argument. If the argument list is omitted, it is taken # from the caller's args variable. # # Each element of the definition list is itself a list containing a unique, # non-empty name element consisting of alphanumerics, underscores, and minus # (not as the first character), then zero or more of the following switches: # # -switch Element is a switch; conflicts with -parameter # -parameter Element is a parameter; conflicts with -switch # -alias ALIAS Alias name; requires -switch # -ignore Element is omitted from result; conflicts with -key and -pass # -key KEY Override key name; not affected by -template # -pass KEY Pass through to result key; not affected by -template # -default VAL Value if omitted; conflicts with -required and -keep # -keep Do not unset if omitted; requires -optional; conflicts -inline # -value VAL Value if present; requires -switch; conflicts with -argument # -boolean Equivalent to "-default 0 -value 1" # -argument Value is next argument following switch; requires -switch # -optional Switch value is optional, or parameter is optional # -required Switch is required, or stop -catchall from implying -optional # -catchall Value is list of all otherwise unassigned arguments # -upvar Links caller variable; conflicts with -inline and -catchall # -level LEVEL This element's [upvar] level; requires -upvar # -standalone If element is present, ignore -required, -require, and -forbid # -require LIST If element is present, other elements that must be present # -forbid LIST If element is present, other elements that must not be present # -imply LIST If element is present, extra switch arguments; requires -switch # -reciprocal This element's -require is reciprocal; requires -require # -validate DEF Name of validation expression, or inline validation definition # -enum DEF Name of enumeration list, or inline enumeration definition # # As a special case, a definition list element may be the single character "#", # which will cause the following element to be completely ignored. This may be # used to place comments directly within the definition list. # # If neither -switch nor -parameter are used, a shorthand form is permitted. If # the name is preceded by "-", it is a switch; otherwise, it is a parameter. An # alias may be written after "-", then followed by "|" and the switch name. The # element name may be followed by any number of flag characters: # # "=" Same as -argument; only valid for switches # "?" Same as -optional # "!" Same as -required # "*" Same as -catchall # "^" Same as -upvar # # -default specifies the value to assign to element keys when the element is # omitted. If -default is not used, keys for omitted switches and parameters # are omitted from the result, unless -catchall is used, in which case the # default value for -default is empty string. # # At most one parameter may use -catchall. # # Multiple elements may share the same -key value if they are switches, do not # use -argument or -catchall, and do not use -default for more than one element. # Such elements are automatically are given -forbid constraints to prevent them # from being used simultaneously. If such an element does not use -boolean or # -value, the element name is used as its default -value. # # -value specifies the value to assign to switch keys when the switch is # present. -value may not be used with -argument, -optional, -required, or # -catchall. -value normally defaults to empty string, except when -boolean is # used (1 is the default -value) or multiple switches share the same -key (the # element name is the default -value). # # -optional, -required, -catchall, and -upvar imply -argument when used with # -switch. Consequently, switches require an argument when any of the shorthand # flag characters defined above are used, and it is not necessary to explicitly # specify "=" if any of the other flag characters are used. # # If -argument is used, the value assigned to the switch's key is normally the # next argument following the switch. With -catchall, the value assigned to the # switch's key is instead the list of all remaining arguments. With -optional, # the following processing is applied: # # - If the switch is not present, the switch's key is omitted from the result. # - If the switch is not the final argument, its value is a two-element list # consisting of empty string and the argument following the switch. # - If the switch is the final argument, its value is empty string. # # By default, switches are optional and parameters are required. Switches can # be made required with -required, and parameters can be made optional with # -optional. -catchall also makes parameters optional, unless -required is # used, in which case at least one argument must be assigned to the parameter. # Otherwise, using -required with -parameter has no effect. -switch -optional # -required means the switch must be present but may be the final argument. # # When -switch and -optional are both used, -catchall, -default, and -upvar are # disallowed. -parameter -optional -required is also a disallowed combination. # # -validate and -enum provide element value validation. The overall -validate # and -enum switches declare named validation expressions and enumeration lists, # and the per-element -validate and -enum switches select which validation # expressions and enumeration lists are used on which elements. The argument to # the overall -validate and -enum switches is a dict mapping from validation or # enumeration name to validation expressions or enumeration lists. The argument # to a per-element -validate switch is a validation name or expression, and the # argument to a per-element -enum switch is an enumeration name or list. An # element may not use both -validate and -enum. # # A validation expression is an [expr] expression parameterized on a variable # named arg which is replaced with the argument. If the expression evaluates to # true, the argument is accepted. # # An enumeration list is a list of possible argument values. If the argument # appears in the enumeration list, the argument is accepted. Unless -exact is # used, if the argument is a prefix of exactly one element of the enumeration # list, the argument is replaced with the enumeration list element. # # Unambiguous prefixes of switch names are acceptable, unless the -exact switch # is used. Switches in the argument list normally begin with a single "-" but # can also begin with "--" if the -long switch is used. Arguments to switches # normally appear as the list element following the switch, but if -equalarg is # used, they may be supplied within the switch element itself, delimited with an # "=" character, e.g. "-switch=arg". # # The per-element -pass switch causes the element argument or arguments to be # appended to the value of the indicated pass-through result key. Many elements # may use the same pass-through key. If -normalize is used, switch arguments # are normalized to not use aliases, abbreviations, the "--" prefix, or the "=" # argument delimiter; otherwise, switches will be expressed the same way they # appear in the original input. Furthermore, -normalize causes omitted switches # that accept arguments and have default values, as well as omitted parameters # that have default values, to be explicitly included in the pass-through key. # If -mixed is used, pass-through keys will list all switches first before # listing any parameters. If the first parameter value for a pass-through key # starts with "-", its value will be preceded by "--" so that it will not appear # to be a switch. If no arguments are assigned to a pass-through key, its value # will be empty string. The intention is that the value of a pass-through key # can be parsed again to get the original data, and if -normalize is used, it # will not be necessary to use -mixed, -long, -equalarg, -alias, or -default to # get the correct result. However, pathological use of -default can conflict # with this goal. For example, if the first optional parameter has no -default # but the second one does, then parsing the result of -normalize can assign the # default value to the first parameter rather than the second. # # The [argparse] -pass switch may be used to collect unrecognized arguments into # a pass-through key, rather than failing with an error. Normalization and # unmixing will not be applied to these arguments because it is not possible to # reliably determine if they are switches or parameters. In particular, it is # not known if an undefined switch expects an argument. # # [argparse] produces a set of keys and values. The keys are the names of # caller variables into which the values are stored, unless -inline is used, in # which case the key-value pairs are returned as a dict. The element names # default to the key names, unless overridden by -key, -pass, or -template. If # both -key and -pass are used, two keys are defined: one having the element # value, the other having the pass-through elements. Unless -keep or -inline # are used, the caller variables for omitted switches and parameters are unset. # # -template applies to elements using neither -key nor -pass. Its value is a # substitution template applied to element names to determine key names. "%" in # the template is replaced with the element name. To protect "%" or "\" from # replacement, precede it with "\". One use for -template is to put the result # in an array, e.g. with "-template arrayName(%)". # # Elements with -upvar are special. Rather than having normal values, they are # bound to caller variables using the [upvar] command. -upvar conflicts with # -inline because it is not possible to map a dict value to a variable. Due to # limitations of arrays and [upvar], -upvar cannot be used with keys whose names # resemble array elements. -upvar conflicts with -catchall because the value # must be a variable name, not a list. The combination -switch -optional -upvar # is disallowed for the same reason. If -upvar is used with switches or with # optional parameters, [info exists KEY] returns 1 both when the element is not # present and when its value is the name of a nonexistent variable. To tell the # difference, check if [info vars KEY] returns an empty list; if so, the element # is not present. Note that the argument to [info vars] is a [string match] # pattern, so it may be necessary to precede *?[]\ characters with backslashes. # # Argument processing is performed in three stages: switch processing, parameter # allocation, and parameter assignment. Each argument processing stage and pass # is performed left-to-right. # # All switches must normally appear in the argument list before any parameters. # Switch processing terminates with the first argument (besides arguments to # switches) that does not start with "-" (or "--", if -long is used). The # special switch "--" can be used to force switch termination if the first # parameter happens to start with "-". If no switches are defined, the first # argument is known to be a parameter even if it starts with "-". # # When the -mixed switch is used, switch processing continues after encountering # arguments that do not start with "-" or "--". This is convenient but may be # ambiguous in cases where parameters look like switches. To resolve ambiguity, # the special "--" switch terminates switch processing and forces all remaining # arguments to be parameters. # # When -mixed is not used, the required parameters are counted, then that number # of arguments at the end of the argument list are treated as parameters even if # they begin with "-". This avoids the need for "--" in many cases. # # After switch processing, parameter allocation determines how many arguments to # assign to each parameter. Arguments assigned to switches are not used in # parameter processing. First, arguments are allocated to required parameters; # second, to optional, non-catchall parameters; and last to catchall parameters. # Finally, each parameter is assigned the allocated number of arguments. proc ::argparse {args} { # Common validation helper routine. set validateHelper {apply {{name opt args} { if {[dict exists $opt enum]} { set command [list tcl::prefix match -message "$name value"\ {*}[if {[uplevel 1 {info exists exact}]} {list -exact}]\ [dict get $opt enum]] set args [lmap arg $args {{*}$command $arg}] } elseif {[dict exists $opt validate]} { foreach arg $args [list if [dict get $opt validate] {} else { return -code error -level 2\ "$name value \"$arg\" fails [dict get $opt validateMsg]" }] } return $args }}} # Process switches and locate the definition argument. set level 1 set enum {} set validate {} for {set i 0} {$i < [llength $args]} {incr i} { if {[lindex $args $i] eq "--"} { # Stop after "--". incr i break } elseif {[catch { regsub {^-} [tcl::prefix match -message switch { -boolean -enum -equalarg -exact -inline -keep -level -long -mixed -normalize -pass -reciprocal -template -validate } [lindex $args $i]] {} switch }]} { # Stop at the first non-switch argument. break } elseif {$switch ni {enum level pass template validate}} { # Process switches with no arguments. set $switch {} } elseif {$i == [llength $args] - 1} { return -code error "-$switch requires an argument" } else { # Process switches with arguments. set $switch [lindex $args [incr i]] } } # Forbid using -inline and -keep at the same time. if {[info exists inline] && [info exists keep]} { return -code error "-inline and -keep conflict" } # Extract the definition and args parameters from the argument list, pulling # from the caller's args variable if the args parameter is omitted. switch [expr {[llength $args] - $i}] { 0 { return -code error "missing required parameter: definition" } 1 { set definition [lindex $args end] set argv [uplevel 1 {::set args}] } 2 { set definition [lindex $args end-1] set argv [lindex $args end] } default { return -code error "too many arguments" }} # Parse element definition list. set def {} set aliases {} set order {} set switches {} set upvars {} set omitted {} foreach elem $definition { # Skip inline comments. if {[info exists comment]} { unset comment continue } elseif {[llength $elem] == 1 && [lindex $elem 0] eq "#"} { set comment {} continue } # Read element definition switches. set opt {} for {set i 1} {$i < [llength $elem]} {incr i} { if {[set switch [regsub {^-} [tcl::prefix match { -alias -argument -boolean -catchall -default -enum -forbid -ignore -imply -keep -key -level -optional -parameter -pass -reciprocal -require -required -standalone -switch -upvar -validate -value } [lindex $elem $i]] {}]] ni { alias default enum forbid imply key pass require validate value }} { # Process switches without arguments. dict set opt $switch {} } elseif {$i == [llength $elem] - 1} { return -code error "-$switch requires an argument" } else { # Process switches with arguments. incr i dict set opt $switch [lindex $elem $i] } } # Process the first element of the element definition. if {![llength $elem]} { return -code error "element definition cannot be empty" } elseif {[dict exists $opt switch] && [dict exists $opt parameter]} { return -code error "-switch and -parameter conflict" } elseif {[info exists inline] && [dict exists $opt keep]} { return -code error "-inline and -keep conflict" } elseif {![dict exists $opt switch] && ![dict exists $opt parameter]} { # If -switch and -parameter are not used, parse shorthand syntax. if {![regexp -expanded { ^(?:(-) # Leading switch "-" (?:(\w[\w-]*)\|)?)? # Optional switch alias (\w[\w-]*) # Switch or parameter name ([=?!*^]*)$ # Optional flags } [lindex $elem 0] _ minus alias name flags]} { return -code error "bad element shorthand: [lindex $elem 0]" } if {$minus ne {}} { dict set opt switch {} } else { dict set opt parameter {} } if {$alias ne {}} { dict set opt alias $alias } foreach flag [split $flags {}] { dict set opt [dict get { = argument ? optional ! required * catchall ^ upvar } $flag] {} } } elseif {![regexp {^\w[\w-]*$} [lindex $elem 0]]} { return -code error "bad element name: [lindex $elem 0]" } else { # If exactly one of -switch or -parameter is used, the first element # of the definition is the element name, with no processing applied. set name [lindex $elem 0] } # Check for collisions. if {[dict exists $def $name]} { return -code error "element name collision: $name" } if {[dict exists $opt switch]} { # -optional, -required, -catchall, and -upvar imply -argument when # used with switches. foreach switch {optional required catchall upvar} { if {[dict exists $opt $switch]} { dict set opt argument {} } } } else { # Parameters are required unless -catchall or -optional are used. if {([dict exists $opt catchall] || [dict exists $opt optional]) && ![dict exists $opt required]} { dict set opt optional {} } else { dict set opt required {} } } # Check requirements and conflicts. foreach {switch other} {reciprocal require level upvar} { if {[dict exists $opt $switch] && ![dict exists $opt $other]} { return -code error "-$switch requires -$other" } } foreach {switch others} { parameter {alias boolean value argument imply} ignore {key pass} required {boolean default} argument {boolean value} upvar {boolean inline catchall} boolean {default value} enum validate } { if {[dict exists $opt $switch]} { foreach other $others { if {[dict exists $opt $other]} { return -code error "-$switch and -$other conflict" } } } } if {[dict exists $opt upvar] && [info exists inline]} { return -code error "-upvar and -inline conflict" } # Check for disallowed combinations. foreach combination { {switch optional catchall} {switch optional upvar} {switch optional default} {switch optional boolean} {parameter optional required} } { foreach switch [list {*}$combination {}] { if {$switch eq {}} { return -code error "[join [lmap switch $combination { string cat - $switch }]] is a disallowed combination" } elseif {![dict exists $opt $switch]} { break } } } # Replace -boolean with "-default 0 -value 1". if {([info exists boolean] && [dict exists $opt switch] && ![dict exists $opt argument] && ![dict exists $opt upvar] && ![dict exists $opt default] && ![dict exists $opt value] && ![dict exists $opt required]) || [dict exists $opt boolean]} { dict set opt default 0 dict set opt value 1 } # Insert default -level if -upvar is used. if {[dict exists $opt upvar] && ![dict exists $opt level]} { dict set opt level $level } # Compute default output key if -ignore, -key, and -pass aren't used. if {![dict exists $opt ignore] && ![dict exists $opt key] && ![dict exists $opt pass]} { if {[info exists template]} { dict set opt key [string map\ [list \\\\ \\ \\% % % $name] $template] } else { dict set opt key $name } } if {[dict exists $opt parameter]} { # Keep track of parameter order. lappend order $name # Forbid more than one catchall parameter. if {[dict exists $opt catchall]} { if {[info exists catchall]} { return -code error "multiple catchall parameters:\ $catchall and $name" } else { set catchall $name } } } elseif {![dict exists $opt alias]} { # Build list of switches. lappend switches -$name } elseif {![regexp {^\w[\w-]*$} [dict get $opt alias]]} { return -code error "bad alias: [dict get $opt alias]" } elseif {[dict exists $aliases [dict get $opt alias]]} { return -code error "element alias collision: [dict get $opt alias]" } else { # Build list of switches (with aliases), and link switch aliases. dict set aliases [dict get $opt alias] $name lappend switches -[dict get $opt alias]|$name } # Map from upvar keys back to element names, and forbid collisions. if {[dict exists $opt upvar] && [dict exists $opt key]} { if {[dict exists $upvars [dict get $opt key]]} { return -code error "multiple upvars to the same variable:\ [dict get $upvars [dict get $opt key]] $name" } dict set upvars [dict get $opt key] $name } # Look up named enumeration lists and validation expressions. if {[dict exists $opt enum] && [dict exists $enum [dict get $opt enum]]} { dict set opt enum [dict get $enum [dict get $opt enum]] } elseif {[dict exists $opt validate]} { if {[dict exists $validate [dict get $opt validate]]} { dict set opt validateMsg "[dict get $opt validate] validation" dict set opt validate [dict get $validate\ [dict get $opt validate]] } else { dict set opt validateMsg "validation: [dict get $opt validate]" } } # Save element definition. dict set def $name $opt # Prepare to identify omitted elements. dict set omitted $name {} } # Process constraints and shared key logic. dict for {name opt} $def { # Verify constraint references. foreach constraint {require forbid} { if {[dict exists $opt $constraint]} { foreach otherName [dict get $opt $constraint] { if {![dict exists $def $otherName]} { return -code error "$name -$constraint references\ undefined element: $otherName" } } } } # Create reciprocal requirements. if {([info exists reciprocal] || [dict exists $opt reciprocal]) && [dict exists $opt require]} { foreach other [dict get $opt require] { dict update def $other otherOpt { dict lappend otherOpt require $name } } } # Perform shared key logic. if {[dict exists $opt key]} { dict for {otherName otherOpt} $def { if {$name ne $otherName && [dict exists $otherOpt key] && [dict get $otherOpt key] eq [dict get $opt key]} { # Limit when shared keys may be used. if {[dict exists $opt parameter]} { return -code error "$name cannot be a parameter because\ it shares a key with $otherName" } elseif {[dict exists $opt argument]} { return -code error "$name cannot use -argument because\ it shares a key with $otherName" } elseif {[dict exists $opt catchall]} { return -code error "$name cannot use -catchall because\ it shares a key with $otherName" } elseif {[dict exists $opt default] && [dict exists $otherOpt default]} { return -code error "$name and $otherName cannot both\ use -default because they share a key" } # Create forbid constraints on shared keys. if {![dict exists $otherOpt forbid] || $name ni [dict get $otherOpt forbid]} { dict update def $otherName otherOpt { dict lappend otherOpt forbid $name } } # Set default -value for shared keys. if {![dict exists $opt value]} { dict set def $name value $name } } } } } # Handle default pass-through switch by creating a dummy element. if {[info exists pass]} { dict set def {} pass $pass } # Force required parameters to bypass switch logic. set end [expr {[llength $argv] - 1}] if {![info exists mixed]} { foreach name $order { if {[dict exists $def $name required]} { incr end -1 } } } set force [lreplace $argv 0 $end] set argv [lrange $argv 0 $end] # Perform switch logic. set result {} set missing {} if {$switches ne {}} { # Build regular expression to match switches. set re ^- if {[info exists long]} { append re -? } append re {(\w[\w-]*)} if {[info exists equalarg]} { append re (?:(=)(.*))? } else { append re ()() } append re $ # Process switches, and build the list of parameter arguments. set params {} while {$argv ne {}} { # Check if this argument appears to be a switch. set argv [lassign $argv arg] if {[regexp $re $arg _ name equal val]} { # This appears to be a switch. Fall through to the handler. } elseif {$arg eq "--"} { # If this is the special "--" switch to end all switches, all # remaining arguments are parameters. set params $argv break } elseif {[info exists mixed]} { # If -mixed is used and this is not a switch, it is a parameter. # Add it to the parameter list, then go to the next argument. lappend params $arg continue } else { # If this is not a switch, terminate switch processing, and # process this and all remaining arguments as parameters. set params [linsert $argv 0 $arg] break } # Process switch aliases. if {[dict exists $aliases $name]} { set name [dict get $aliases $name] } # Preliminary guess for the normalized switch name. set normal -$name # Perform switch name lookup. if {[dict exists $def $name switch]} { # Exact match. No additional lookup needed. } elseif {![info exists exact] && ![catch { tcl::prefix match -message switch [lmap {key data} $def { if {[dict exists $data switch]} { set key } else { continue } }] $name } name]} { # Use the switch whose prefix unambiguously matches. set normal -$name } elseif {[dict exists $def {}]} { # Use default pass-through if defined. set name {} } else { # Fail if this is an invalid switch. set switches [lsort $switches] if {[llength $switches] > 1} { lset switches end "or [lindex $switches end]" } set switches [join $switches\ {*}[if {[llength $switches] > 2} {list ", "}]] return -code error "bad switch \"$arg\": must be $switches" } # If the switch is standalone, ignore all constraints. if {[dict exists $def $name standalone]} { foreach other [dict keys $def] { dict unset def $other required dict unset def $other require dict unset def $other forbid if {[dict exists $def $other parameter]} { dict set def $other optional {} } } } # Keep track of which elements are present. dict set def $name present {} # If the switch value was set using -switch=value notation, insert # the value into the argument list to be handled below. if {$equal eq "="} { set argv [linsert $argv 0 $val] } # Load key and pass into local variables for easy access. unset -nocomplain key pass foreach var {key pass} { if {[dict exists $def $name $var]} { set $var [dict get $def $name $var] } } # Keep track of which switches have been seen. dict unset omitted $name # Validate switch arguments and store values into the result dict. if {[dict exists $def $name catchall]} { # The switch is catchall, so store all remaining arguments. set argv [{*}$validateHelper $normal\ [dict get $def $name] {*}$argv] if {[info exists key]} { dict set result $key $argv } if {[info exists pass]} { if {[info exists normalize]} { dict lappend result $pass $normal {*}$argv } else { dict lappend result $pass $arg {*}$argv } } break } elseif {![dict exists $def $name argument]} { # The switch expects no arguments. if {$equal eq "="} { return -code error "$normal doesn't allow an argument" } if {[info exists key]} { if {[dict exists $def $name value]} { dict set result $key [dict get $def $name value] } else { dict set result $key {} } } if {[info exists pass]} { if {[info exists normalize]} { dict lappend result $pass $normal } else { dict lappend result $pass $arg } } } elseif {$argv ne {}} { # The switch was given the expected argument. set argv0 [lindex [{*}$validateHelper $normal\ [dict get $def $name] [lindex $argv 0]] 0] if {[info exists key]} { if {[dict exists $def $name optional]} { dict set result $key [list {} $argv0] } else { dict set result $key $argv0 } } if {[info exists pass]} { if {[info exists normalize]} { dict lappend result $pass $normal $argv0 } elseif {$equal eq "="} { dict lappend result $pass $arg } else { dict lappend result $pass $arg [lindex $argv 0] } } set argv [lrange $argv 1 end] } else { # The switch was not given the expected argument. if {![dict exists $def $name optional]} { return -code error "$normal requires an argument" } if {[info exists key]} { dict set result $key {} } if {[info exists pass]} { if {[info exists normalize]} { dict lappend result $pass $normal } else { dict lappend result $pass $arg } } } # Insert this switch's implied arguments into the argument list. if {[dict exists $def $name imply]} { set argv [concat [dict get $def $name imply] $argv] dict unset def $name imply } } # Build list of missing required switches. dict for {name opt} $def { if {[dict exists $opt switch] && ![dict exists $opt present] && [dict exists $opt required]} { if {[dict exists $opt alias]} { lappend missing -[dict get $opt alias]|$name } else { lappend missing -$name } } } # Fail if at least one required switch is missing. if {$missing ne {}} { set missing [lsort $missing] if {[llength $missing] > 1} { lset missing end "and [lindex $missing end]" } set missing [join $missing\ {*}[if {[llength $missing] > 2} {list ", "}]] return -code error [string cat "missing required switch"\ {*}[if {[llength $missing] > 1} {list es}] ": " $missing] } } else { # If no switches are defined, bypass the switch logic and process all # arguments using the parameter logic. set params $argv } # Allocate one argument to each required parameter, including catchalls. set alloc {} lappend params {*}$force set count [llength $params] set i 0 foreach name $order { if {[dict exists $def $name required]} { if {$count} { dict set alloc $name 1 dict unset omitted $name incr count -1 } else { lappend missing $name } } incr i } # Fail if at least one required parameter is missing. if {$missing ne {}} { if {[llength $missing] > 1} { lset missing end "and [lindex $missing end]" } return -code error [string cat "missing required parameter"\ {*}[if {[llength $missing] > 1} {list s}] ": "\ [join $missing {*}[if {[llength $missing] > 2} {list ", "}]]] } # Try to allocate one argument to each optional, non-catchall parameter, # until there are no arguments left. if {$count} { foreach name $order { if {![dict exists $def $name required] && ![dict exists $def $name catchall]} { dict set alloc $name 1 dict unset omitted $name if {![incr count -1]} { break } } } } # Process excess arguments. if {$count} { if {[info exists catchall]} { # Allocate remaining arguments to the catchall parameter. dict incr alloc $catchall $count dict unset omitted $catchall } elseif {[dict exists $def {}]} { # If there is no catchall parameter, instead allocate to the default # pass-through result key. lappend order {} dict set alloc {} $count } else { return -code error "too many arguments" } } # Check constraints. dict for {name opt} $def { if {[dict exists $opt present]} { foreach {match condition description} { 1 require requires 0 forbid "conflicts with" } { if {[dict exists $opt $condition]} { foreach otherName [dict get $opt $condition] { if {[dict exists $def $otherName present] != $match} { foreach var {name otherName} { if {[dict exists $def [set $var] switch]} { set $var -[set $var] } } return -code error "$name $description $otherName" } } } } } } # If normalization is enabled, explicitly store into the pass-through keys # all omitted switches that have a pass-through key, accept an argument, and # have a default value. if {[info exists normalize]} { dict for {name opt} $def { if {[dict exists $opt switch] && [dict exists $opt pass] && [dict exists $opt argument] && [dict exists $opt default] && [dict exists $omitted $name]} { dict lappend result [dict get $opt pass]\ -$name [dict get $opt default] } } } # Validate parameters and store in result dict. set i 0 foreach name $order { set opt [dict get $def $name] if {[dict exists $alloc $name]} { if {![dict exists $opt catchall] && $name ne {}} { set val [lindex [{*}$validateHelper $name\ $opt [lindex $params $i]] 0] if {[dict exists $opt pass]} { if {[string index $val 0] eq "-" && ![dict exists $result [dict get $opt pass]]} { dict lappend result [dict get $opt pass] -- } dict lappend result [dict get $opt pass] $val } incr i } else { set step [dict get $alloc $name] set val [lrange $params $i [expr {$i + $step - 1}]] if {$name ne {}} { set val [{*}$validateHelper $name $opt {*}$val] } if {[dict exists $opt pass]} { if {[string index [lindex $val 0] 0] eq "-" && ![dict exists $result [dict get $opt pass]]} { dict lappend result [dict get $opt pass] -- } dict lappend result [dict get $opt pass] {*}$val } incr i $step } if {[dict exists $opt key]} { dict set result [dict get $opt key] $val } } elseif {[info exists normalize] && [dict exists $opt default] && [dict exists $opt pass]} { # If normalization is enabled and this omitted parameter has both a # default value and a pass-through key, explicitly store the default # value in the pass-through key, located in the correct position so # that it can be recognized again later. if {[string index [dict get $opt default] 0] eq "-" && ![dict exists $result [dict get $opt pass]]} { dict lappend result [dict get $opt pass] -- } dict lappend result [dict get $opt pass] [dict get $opt default] } } # Create default values for missing elements. dict for {name opt} $def { if {[dict exists $opt key] && ![dict exists $result [dict get $opt key]]} { if {[dict exists $opt default]} { dict set result [dict get $opt key] [dict get $opt default] } elseif {[dict exists $opt catchall]} { dict set result [dict get $opt key] {} } } if {[dict exists $opt pass] && ![dict exists $result [dict get $opt pass]]} { dict set result [dict get $opt pass] {} } } if {[info exists inline]} { # Return result dict. return $result } else { # Unless -keep was used, unset caller variables for omitted elements. if {![info exists keep]} { dict for {name val} $omitted { set opt [dict get $def $name] if {![dict exists $opt keep] && [dict exists $opt key] && ![dict exists $result [dict get $opt key]]} { uplevel 1 [list ::unset -nocomplain [dict get $opt key]] } } } # Process results. dict for {key val} $result { if {[dict exists $upvars $key]} { # If this element uses -upvar, link to the named variable. uplevel 1 [list ::upvar\ [dict get $def [dict get $upvars $key] level] $val $key] } else { # Store result into caller variables. uplevel 1 [list ::set $key $val] } } } } # vim: set sts=4 sw=4 tw=80 et ft=tcl: ====== ***pkgIndex.tcl*** ====== package ifneeded argparse 0.3 [list source [file join $dir argparse.tcl]] ====== <> <> Command | Argument Processing