dispatch

This is a new control structure that I created an use extensively in my own code. I am posting it because I think it may be useful to other people as well. Please feel free to comment and/or criticize this code.

22Nov02 - Brian Theado - How about a brief description on what is does/what it is good for? I'm pretty slow at reading code and it is taking me longer to figure out what this is than I want to spend. By glancing at the code, it looks similar to the switch statement. Is that the case?

24Nov02 - Joe Mistachkin - Yes, it is similar to a switch statement. The primary way that they differ is that dispatch supports fully "dynamic" cases. Cases can be matched on any valid literals, variables, or commands. Matching can be done in all "standard" modes (exact, glob, regexp, and nocase). In the situation where there may be more than one match, only the first matching case is evaluated. Conforms to all other "standard" switch command behavior. See below for examples.

21Aug03 - Lars H - While examples of making your own control structures are often useful, it looks to me as though this is mostly doing things that the first form of switch (no {} around the list of patterns and bodies, hence one can subject the patterns to all sorts of substitutions) already provides. Or am I overlooking something? The -nocase option can be done with explicit [string tolower], although with variable patterns one might need a lot of these. The -expr option I don't quite understand.


 #
 # Example #1 (variables and commands)
 #

 set case_1 "this"
 set case_2 "that"
 set case_3 "foo"

 set string_to_match "THIS"

 dispatch -exact -nocase -- $string_to_match {
   $case_1 {
     # NOTICE we used a variable for this?
     puts stdout "MATCHED case #1."
   }
   $case_2 {
     # NOTICE we used a variable for this?
     puts stdout "MATCHED case #2."
   }
   $case_3 {
     # NOTICE we used a variable for this?
     puts stdout "MATCHED case #3."
   }
   [string trim $string_to_match] {
     # this case refers to the trimmed version of itself 
     # (the variable being matched), variations on this 
     # could prove quite useful.
     puts stdout "MATCHED trimmed version of self."
   }
   "literal" -
   default {
     # NOTE: the above "literal" case would fall through to this case.
     puts stdout "MATCHED default."
   }
 }
 #
 # Example #2 (use with regexp):
 #

 set email_regexp {^([0-9A-Za-z])([0-9A-Za-z_\.\-]*)@([0-9A-Za-z])([0-9A-Za-z\.\-]*)$}

 set string_to_match "[email protected]"
 
 dispatch -regexp -nocase -- $string_to_match {
   $email_regexp {
     # NOTICE we used a variable for this?
     puts stdout "MATCHED, valid email address."
   }
   default {
     puts stdout "MATCHED default."
   }
 } 

