subproc

by slebetman:

Procs with subcommands are very common in Tcl and Tk. A good example is the string command which rolls all string operations into a single proc. I personally like this style of programming (though it may be verbose at times) so I've written the following helper proc to construct such procs-with-subcommands:

SYNOPSIS:

subproc name ?init? body

DESCRIPTION:

The subproc command creates a new tcl procedure named name just like a proc command would. The body of the subproc command contains subcommands of the newly created procedure. If init is specified its contents will be evaluated before the specified subcommand in the same context as the subcommand. The subcommands are defined in the format:

subcommand_name args body

The format of subcommands are similar to the arguments of the proc command. If the subcommand_name is default then its body is evaluated if no other subcommands matches the invoked command. The parameter args is a list of parameters to pass to the specified subcommand. Just like a proc, if the last argument is args then all remaining parameters are passed as a single list. Unlike a proc however, the list of parameters args does not support default values.

IMPLEMENTATION:

It's quite ugly but it works:

    proc subproc {name args} {
      # Process the optional initial body code:
      if {[llength $args] == 2} {
        set body [lindex $args 0]
        set args [lindex $args 1]
      } else {
        set args [lindex $args 0]
      }
      
      # Strip comments from the subproc body so that we can
      # support comments in between subprocs:
      set procs ""
      foreach x [split $args "\n"] {
        if {[string index [string trim $x] 0] != "#"} {
          append procs "$x\n"
        }
      }
      
      # Construct the proc:
      append body "\nset op \[lindex \$args 0\]\n"
      append body "set args \[lrange \$args 1 end\]\n"
      append body "switch -exact \$op \{\n"
      foreach {op params script} $procs {
        if {$op != "default"} {
          append body "{$op} \{\n"
        } else {
          append body "default \{\n"
        }
        set paramLength [llength $params]
        if {[lindex $params end] == "args"} {
          append body "if {\[llength \$args\] < [expr $paramLength-1]} "
        } else {
          append body "if {\[llength \$args\] != $paramLength} "
        }
        append body "{error {wrong # args: should be \"$name $op $params\"}}\n"
        for {set i 0} {$i < $paramLength} {incr i} {
          set par [lindex $params $i]
          if {$par == "args"} {
            append body "set {$par} \[lrange \$args $i end\]\n"
          } else {
            append body "set {$par} \[lindex \$args $i\]\n"
          }
        }
        append body "$script\}\n"
      }
      append body "\}\n"
      proc $name {args} $body
    }

EXAMPLES:

Defining a subproc is easy and defining the body of subcommands work just like writing a regular proc:

    subproc listOp {

      # Returns the list element at idx.
      index {L idx} {
        return [lindex $L $idx]
      }

      # Silly code to dump the list.
      dump {L} {
        foreach x $L {
          if {[listOp length $x] == 1} {
            puts $x
          } else {
            foreach y $x {
              puts "  $y"
            }
          }
        }
      }

      # Returns elements in the list from start to end.
      range {L start end} {
        return [lrange $L $start $end]
      }

      # Returns the number of elements in the list.
      length {L} {
        return [llength $L]
      }
    }

Now you can use the proc listOp:

    % set test [list This is "very cool"]
    This is {very cool}
    % listOp length $test
    3
    % listOp dump $test
    This
    is
      very
      cool

The following example illustrates the use of the default subcommand and using args in the subcommand definition:

    subproc config {
      # Init code for all subcommands:
      global configArray
      } {

      # Initialise config array with defaults:
      init {} {
        array set configArray {
          cfg1 1
          cfg2 "two"
        }
        return
      }

      # Get configuration:
      get {args} {
        set ret ""
        foreach x $args {
          lappend ret $configArray($x)
        }
        return $ret
      }

      # Set configuration
      # Generate error if configuration item doesn't exist:
      set {args} {
        foreach {key val} $args {
          set configArray($key)
          set configArray($key) $val
        }
        return
      }

      # If called without any subcommand, dump configs:
      default {} {
        return [array get configArray]
      }
    }
    % config init
    % config set cfg1 100
    % puts [config get cfg1]
    100
    % config set cfg1 100 cfg2 200
    % puts [config]
    cfg1 100 cfg2 200

See also