Forward-compatible tcl::prefix

AMG: Here is a Tcl 8.5-compatible implementation of tcl::prefix that passes the 8.6.8 test suite. It uses throw, so also use forward-compatible try and throw to make it actually work in 8.5.

proc tcl::prefix {subcommand args} {
    switch $subcommand {
    a - al - all {
        # Process arguments.
        if {[llength $args] != 2} {
            throw {TCL WRONGARGS} "wrong # args: should be\
                    \"tcl::prefix all table string\""
        }
        lassign $args table string

        # Return list of all strings with the given prefix.
        lsearch -all -inline $table [regsub -all {[][?*\\]} $string {\\&}]*
    } l - lo - lon - long - longe - longes - longest {
        # Process arguments.
        if {[llength $args] != 2} {
            throw {TCL WRONGARGS} "wrong # args: should be\
                    \"tcl::prefix longest table string\""
        }
        lassign $args table string

        # Search for the longest common prefix.
        foreach entry $table {
            if {[string equal -length [string length $string] $entry $string]} {
                if {![info exists common]} {
                    set common $entry
                } else {
                    for {set i 0} {$i < [string length $common]
                                && $i < [string length $entry]} {incr i} {
                        if {[string index $common $i]
                         ne [string index $entry $i]} {
                            break
                        }
                    }
                    set common [string range $common 0 [expr {$i - 1}]]
                }
            }
        }

        # Return the longest common prefix, or empty string if no matches.
        if {[info exists common]} {
            return $common
        }
    } m - ma - mat - matc - match {
        # Process arguments.
        if {[llength $args] < 2} {
            throw {TCL WRONGARGS} "wrong # args: should be\
                    \"tcl::prefix match ?options? table string\""
        }
        lassign [lrange $args end-1 end] table string
        set args [lrange $args 0 end-2]
        set message option
        while {[llength $args]} {
            set args [lassign $args arg]
            switch $arg {
            -ex - -exa - -exac - -exact {
                # -exact switch.
                set exact {}
            } -m - -me - -mes - -mess - -messa - -messag - -message {
                # -message switch.  Next argument is the message string.
                if {![llength $args]} {
                    throw {TCL OPERATION NOARG} "missing value for -message"
                }
                set args [lassign $args message]
            } -er - -err - -erro - -error {
                # -error switch.  Next argument is the error options dict.
                if {![llength $args]} {
                    throw {TCL OPERATION NOARG} "missing value for -error"
                }
                set args [lassign $args options]
                if {[llength $options] & 1} {
                    throw {TCL VALUE DICTIONARY} "error options must have an\
                            even number of elements"
                }
            } -e {
                # Ambiguous switch.
                throw [list TCL LOOKUP INDEX option $arg] "ambiguous option\
                        \"$arg\": must be -error, -exact, or -message"
            } default {
                # Invalid switch.
                throw [list TCL LOOKUP INDEX option $arg] "bad option\
                        \"$arg\": must be -error, -exact, or -message"
            }}
        }

        # Always accept exact match, no questions asked, even if it happens to
        # also be the prefix for another string in the table.
        if {$string in $table} {
            return $string
        }

        # Attempt prefix matching unless -exact was used.  Accept a prefix match
        # if unambiguous.
        if {![info exists exact]} {
            set matches [prefix all $table $string]
            if {[llength $matches] == 1} {
                return [lindex $matches 0]
            }
        }

        # Match failed.  Assemble and return the error result.
        if {![info exists exact] && [llength $matches]} {
            set message "ambiguous $message \"$string\": "
        } else {
            set message "bad $message \"$string\": "
        }
        if {![llength $table]} {
            append message "no valid options"
        } else {
            if {[llength $table] > 1} {
                lset table end "or [lindex $table end]"
            }
            append message "must be [join $table\
                    {*}[if {[llength $table] > 2} {list ", "}]]"
        }
        if {![info exists options]} {
            set options [list -level 0 -code error\
                    -errorcode [list TCL LOOKUP INDEX $message $string]]
        }
        if {![dict size $options]} {
            set message {}
        } elseif {![dict exists $options -code]} {
            dict set options -code error
        }
        dict incr options -level
        return {*}$options $message
    } default {
        # Invalid subcommand.
        throw [list TCL LOOKUP SUBCOMMAND $arg] "unknown or ambiguous\
                subcommand \"$arg\": must be all, longest, or match"
    }}
}