Main Source File (dispatch.tcl)

 ###############################################################################
 #
 # Tcl dispatch command
 #
 # Copyright (c) 2001-2003 by Joe Mistachkin.  All rights reserved.
 #
 #  written by: Joe Mistachkin <[email protected]>
 #  created on: 10/07/2001
 # modified on: 08/21/2003
 #
 ###############################################################################
 #
 # The authors hereby grant permission to use, copy, modify, distribute,
 # and license this software and its documentation for any purpose, provided
 # that existing copyright notices are retained in all copies and that this
 # notice is included verbatim in any distributions. No written agreement,
 # license, or royalty fee is required for any of the authorized uses.
 # Modifications to this software may be copyrighted by their authors
 # and need not follow the licensing terms described here, provided that
 # the new terms are clearly indicated on the first page of each file where
 # they apply.
 #
 # IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
 # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 # ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
 # DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 #
 # THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
 # IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
 # NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
 # MODIFICATIONS.
 #
 # GOVERNMENT USE: If you are acquiring this software on behalf of the
 # U.S. government, the Government shall have only "Restricted Rights"
 # in the software and related documentation as defined in the Federal
 # Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2).  If you
 # are acquiring the software on behalf of the Department of Defense, the
 # software shall be classified as "Commercial Computer Software" and the
 # Government shall have only "Restricted Rights" as defined in Clause
 # 252.227-7013 (c) (1) of DFARs.  Notwithstanding the foregoing, the
 # authors grant the U.S. Government and others acting in its behalf
 # permission to use and distribute the software in accordance with the
 # terms specified in this license.
 #
 ###############################################################################
 
 # REQUIRES Tcl 8.0+
 package require "Tcl" "8.0"
 
 # maximum possible number of arguments for dispatch proc
 set dispatch_maximum_arguments "8"
 
 # dispatch error string
 set dispatch_argument_error "wrong # args: should be \"dispatch ?switches? string pattern body ... ?default body?\""
 
 # THESE are ALL the allowed switches for the dispatch proc
 # (except for "--", which is a special case)
 set dispatch_switches [list "-exact" "-nocase" "-expr" "-glob" "-regexp" "-all"]
 
 # dispatch switch error string
 set dispatch_switch_error "bad option, must be one of: $dispatch_switches"
 
 # do not change this
 set dispatch_name "Tcl_dispatch"
 
 # do not change this
 set dispatch_version "2.7"
 
 proc valid_switch { argument variable_name } {
   #
   # check if valid switch (is it in the list?)...
   #
   if {[string index $argument "0"] == "-"} then {
     if {$variable_name != ""} then {
       if {$argument == "--"} then {
         # value 4 means "end of switches"
         # (this is always a valid switch)
         set result "4"
       } else {
         if {(([string index $argument "0"] == "-") && ([string is integer -strict [string range $argument "1" "end"]] != "0"))} then {
           # this is the integer value for use in the future...
           # value 3 means "valid switch"
           set result "3"
         } else {
           upvar "1" $variable_name valid_switches
   
           if {[lsearch $valid_switches $argument] != "-1"} then {
             # value 3 means "valid switch"
             set result "3"
           } else {
             # value 2 means "not a supported switch"
             set result "2"
           }
         }
       }
     } else {
       # value 1 means "invalid variable name" (in this context)
       set result "1"
     }
   } else {
     # value 0 means "not a switch OR not a supported switch"
     set result "0"
   }
 
   return $result
 }
 
 proc check_switch { argument variable_name force } {
   #
   # simply see if passed argument is a supported option
   #
   if {[string index $argument "0"] == "-"} then {
     if {$variable_name != ""} then {
       set switch_name [string range $argument "1" "end"]
 
       # get a handle on the variable (array) that we need to modify
       upvar "1" $variable_name switches
 
       # if always allow or if the switch is actually considered valid...
       if {(($force != "0") || ([info exists switches($switch_name)] != "0"))} then {
         # value 1 means "switch enabled"
         set switches($switch_name) "1"
 
         # value 1 means "processed switch"
         set result "1"
       } else {
         if {[string is integer -strict $argument] != "0"} then {
           # set the integer value for use in the future...
           set switches(value) $argument
 
           # value 1 means "processed switch"
           set result "1"
         } else {
           # value 2 means "invalid switch"
           set result "2"
         }
       }
     } else {
       # value 0 means "did NOT process switch"
       set result "0"
     }
   } else {
     # value 0 means "did NOT process switch"
     set result "0"
   }
 
   return $result
 }
 
 proc dispatch { args } {
   #
   # This is the OUTER dispatch proc.  It handles translation of switches
   # and then forwards the request to dispatch_internal.
   #
   global dispatch_argument_error
   global dispatch_maximum_arguments
   global dispatch_switch_error
   global dispatch_switches
 
   set result ""
 
   # the integer value for use in the future...
   set switches(value) "0"
 
   # all the possible switches...
   set switches(exact) "0"
   set switches(nocase) "0"
   set switches(expr) "0"
   set switches(glob) "0"
   set switches(regexp) "0"
   set switches(all) "0"
   set switches(end) "0"
 
   set count [llength $args]
 
   if {$count <= $dispatch_maximum_arguments} then {
     #
     # this loop is trying to find "the first non-switch argument"...
     #
     set invalid "0"
     set found "0"
     set index "0"
     while {(($index < $count) && ($found == "0") && ($invalid == "0"))} {
       set is_switch [valid_switch [lindex $args $index] dispatch_switches]
 
       switch -exact -- $is_switch {
         "0" {
           #
           # we are done, we found an actual non-switch argument...
           #
           set found "1"
         }
         "1" {
           #
           # invalid...
           #
           set invalid "1"
         }
         "2" {
           #
           # we are done, we found an invalid switch...
           #
           set invalid "1"
         }
         "3" {
           #
           # found a valid switch, process it
           #
           check_switch [lindex $args $index] switches "1"
 
           # skip to next index now
           set index [expr {$index + "1"}]
         }
         "4" {
           #
           # found FINAL switch, process it
           #
           check_switch [lindex $args $index] switches "1"
 
           # skip to next index now
           # next argument, this is still a switch
           set index [expr {$index + "1"}]
 
           set found "1"
         }
         default {
           # we found something invalid...???
           set invalid "1"
         }
       }
     }
 
     if {$found != "0"} then {
       # we must have at least two arguments left...
       if {$index < ($count - "1")} then {
         # what are we dispatching on?
         set dispatch_string [lindex $args $index]
         # advance to the next argument.
         set index [expr {$index + "1"}]
         # this is the body that contains the different possible matches...
         set dispatch_body [lindex $args $index]
         #
         # the magic number "2" in this command is the
         # parameter required for the uplevel commands
         # contained within dispatch_internal
         #
         set result [dispatch_internal $switches(exact) $switches(nocase) $switches(expr) $switches(glob) $switches(regexp) $switches(all) $switches(end) "2" $dispatch_string $dispatch_body]
 
         set dispatch_error "0"
       } else {
         set dispatch_error "1"
       }
     } else {
       if {$invalid != "0"} then {
         set dispatch_error "2"
       } else {
         set dispatch_error "1"
       }
     }
   } else {
     set dispatch_error "1"
   }
 
   switch -exact -- $dispatch_error {
     "1" {
       error $dispatch_argument_error
     }
     "2" {
       error $dispatch_switch_error
     }
   }
 
   return $result
 }
 
 proc dispatch_internal { dispatch_exact dispatch_nocase dispatch_expr dispatch_glob dispatch_regexp dispatch_all dispatch_end dispatch_level dispatch_string dispatch_body } {
   global dispatch_argument_error
   #
   # NOTE: This does NOT function EXACTLY the same as the "switch" command, but it's pretty darn close.
   #
   # 1. ALL of the standard switches for "switch" are supported plus "-nocase".
   # 2. default case can be anywhere (matching STOPS when it is found).
   # 3. string variables ARE supported (the main reason this proc exists).
   # 4. commands are supported for the PATTERNS as well as the script bodies
   #    (must be enclosed in curly braces)...
   #
   # NOTE: Obviously, the length of the dispatch_body argument list must be divisible by 2.
   #
   set result ""
 
   # must have some elements dispatch_body...
   if {[llength $dispatch_body] > "0"} then {
     # must have even number of elements in dispatch_body
     if {[llength $dispatch_body] % "2" == "0"} then {
       #
       # initially, we will return null if nothing matches...
       # same as switch
       #
       set evaluated "0"
       set matched "0"
       foreach {this_pattern this_body} $dispatch_body {
         #
         # make sure we aren't just searching for a proc body
         #
         if {$matched == "0"} then {
           #
           # check if it's the default
           #
           if {$this_pattern == "default"} then {
             # THIS ALWAYS MATCHES, regardless of switches
             # presumably, default is the last one
             set matched "1"
           } else {
             #
             # check if string variable
             #
             if {[string index $this_pattern "0"] == "\$"} then {
               # get variable name portion only
               set variable_name [string range $this_pattern "1" "end"]
 
               # unset in case we set it previously
               # BUGFIX: SQUASH annoying error messages in errorInfo!
               if {[info exists variable_value] != "0"} then {
                 catch {unset variable_value}
               }
               #
               # get variable value from calling proc
               # (could this be done better with upvar?)
               #
               # this needs the [list] command to account for the pathological 
               # case of {this_happy variable_name}.
               #
               set variable_value [uplevel $dispatch_level [list set $variable_name]]
             } else {
               #
               # command, interesting...
               #
               if {[string index $this_pattern "0"] == "\["} then {
                 # get command portion only
                 set variable_name [string range $this_pattern "1" "end-1"]
 
                 # just evaluate the command using uplevel...
                 # [list] is not required here, $variable_name contains a 
                 # complete command in proper form list form.
                 set variable_value [uplevel $dispatch_level $variable_name]
               } else {
                 #
                 # must be some kind of string constant
                 #
                 set variable_value $this_pattern
               }
             }
 
             if {$dispatch_regexp != "0"} then {
               #
               # regexp (for experts only!)
               #
               if {$dispatch_nocase != "0"} then {
                 #
                 # case insensitive specified
                 # check if we matched the value...
                 #
                 if {[regexp -nocase -- $variable_value $dispatch_string] != "0"} then {
                   set matched "1"
                 } else {
                   set matched "0"
                 }
               } else {
                 #
                 # case sensitive is the default
                 # check if we matched the value...
                 #
                 if {[regexp -- $variable_value $dispatch_string] != "0"} then {
                   set matched "1"
                 } else {
                   set matched "0"
                 }
               }
             } else {
               if {$dispatch_glob != "0"} then {
                 #
                 # string match (always a family favorite)
                 #
                 if {$dispatch_nocase != "0"} then {
                   #
                   # case insensitive specified
                   # check if we matched the value...
                   #
                   if {[string match [string tolower $variable_value] [string tolower $dispatch_string]] != "0"} then {
                     set matched "1"
                   } else {
                     set matched "0"
                   }
                 } else {
                   #
                   # case sensitive is the default
                   # check if we matched the value...
                   #
                   if {[string match $variable_value $dispatch_string] != "0"} then {
                     set matched "1"
                   } else {
                     set matched "0"
                   }
                 }
               } else {
                 if {$dispatch_expr != "0"} then {
                   #
                   # NEW: check to see if the truth value of the dispatch arm by itself is non-zero
                   #      (it may have a dynamic value).
                   #
                   if {[expr {int($variable_value)}]} then {
                     set matched "1"
                   } else {
                     set matched "0"
                   }
                 } else {
                   # dispatch_exact is the default
                   if {$dispatch_nocase != "0"} then {
                     #
                     # case insensitive specified
                     # check if we matched the value...
                     #
                     if {[string tolower $dispatch_string] == [string tolower $variable_value]} then {
                       set matched "1"
                     } else {
                       set matched "0"
                     }
                   } else {
                     #
                     # case sensitive is the default
                     # check if we matched the value...
                     #
                     if {$dispatch_string == $variable_value} then {
                       set matched "1"
                     } else {
                       set matched "0"
                     }
                   }
                 }
               }
             }
           }
         }
 
         if {$matched != "0"} then {
           #
           # check for "search for next proc body" like switch does
           #
           if {$this_body == "-"} then {
             #
             # skill skipping to next script body...
             #
             continue
           } else {
             #
             # evaluate this script body (IN THE PROPER LEVEL) and exit loop
             # [list] is not required at this level because the body is a script, not a command.
             #
             set result [uplevel $dispatch_level $this_body]
             set evaluated "1"
             set matched "0"
 
             if {$dispatch_all == "0"} then {
               #
               # if they are NOT allowing multiple (default)
               # break out of loop
               #
               break
             }
           }
         }
       }
 
       set dispatch_error "0"
     } else {
       set dispatch_error "1"
     }
   } else {
     set dispatch_error "1"
   }
 
   if {$dispatch_error != "0"} then {
     error $dispatch_argument_error
   }
 
   return $result
 }
 
 proc dispatch_terminate {} {
   global dispatch_name
   #
   # forget package
   #
   package forget $dispatch_name
 
   #
   # kill vars
   #
   foreach this_global [info globals] {
     if {[string match "dispatch_*" $this_global] != "0"} then {
       # nuke variable in global scope... (dead)
       uplevel "#0" unset $this_global
     }
   }
 
   #
   # kill procs
   #
   rename dispatch ""
   rename dispatch_internal ""
   rename valid_switch ""
   rename check_switch ""
   rename dispatch_terminate ""
 
   return "0"
 }
 
 # loaded OK, provide package
 package provide $dispatch_name $dispatch_version
 
 # // end of file

