Skip to content

Instantly share code, notes, and snippets.

@nat-418
Last active November 5, 2022 23:13
Show Gist options
  • Save nat-418/0155a9f8093385de39e83d41e61606d5 to your computer and use it in GitHub Desktop.
Save nat-418/0155a9f8093385de39e83d41e61606d5 to your computer and use it in GitHub Desktop.
How to make Tcl packages and modules

How to make Tcl packages and modules

Tcl provides a package system that allows Tclers to share code. This package system supports two different formats that serve different use-cases:

  1. The "package" or "8.5" format is more verbose, but allows package authors more flexibility through the pkgIndex.tcl file. This is designed for complex, multi-file programs.

  2. The "module" or tm format is simpler, reducing all package setup to the module file name, e.g. foo-1.0.0.tm. This is designed for simple, single-file programs.

Both formats can be used with the same syntax, e.g. package require foo 1.0.0.

Creating a Tcl package

We are going to steal an example from the Tcl 8.5 tutorial, and create a package that provides a stack data structure.

First we need to write the code we want to share, tutstack.tcl:

# Register the package
package provide tutstack 1.0
package require Tcl      8.5

# Create the namespace
# Note: Tcl namespaces are traversed using `::`.
namespace eval ::tutstack {
    # Export commands
    namespace export create destroy push pop peek empty

    # Set up state
    variable stack
    variable id 0
}

# Create a new stack
proc ::tutstack::create {} {
    variable stack
    variable id

    set token "stack[incr id]"
    set stack($token) [list]
    return $token
}

# Destroy a stack
proc ::tutstack::destroy {token} {
    variable stack

    unset stack($token)
}

# Push an element onto a stack
proc ::tutstack::push {token elem} {
    variable stack

    lappend stack($token) $elem
}

# Check if stack is empty
proc ::tutstack::empty {token} {
    variable stack

    set num [llength $stack($token)]
    return [expr {$num == 0}]
}

# See what is on top of the stack without removing it
proc ::tutstack::peek {token} {
    variable stack

    if {[empty $token]} {
	error "stack empty"
    }

    return [lindex $stack($token) end]
}

# Remove an element from the top of the stack
proc ::tutstack::pop {token} {
    variable stack

    set ret [peek $token]
    set stack($token) [lrange $stack($token) 0 end-1]
    return $ret
}

Then we need to create the pkgIndex.tcl file:

echo 'pkg_mkIndex .' | tclsh
Which should create the following:

# Tcl package index file, version 1.1
# This file is generated by the "pkg_mkIndex" command
# and sourced either when an application starts up or
# by a "package unknown" script.  It invokes the
# "package ifneeded" command to set up package-related
# information so that packages will be loaded automatically
# in response to "package require" commands.  When this
# script is sourced, the variable $dir must contain the
# full path name of this file's directory.

package ifneeded tutstack 1.0 [list source [file join $dir tutstack.tcl]]

We can see here that this file is telling Tcl where to find our package, but if we need to do more than that we can write whatever we want in here.

Tcl scans for packages in various directories, you can see which using:

echo 'puts $auto_path | tclsh'

Paths can be added to the list using the $TCLLIBPATH environment variable.

The Tcl manual has more information about the various package commands.

Creating a Tcl module

To writ the above as a module we create tutstack-1.0.0.tm:

# Create the namespace
namespace eval ::tutstack {
    # Export commands
    namespace export create destroy push pop peek empty

    # Set up state
    variable stack
    variable id 0
}

# Create a new stack
proc ::tutstack::create {} {
    variable stack
    variable id

    set token "stack[incr id]"
    set stack($token) [list]
    return $token
}

# Destroy a stack
proc ::tutstack::destroy {token} {
    variable stack

    unset stack($token)
}

# Push an element onto a stack
proc ::tutstack::push {token elem} {
    variable stack

    lappend stack($token) $elem
}

# Check if stack is empty
proc ::tutstack::empty {token} {
    variable stack

    set num [llength $stack($token)]
    return [expr {$num == 0}]
}

# See what is on top of the stack without removing it
proc ::tutstack::peek {token} {
    variable stack

    if {[empty $token]} {
	error "stack empty"
    }

    return [lindex $stack($token) end]
}

# Remove an element from the top of the stack
proc ::tutstack::pop {token} {
    variable stack

    set ret [peek $token]
    set stack($token) [lrange $stack($token) 0 end-1]
    return $ret
}

There are no package commands in this file, everything is setup in the filename. There is also no need to generate a pkgIndex.tcl. There is however one detail: Tcl looks for modules using a different list of paths, which we can list out using:

$ echo 'puts [::tcl::tm::list]' | tclsh

We can add to these paths using environment variables. Since modules cannot be scripted to check what version of tclsh is calling them (unlike 8.5-style packages with their pkgIndex.tcl file), so there are different module paths for different versions of Tcl. So for Tcl 8.6 we would add paths to $TCL8_6_TM_PATH and for 8.5 $TCL8_5_TM_PATH. Module paths can also be set in a file using the ::tcl::tm::path commands. The Tcl manual has more information on the module system and tm commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment