Tcl Module basic template

Sometimes it's convenient to use an existing Tcl module file as a starting point for a new one, or to quickly change a version number for testing.

Following the general principal of programming that roughly says 'where practical, define things once and only once', below is a basic template for a Tcl .tm file which allows you to change the package name and associated namespace, as well as the version number, simply by renaming the .tm file itself.

As per the standard Tcl module naming convention, save as a file with a name in the format <modulename>-<version>.tm e.g mymodule-1.0.tm

When placed in one of the folders in your 'module path' (see result of command ::tcl::tm::list) 'package require mymodule' will now automatically provide the module with version number 1.0 with your functions under the namespace ::mymodule

  namespace eval pkgtemp {
      set modver [file root [file tail [info script]]]
      lassign [split $modver -] ::pkgtemp::ns ::pkgtemp::version
  }
  package provide $::pkgtemp::ns [namespace eval $::pkgtemp::ns {
      variable version $::pkgtemp::version
      #initialise your module's namespace here (or in the next namespace eval block if desired)
      set version ;#this statement must be the last line in this block (version number is returned to the 'package provide' statement)
  }]
  namespace eval $::pkgtemp::ns {
      #module procs here
  }
  namespace delete pkgtemp

The temporary namespace pkgtemp seems pretty unlikely to collide with existing namespaces - and is simply there to avoid polluting/overwriting any variables an application may have in the global namespace.


Lars H: Isn't it a principle for modules that filename hierarchy is reflected in package name and package namespace? For example the foo::bar package version 1.0 is in a file foo/bar-1.0.tm relative to a directory on the module search path. The above code would just use bar as package name and namespace.

For avoiding global variables, one can also use apply:

 apply {code {
    set modver [file root [file tail [info script]]]
    lassign [split $modver -] ns version
    package provide $ns $version
    namespace eval $ns $code
 } ::} {
    # Module procs here, where current namespace is that of the module.
    # Package version can, if needed, be accessed as [uplevel 1 {set version}]
 }

JMN Thanks - That's a good point about module hierarchies. I have most of mine directly on the module path - but it should be possible to adjust the template code to work out how far down the hierarchy the .tm file is.

I'm less sure about apply in this situation - as one of the goals was to avoid hardcoding any namespaces in the '# Module procs here' section.. but perhaps it can be worked in.

Lars H: Note the namespace eval being applied to $code! A proc in the code argument (the "block" that begins "# Module procs here...") will thus create a command in the namespace derived from the file name (and variable will create a variable there), exactly as in the first template.

JMN Aha.. I completely misread this code. Cheers - this is nicer than creating and deleting a namespace.

Here is an attempt at automatically handling arbitrary hierarchies of module namespaces. The code is now a bit unwieldy for a template..

It's almost a case for an additional helper function or two within the tcl::tm::path ensemble

e.g something like: 'tcl::tm::path which ?tm_path'

 apply {code {
    set mypath [file dirname [file normalize [info script]]]
    set mysegs [file split $mypath]
    foreach libpath [tcl::tm::list] {
        set libsegs [file split $libpath]
        if { [file join {*}[lrange $mysegs 0 [llength $libsegs]-1]]
             eq
             [file join {*}$libsegs] } {
            # mypath is libpath or below
            set overhang [lrange $mysegs [llength $libsegs] end]
            break
        }
    }
    set modver [file root [file tail [info script]]]
    lassign [split $modver -] nsfinal version
    set ns [join [concat $overhang $nsfinal] ::]
    package provide $ns $version
    namespace eval $ns $code
 } ::} {
    # Module procs here, where current namespace is that of the module.
    # Package version can, if needed, be accessed as [uplevel 1 {set version}]
 }

Fixed for

  • Incorrect use of file join
  • Off-by-one errors in lrange
  • Shortening the wrong path in the comparison

See also