Tests File (dispatch_sample.tcl)

 ###############################################################################
 #
 # Tcl dispatch command sample and [torture] test suite
 #
 # Copyright (c) 2001-2003 by Joe Mistachkin.  All rights reserved.
 #
 #  written by: Joe Mistachkin <[email protected]>
 #  created on: 10/07/2001
 # modified on: 05/06/2003
 #
 ###############################################################################
 #
 # The authors hereby grant permission to use, copy, modify, distribute,
 # and license this software and its documentation for any purpose, provided
 # that existing copyright notices are retained in all copies and that this
 # notice is included verbatim in any distributions. No written agreement,
 # license, or royalty fee is required for any of the authorized uses.
 # Modifications to this software may be copyrighted by their authors
 # and need not follow the licensing terms described here, provided that
 # the new terms are clearly indicated on the first page of each file where
 # they apply.
 #
 # IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
 # FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
 # ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
 # DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
 # POSSIBILITY OF SUCH DAMAGE.
 #
 # THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.  THIS SOFTWARE
 # IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
 # NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
 # MODIFICATIONS.
 #
 # GOVERNMENT USE: If you are acquiring this software on behalf of the
 # U.S. government, the Government shall have only "Restricted Rights"
 # in the software and related documentation as defined in the Federal
 # Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2).  If you
 # are acquiring the software on behalf of the Department of Defense, the
 # software shall be classified as "Commercial Computer Software" and the
 # Government shall have only "Restricted Rights" as defined in Clause
 # 252.227-7013 (c) (1) of DFARs.  Notwithstanding the foregoing, the
 # authors grant the U.S. Government and others acting in its behalf
 # permission to use and distribute the software in accordance with the
 # terms specified in this license.
 #
 ###############################################################################
 
 # require Tcl 8.0+
 package require Tcl 8.0
 
 # attempt to load dispatch package
 source "dispatch.tcl"
 
 # require dispatch package 2.0+ to be loaded...
 package require Tcl_dispatch 2.0
 
 proc DispatchSample1 { string_to_match } {
   set test_1 "this"
   set test_2 "that"
   set test_3 "foo"
   set test_4 "not used"
   set test_5 "bar"
   set test_6 "FOO"
   set test_7 "BAR"
 
   dispatch $string_to_match {
     $test_1 {
       puts stdout "MATCHED #1\n"
     }
     $test_2 {
       puts stdout "MATCHED #2\n"
     }
     $test_3 {
       puts stdout "MATCHED #3\n"
     }
     "test 4" {
       puts stdout "MATCHED #4\n"
     }
     $test_5 -
     $test_6 -
     $test_7 {
       puts stdout "MATCHED #5,#6,#7\n"
     }
     default {
       puts stdout "MATCHED DEFAULT!\n"
     }
   }
 }
 
 proc DispatchSample2 { string_to_match } {
   set test_1 "this"
   set test_2 "that"
   set test_3 "foo"
   set test_4 "not used"
   set test_5 "bar"
   set test_6 "FOO"
   set test_7 "BAR"
   set test_8 "NOEVAL"
 
   dispatch $string_to_match {
     $test_1 {
       puts stdout "MATCHED #1\n"
     }
     $test_2 {
       puts stdout "MATCHED #2\n"
     }
     $test_3 {
       puts stdout "MATCHED #3\n"
     }
     "test 4" {
       puts stdout "MATCHED #4\n"
     }
     $test_5 -
     $test_6 -
     $test_7 {
       puts stdout "MATCHED #5,#6,#7\n"
     }
     $test_8 -
   }
 }
 
 proc DispatchSample3 { string_to_match } {
 
   dispatch -glob -- $string_to_match {
     "1" {
       puts stdout "MATCHED #1\n"
     }
     "2" {
       puts stdout "MATCHED #2\n"
     }
     "3" {
       error "cannot match #3"
     }
     "*" {
       puts stdout "MATCHED *\n"
     }
   }
 }
 
 proc DispatchSample4 { string_to_match } {
   # MALFORMED dispatch statement test
 
   dispatch $string_to_match {
     "1" {
       puts stdout "MATCHED #1\n"
     }
     "2" {
       puts stdout "MATCHED #2\n"
     }
     "3"
   }
 }
 
 proc DispatchSample5 { string_to_match } {
   set email_regexp {^([0-9A-Za-z])([0-9A-Za-z_\.\-]*)@([0-9A-Za-z])([0-9A-Za-z\.\-]*)$}
 
   dispatch -regexp -nocase -- $string_to_match {
     {^([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])\.([01]?\d\d?|2[0-4]\d|25[0-5])$} {
       puts stdout "MATCHED, VALID IP\n"
     }
     {^([01]?[0123456789][0123456789]?|2[0-4][0123456789]|25[0-5])\.([01]?[0123456789][0123456789]?|2[0-4][0123456789]|25[0-5])\.([01]?[0123456789][0123456789]?|2[0-4][0123456789]|25[0-5])\.([01]?[0123456789][0123456789]?|2[0-4][0123456789]|25[0-5])$} {
       puts stdout "MATCHED, VALID IP, PRE 8.0\n"
     }
     $email_regexp {
       # NOTICE we used a variable for this?
       puts stdout "MATCHED, VALID EMAIL ADDRESS\n"
     }
     {(<A )(.*?)(HREF=\")(.*?)(\")} {
       puts stdout "MATCHED, VALID HYPERLINK\n"
     }
     default {
       puts stdout "NOT MATCHED REGEXP\n"
     }
   }
 }
 
 proc DispatchSample6 { string_to_match } {
   set sample6_var "this_is_a_test"
 
   dispatch -exact -nocase -- $string_to_match {
     "test" {
       puts stdout "MATCHED TEST\n"
     }
     {[string repeat $sample6_var "2"]} {
       puts stdout "MATCHED TEST * 2\n"
     }
     {\[fakecommand\]} {
       puts stdout "MATCHED FAKE COMMAND\n"
     }
     {[string repeat $sample6_var "3"]} -
     {[string repeat $sample6_var "4"]} {
       puts stdout "MATCHED TEST * 3 OR 4\n"
 
       if {$string_to_match == "this_is_a_testthis_is_a_testthis_is_a_testthis_is_a_test"} then {
         puts stdout "MATCHED TEST * 4\n"
       } else {
         puts stdout "MATCHED TEST * 3\n"
       }
     }
     default {
       puts stdout "NOT MATCHED SAMPLE\n"
     }
   }
 }
 
 proc DispatchSample7 { string_to_match } {
   #
   # default string test
   #
   dispatch $string_to_match {
     "1" {
       puts stdout "MATCHED #1\n"
     }
     "2" {
       puts stdout "MATCHED #2\n"
     }
     "3" {
       puts stdout "MATCHED #3\n"
     }
     "4" {
       puts stdout "MATCHED #4\n"
     }
     "5" {
       puts stdout "MATCHED #5\n"
     }
     "6" {
       puts stdout "MATCHED #6\n"
     }
     "default" {
       puts stdout "MATCHED DEFAULT!\n"
     }
   }
 }
 
 proc DispatchSample8 { string_to_match } {
   #
   # multiple glob test...
   #
   dispatch -glob -all -- $string_to_match {
     "1" {
       puts stdout "MATCHED #1\n"
     }
     "1*" {
       puts stdout "MATCHED GLOB 1*\n"
     }
     "2" {
       puts stdout "MATCHED #2\n"
     }
     "2*" {
       puts stdout "MATCHED GLOB 2*\n"
     }
     "default" {
       puts stdout "MATCHED DEFAULT!\n"
     }
   }
 }
 
 proc DispatchSample9 { string_to_match } {
   #
   # invalid switch test
   #
   dispatch -glob -all -notvalid -- $string_to_match {
     "1" {
       puts stdout "MATCHED #1\n"
     }
     "2" {
       puts stdout "MATCHED #2\n"
     }
     "default" {
       puts stdout "MATCHED DEFAULT!\n"
     }
   }
 }
 
 proc DispatchSample10 { string_to_match } {
   #
   # valid switch-like looking argument after end of switches
   #
   dispatch -glob -- -notvalid {
     "-notvalid" {
       puts stdout "MATCHED -notvalid\n"
     }
     "default" {
       puts stdout "MATCHED DEFAULT!\n"
     }
   }
 }
 
 ###############################################################################
 # series 1, test ``normal`` usage
 ###############################################################################
 puts stdout "TEST #1, should match #1..."
 DispatchSample1 "this"
 puts stdout "TEST #2, should match #2..."
 DispatchSample1 "that"
 puts stdout "TEST #3, should match #3..."
 DispatchSample1 "foo"
 puts stdout "TEST #4, should match #4..."
 DispatchSample1 "test 4"
 puts stdout "TEST #4a, should DEFAULT..."
 DispatchSample1 "not_in_list"
 puts stdout "TEST #5, should match #5,#6,#7..."
 DispatchSample1 "bar"
 puts stdout "TEST #6, should match #5,#6,#7..."
 DispatchSample1 "FOO"
 puts stdout "TEST #7, should match #5,#6,#7..."
 DispatchSample1 "BAR"
 
 ###############################################################################
 # series 2, do bad things
 ###############################################################################
 puts stdout "TEST #8, should not match anything..."
 DispatchSample2 "not_in_list"
 puts stdout ""
 
 puts stdout "TEST #9, should match, but not evaluate anything..."
 DispatchSample2 "NOEVAL"
 puts stdout ""
 
 puts stdout "TEST #10, should give error..."
 catch {DispatchSample3 "3"} dispatch_error
 puts stdout "ERROR: `` $dispatch_error ``"
 puts stdout ""
 
 puts stdout "TEST #11, should give error (malformed dispatch)..."
 catch {DispatchSample4 "1"} dispatch_error
 puts stdout "ERROR: `` $dispatch_error ``"
 puts stdout ""
 
 puts stdout "TEST #12, should match glob..."
 DispatchSample3 "4"
 
 puts stdout "TEST #13, should match regexp IP..."
 DispatchSample5 "198.102.29.10"
 
 puts stdout "TEST #14, should NOT match regexp..."
 DispatchSample5 "198.102.29.290"
 
 puts stdout "TEST #15, should NOT match regexp..."
 DispatchSample5 "*"
 
 puts stdout "TEST #16, should match regexp email..."
 DispatchSample5 "[email protected]"
 
 puts stdout "TEST #17, should match regexp hyperlink..."
 DispatchSample5 "<A HREF=\"http://www.scriptics.com/\">"
 
 puts stdout "TEST #18, should match command test..."
 DispatchSample6 "test"
 
 puts stdout "TEST #19, should match command test * 2..."
 DispatchSample6 "this_is_a_testthis_is_a_test"
 
 puts stdout "TEST #20, should match fake command..."
 set test20_var {\[fakecommand\]}
 DispatchSample6 $test20_var
 
 puts stdout "TEST #21, should match command test * 3 OR 4..."
 DispatchSample6 "this_is_a_testthis_is_a_testthis_is_a_test"
 
 puts stdout "TEST #22, should match command test * 3 OR 4..."
 DispatchSample6 "this_is_a_testthis_is_a_testthis_is_a_testthis_is_a_test"
 
 puts stdout "TEST #23, default string test..."
 DispatchSample7 "8"
 
 puts stdout "TEST #24, multiple test 1, should match 1, glob 1*, and default..."
 DispatchSample8 "1"
 
 puts stdout "TEST #25, multiple test 2, should match 2, glob 2*, and default..."
 DispatchSample8 "2"
 
 puts stdout "TEST #26, multiple test 3, should match default..."
 DispatchSample8 "3"
 
 puts stdout "TEST #27, invalid switch test, should give error..."
 catch {DispatchSample9 "3"} dispatch_error
 puts stdout "ERROR: `` $dispatch_error ``"
 
 puts stdout "TEST #28, switch-like argument after end of switches test, should match -notvalid..."
 DispatchSample10 ""

Version History

  07/Oct/2001 Version 1.00 -- initial version
  19/Nov/2002 Version 2.40 -- initial public release version
  06/May/2003 Version 2.60 -- updated, various internal changes
  21/Aug/2003 Version 2.70 -- updated, added -expr switch, minor tweaks

elfring 2003-11-01 Is there a relationship to the function library "liboop"? Can an adaptor be created to achieve a cooperation?

Alternate package by Andy Goth

AMG: Here is another command called [dispatch]. In addition to switch-like script execution, this command allows each script to have arguments, implemented in terms of argparse.

Code

package require Tcl 8.6
package require argparse
package provide dispatch 0.1

# dispatch --
# Table-driven script execution.
#
# The first argument is a list containing the method name and any number of
# arguments to the method.
#
# The second argument is the method table, which is a list alternating between
# method names and definitions.
#
# The method table is searched using unambiguous prefix matching on the method
# name.  There is no facility for defaults or other kinds of patterns.
#
# Method definitions are lists of zero or more elements.  The final element is
# the script body to be executed, and any preceding elements are used as initial
# arguments to [argparse], with the final argument being the input argument sans
# its first element, that being the method name.
#
# Argument parsing and script execution are performed in the caller's context,
# which is one of the main distinctions between [dispatch] and normal command
# dispatch using [namespace ensemble] or similar systems.
proc ::dispatch {input table} {
    # Look up the method definition in the method table.
    set method [dict get $table [tcl::prefix match -message method\
            [dict keys $table] [lindex $input 0]]]

    # Parse method arguments.
    if {[llength $method] > 1} {
        uplevel 1 [list ::argparse {*}[lrange $method 0 end-1]\
                [lrange $input 1 end]]
    } elseif {[llength $input] > 1} {
        return -code error "wrong # args: should be \"[lindex $input 0]\""
    }

    # Execute method body.
    uplevel 1 [lindex $method end]
}

Here's the pkgIndex.tcl:

package ifneeded dispatch 0.1 [list source [file join $dir dispatch.tcl]]

Examples

% package require dispatch
0.1
% set table {
foo {-boolean {
    -hello
    {-world= -default 42}
} {
    puts "method: foo"
    if {$hello} {
        puts "world: $world"
    }
}} bar {{
    puts "method: bar"
}}}
% dispatch {foo -hello} $table
method: foo
world: 42
% set hello
1
% set world
42
% dispatch bar $table
method: bar
% dispatch ba $table
method: bar
% dispatch quux $table
bad method "quux": must be foo or bar
% dispatch {bar -hello} $table
wrong # args: should be "bar"
% dispatch {foo -world} $table
-world requires an argument

Ideas

I've tried to keep things as simple as possible for now, so I'm unlikely to do any of the following until I have a real need.

Hierarchical methods

Currently, hierarchical methods can be implemented via nested invocation of [dispatch]. Flattening the implementation by allowing the method names to be lists might be an attractive alternative.

dispatch $input {
{list search} {{...} {...}}
{list sort}   {{...} {...}}
{dict append} {{dictVar key strings*} {...}}
{dict exists} {{dictVal keys*!} {...}}
}

Default methods

Maybe some way to specify default handlers? Or perhaps also wildcard and other kinds of pattern matching? This would lose the dict performance benefits and would complicate the code, so I have not implemented it. Even simply having "default" would not work with dict. "default" should only be special when it is the final key, and that same word "default" may also be used earlier and be interpreted literally. dict is supposed to be agnostic about key ordering, and it does not allow keys to appear multiple times.

One possibility is to move away from switch compatibility and jettison the word "default". Instead, bring in the hierarchical methods idea from above, while allowing one method name list to be a prefix of another. Execute the method with the longest matching name prefix, then (as is current) assign the subsequent input arguments via argparse.

dispatch {a b c d e} {
{a b c} {{D E} {...}}
{a b}   {{method args*} {error "unknown method: a b $method"}}
{w x y} {{...} {...}}
w       {{...} {...}}
{}      {{method args*} {error "unknown method: $method"}}
}

Or something like that. The above example isn't very useful though since there's already much better error reporting listing the valid methods.

Customizing tcl::prefix

Maybe don't always call the input a "method"; let the user specify the -message switch to tcl::prefix. Also, maybe let the user specify -exact.