Handling command line options with dict

After replying about this on Stackoverflow, I just wanted to jot down some notes on using dictionaries to handle command line options.

Basics

The options need to be passed as an even-sized list of option names and option values.

Passing options together with non-option arguments or with flag options (options that don't take a value) is discussed below.

By convention, option names have a "-" prefix, but dict doesn't really care if they have them or not (see at the bottom of the page).

Doing the handling

Initializing the options dictionary

An options dictionary can be initialized using a syntax similar to (but in this case not the same as) that of cmdline:

# options and optUsage need to be unset or empty at this point
foreach {option default usage} {
    r  ""  "use time from ref_file"
    t  -1  "use specified time"
} {
    dict set options  -$option $default
    dict set optUsage -$option $usage
}

% set options
# => -r {} -t -1
% set optUsage
# => -r {use time from ref_file} -t {use specified time}

(A slightly more complicated procedure could be used to deal with flag options or value options without default values, as cmdline can.)

At this point, something like this will print a handy usage message:

dict for {option usage} $optUsage { puts "$option   $usage" }

Getting the passed options

Now, if the argument args contains the list of option names and option values passed to the command, a dictionary of actual options for use inside the command can be created like this:

% set args
# => -t 99

set actualOptions [dict merge $::options [dict create {*}$args]]
# => -r {} -t 99

(If you're really close to the deadline, you can save a few characters by doing it this way:)

set actualOptions [dict merge $::options $args]

This works because any even-sized list seems to be equivalent to a dictionary value.

If you want to make sure no unexpected options are added to the dictionary, you can't use dict merge but have to do it like this:

set actualOptions $options
dict for {opt val} $args {
    if {[dict exists $options $opt]} {
        # only add an option if it occurs in $options
        dict set actualOptions $opt $val
    } else {
        puts stderr "Unknown option \"$opt\""
    }
}

Getting the option values

You can query the options dictionary like this:

dict get $actualOptions -t
# => 99

Or you can evaluate your code with the option names as variables (note that those variables are still linked to the option values):

dict with actualOptions {
    incr -t
    puts ${-t}
}

% set actualOptions
# => -r {} -t 100

Or use any other kind of dict operation on the options.

If you dislike the "-" prefix for options (which forces one to use braces in the previous example), that can be remedied, see below.

Passing options

The options can be put in a literal list:

proc foo {bar} {
    # handles options inside $bar
}

% foo [list -optA valA -optB valB]
# or
% foo {-optA valA -optB valB}

but using the args special argument name, they can be passed in sequence:

proc foo {args} {
    # handles options inside $args
}

% foo -optA valA -optB valB

If you use args, any arguments that aren't options with values need to be passed in front of the options:

proc foo {bar baz args} {
    # handles options inside $args
}

% foo 99 "tomato juice" -optA valA -optB valB

Flag options

Options that are just flags, i.e. they have no following value but instead passing or not passing them is in itself a boolean value, can't be handled with dict. If you use them, you must separate them from the other options, or insert a value like 1 after them, before processing the options. Examples:

Handle flags separate from options

# $args holds all options
set flaglist {-flagA -flagB -flagC}
# set each flag option to true or false
foreach flag $flaglist {
    # "set $name val" is usually an error, but here it's intentional
    set $flag [expr {$flag in $args}]
}
# purge the flag options (the ldiff command is non-standard but can be found on this wiki)
set args [ldiff $args $flaglist]
# flag option names (e.g. -flagB) are now variables with boolean values
# now dict can handle the remaining options in $args

(See ldiff.)

Handle flags together with options

# $args holds all options
set flaglist {-flagA -flagB -flagC}
# set each flag option to true or false
foreach flag $flaglist {
    # add the value 1 after each flag, if it occurs in $args
    set index [lsearch -exact $args $flag]
    if {$index >= 0} {
        set args [lreplace $args $index $index $flag 1]
    }
}
# now dict can handle all options, because $args is now an even-sized list of names and values

Undashed option names

If you don't want the "-" prefix, you can of course just define your option dictionary without dashed names (making them look more like named arguments, which is basically the same anyway).

If you want the dashes in the invocation of the command but want to get rid of them inside the command, that's easy too:

% set actualOptions
# => -optA valA -optB valB -optC valC
% dict for {name value} $actualOptions { dict set undashedOptions [string range $name 1 end] $value }
% set undashedOptions
# => optA valA optB valB optC valC