mini-getopt

This is a tiny getopt implementation that closely mirrors Unix (POSIX) getopt(3).

Design goals were to match the behavior of its C namesake with very little code. The former makes it familiar, compatible, and flexible. The latter makes it fine to copy and paste into stand-alone scripts.

Non-goals: built-in help, long options, options after the first non-option, optional option arguments.

Existing solutions did not meet my chosen goals:

  • alternative getopt is nice but too long (~240 lines).
  • basic getopts includes a getopt that is very close to what I want but it has a slightly less convenient interface and is a few lines longer.
  • Better getopt has a different interface and is too long (~300 lines).
  • cmdline does not do POSIX-style options and technically introduces a dependency (on Tcllib).
  • getopt is cute and very short, but does not do POSIX-style options and cannot report errors.

Usage example:

    set u {[-frv] [-C path] [-p pattern] [file ...]}
    while {[getopt argv {C:p:frv} opt arg]} {
        if {$opt eq "?"} {
            puts stderr "usage: [file tail $::argv0] $u"
            exit 1
        }
        set opts(-$opt) $arg
    }

Remarks:

  • It modifies the supplied "argv" variable.
  • It does not take an "argc" variable since the user can easily use [llength $argv].
  • It returns the option character in the supplied "opt" variable.
  • For boolean options (flags), the "arg" output variable is set to 1.

Implementation:

proc getopt {argvvar optstring optvar argvar} {
    upvar $argvvar argv $optvar opt $argvar arg

    set arg [lindex $argv 0]                    ;# "" if argv empty
    set opt [string index $arg 1]               ;# "" if arg empty or length 1
    if {$opt eq "" || [string index $arg 0] ne "-"} {
        return 0                                ;# no option
    }
    set argv [lreplace $argv 0 0]               ;# tentative consumption
    if {$arg eq "--"} {
        return 0                                ;# terminate option processing
    }

    # consume option character
    set arg [string replace $arg 0 1]           ;# cut off the dash, too
    if {$arg ne ""} {                           ;# not yet exhausted?
        set argv [linsert $argv 0 -$arg]        ;# put it back for now
    }

    # check if option is valid
    set idx [string first $opt $optstring]
    if {$idx == -1 || $opt eq ":"} {
        puts stderr "[file tail $::argv0]: unknown option -$opt"
        set opt "?"
        return 1                                ;# can call again for more
    }

    # handle option arguments
    if {[string index $optstring [incr idx]] eq ":"} {
        if {$arg eq ""} {
            if {[llength $argv] == 0} {
                puts stderr "[file tail $::argv0]: -$opt requires an argument"
                set opt [expr {[string index $optstring 0] eq ":" ? ":" : "?"}]
            }
            set arg [lindex $argv 0]            ;# use next arg, no-op if empty
        }
        set argv [lreplace $argv 0 0]           ;# consume arg, no-op if empty
    } else {
        set arg 1                               ;# boolean flag
    }

    return 1
}