TIP #352: Tcl Style Guide


TIP:352
Title:Tcl Style Guide
Version:$Revision: 1.4 $
Authors: Ray Johnson <rjohnson at eng dot sun dot com>
Donal K. Fellows <dkf at defiant dot rcs dot manchester dot ac dot uk>
Mark Janssen <mpc dot janssen at gmail dot com>
State:Draft
Type:Informative
Vote:Pending
Created:Tuesday, 14 July 2009

Abstract

This document describes a set of conventions that it is suggested people use when writing Tcl code. It is substantially based on the Tcl/Tk Engineering Manual TIP #247.

NOTE

A transcription of the original version (dated August 22, 1997) of this file into PDF is available online at http://www.tcl.tk/doc/styleGuide.pdf - Donal K. Fellows.

Introduction

This is a manual for people who are developing Tcl code for Wish or any other Tcl application. It describes a set of conventions for writing code and the associated test scripts. There are three reasons for the conventions. First, the conventions ensure that certain important things get done; for example, every procedure must have documentation that describes each of its arguments and its result, and there must exist test scripts that exercise every line of code. Second, the conventions guarantee that all of the Tcl and Tk code has a uniform style. This makes it easier for us to use, read, and maintain each other's code. Third, the conventions help to avoid some common mistakes by prohibiting error-prone constructs such as building lists by hand instead of using the list building procedures.

This document is based heavily on the Tcl/Tk Engineering Manual written by John Ousterhout. John's engineering manual specified the style of the C code used in the implementation of Tcl/Tk and many of its extensions. The manual is very valuable to the development of Tcl/Tk and is an important reason why Tcl is a relatively easy system to maintain.

Deciding any style standard involves making trade-offs that are usually subjective. This standard was created in an iterative process involving the Tcl/Tk group at Sun Laboratories. I don't claim that these conventions are the best possible ones, but the exact conventions don't really make that much difference. The most important thing is that we all do things the same way.

Please write your code so that it conforms to the conventions from the very start. For example, don't write comment-free code on the assumption that you'll go back and put the comments in later once the code is working. This simply won't happen. Regardless of how good your intentions are, when it comes time to go back and put in the comments you'll find that you have a dozen more important things to do; as the body of uncommented code builds up, it will be harder and harder to work up the energy to go back and fix it all. One of the fundamental rules of software is that its structure only gets worse over time; if you don't build it right to begin with, it will never get that way later.

The rest of this document consists of 8 major parts. We start with Section 2 which discusses executable files. Section 3 discusses the overall structure of packages and namespaces. Section 4 describes the structure of a Tcl code file and how to write procedure headers. Section 5 desribes the Tcl naming conventions. Section 6 presents low-level coding conventions, such as how to indent and where to put curly braces. Section 7 contains a collection of rules and suggestions for writing comments. Section 8 describes how to write and maintain test suites. Section 9 contains a few miscellaneous topics, such as keeping a change log.

Executable files

An executable is a file, collection of files, or some other collection of Tcl code and necessary runtime environment. Often referred to as applications, an executable is simply what you run to start your program. The format and exact make up of an executable is platform-specific. At some point, however, a Tcl start-up script will be evaluated. It is the start-up script that will bootstrap any Tcl based application.

The role of the start-up script is to load any needed packages, set up any non-package specific state, and finally start the Tcl application by calling routines inside a Tcl package. If the start-up script is more than a few lines it should probably be a package itself.

There are several ways to create executable scripts. Each major platform usually has a unique way of creating an executable application. Here is a brief description of how these applications should be created on each platform:

  1. The most common method for creating executable applications on UNIX platforms is the infamous #! mechanism built into most shells. Unfortunately, the most common approach of just giving a path to wish is not recommended. Don't do:

        #! /usr/local/tclsh8.0 -f "$0" "$@"
    

    This method will not work if the file tclsh is another script that, for example, locates and starts the most recent version of Tcl. It also requires tclsh to be in a particular place, which makes the script less portable. Instead, the following method should be used which calls /bin/sh which will in turn exec the wish application.

        #!/bin/sh
        # the next line restarts using wish \
        exec wish8.0 "$0" "$@"
    

    This example will actually locate the wish application in the user's path which can be very useful for developers. The backslash is recognized as part of a comment to sh, but in Tcl the backslash continues the comment into the next line which keeps the exec command from executing again. However, more stable sites would probably want to include the full path instead of just wish. Note that the version number of the tclsh or wish interpreter is usually added to the end of the program name. This allows you use a specific version of Tcl. In addition, many sites include a link of wish to the latest version currently installed. This is useful if you know that your code will work on any version of Tcl.

  2. On the Windows platform you only need to end a file with the .tcl extension and the file will be run when the user double clicks on the file. This is, of course, assuming you have installed Tcl/Tk.

    Alternatively, you may create a .bat file which explicitly executes tclsh or wish with an absolute path to your start-up script. Please check the Windows documentation for more details about .bat files.

  3. The Macintosh platform doesn't really have a notion of an executable Tcl file. One of the reasons for this is that, unlike UNIX or Windows, you can only run one instance of an application at a time. So instead of callingwish with a specific script to load, we must create a copy of the wish application that is tied to our script.

    The easiest way to do this is to use the application Drag&Drop Tclets or the SpecTcl GUI builder which can do this work for you. You can also do this by hand by putting the start-up script into a TEXT resource and name it tclshrc - which ensures it gets sourced on start-up. This can be done with ResEdit (a tool provided by Apple) or other tools that manipulate resources. Additional scripts can also be placed in TEXT resource to make the application completely contained.

Packages and namespaces

Tcl applications consist of collections of packages. Each package provides code to implement a related set of features. For example, Tcl itself is a package, as is Tk; these packages happen to be implemented in both C and Tcl. Other packages are implemented completely in Tcl such as the http package included in the Tcl distribution. Packages are the units in which code is developed and distributed: a single package is typically developed by a single person or group and distributed as a unit. It is possible to combine many independently-developed packages into a single application; packages should be designed with this in mind. The notion of namespaces were created to help make this easier. Namespaces help to hide private aspects of packages and avoid name collisions. A package will generally export one public namespace which will include all state and routines that are associated with the package. A package should not contain any global variables or global procedures. Side effects when loading a package should be avoided. This document will focus on packages written entirely in Tcl. For a discussion of packages built in C or C and Tcl see the Tcl/Tk Engineering Manual.

Package names

Each package should have a unique name. The name of the package is used to identify the package. It is also used as the name of the namespace that the package exports. It is best to have a simple one word name in all lower-case like http. Multi-word names are ok as well. Additional words should just be concatenated with the first word but start with a capital letter like specMenu.

Coming up with a unique name for your package requires a collaborative component. For internal projects this is an easy task and can usually be decided among the management or principal engineers in your organization. For packages you wish to publish, however, you should make an effort to make sure that an existing package isn't already using the same name you are. This can often be done by checking the comp.lang.tcl newsgroup or the standard Tcl ftp sites. It is also suggested (but not required) that you register your name on the NIST Identifier Collaboration Service (NICS). It is located at: http://pitch.nist.gov/nics

Version numbers

Each package has a two-part version number such as 7.4. The first number (7) is called the major version number and the second (4) is called the minor version number. The version number changes with each public release of the package. If a new release contains only bug fixes, new features, and other upwardly compatible changes, so that code and scripts that worked with the old version will also work with the new version, then the minor version number increments and the major version number stays the same (e.g., from 7.4 to 7.5). If the new release contains substantial incompatibilities, so that existing code and scripts will have to be modified to run with the new version, then the major version number increments and the minor version number resets to zero (e.g., from 7.4 to 8.0).

Package namespaces

As of version 8.0, Tcl supports namespaces to hide the internal structure of a package. This helps avoid name collisions and provides a simpler way to manage packages. All packages written for Tcl 8.0 or newer should use namespaces. The name of the name space should be the same as the package name.

Structure

There are a couple of ways to deploy a package of Tcl commands.

How to organize a code file

Each source code file should either contain an entire application or a set of related procedures that make up a package or a another type of identifiable module, such as the implementation of the menus for your application, or a set of procedures to implement HTTP access. Before writing any code you should think carefully about what functions are to be provided and divide them into files in a logical way. The most manageable size for files is usually in the range of 500-2000 lines. If a file gets much larger than this, it will be hard to remember everything that the file does. If a file is much shorter than this, then you may end up with too many files in a directory, which is also hard to manage.

The file header

The first part of a code file is referred to as the header. It contains overall information that is relevant throughout the file. It consists of everything but the definitions of the file's procedures. The header typically has four parts, as shown below:

           /   # specMenu.tcl --
           |   #
Abstract   |   #       This file implements the Tcl code for creating and
           |   #       managing the menus in the SpecTcl application.
           \   #
           /   # Copyright (c) 1994-1997 Sun Microsystems, Inc.
           |   #
Copyright  |   # See the file "license.terms" for information on usage and
           |   # redistribution of this file, and for a DISCLAIMER OF ALL
           \   # WARRANTIES.
               #
Revision       # SCCS: %Z% %M% %I% %E% %U%
String         # RCS: $Id: 352.tip,v 1.4 2017/03/16 14:42:34 tclhttpd Exp $msg
   } {1 {divide by zero}}

test is a procedure defined in a script file named defs, which is sourced by each test file. test takes four or five arguments: a test identifier, a string describing the test, an optional argument describing the conditions under which this test should run, a test script, and the expected result of the script. test evaluates the script and checks to be sure that it produces the expected result. If not, it prints a message like the following:

   ==== expr-3.1 floating-point operators
   ==== Contents of test case:
       expr 2.3*.6
   ==== Result was:
   1.39
   ---- Result should have been:
   1.38
   ---- expr-3.1 FAILED

To run a set of tests, you start up the application and source a test file. If all goes well no messages appear; if errors are detected, a message is printed for each error.

The test identifier, such as expr-3.1, is printed when errors occur. It can be used to search a test script to locate the source for a failed test. The first part of the identifier, such as expr, should be the same as the name of the test file, except that the test file should have a .test extension, such as expr.test. The two numbers allow you to divide your tests into groups. The tests in a particular group (e.g., all the expr-3.n tests) relate to a single sub-feature, such as a single procedure. The tests should appear in the test file in the same order as their numbers.

The test name, such as floating-point operators, is printed when errors occur. It provides human-readable information about the general nature of the test.

Before writing tests I suggest that you look over some of the test files for Tcl and Tk to see how they are structured. You may also want to look at the README files in the Tcl and Tk test directories to learn about additional features that provide more verbose output or restrict the set of tests that are run.

Organizing tests

Organize your tests to match the code being tested. The best way to do this is to have one test file for each source code file, with the name of the test file derived from the name of the source file in an obvious way (e.g. http.test contains tests for the code in http.tcl). Within the test file, have one group of tests for each procedure (for example, all the http-3.n tests in http.test are for the procedure http::geturl). The order of the tests within a group should be the same as the order of the code within the procedure. This approach makes it easy to find the tests for a particular piece of code and add new tests as the code changes.

The Tcl test suite was written a long time ago and uses a different style where there is one file for each Tcl command or group of related commands, and the tests are grouped within the file by sub-command or features. In this approach the relationship between tests and particular pieces of code is much less obvious, so it is harder to maintain the tests as the code evolves. I don't recommend using this approach for new tests.

Coverage

When writing tests, you should attempt to exercise every line of source code at least once. There will be occasionally be code that you can't exercise, such as code that exits the application, but situations like this are rare. You may find it hard to exercise some pieces of code because existing Tcl commands don't provide fine enough control to generate all the possible execution paths. In situations like this, write one or more new Tcl commands just for testing purposes. It's much better to test a facility directly then to rely on some side effect for testing that may change over time. Use a similar approach in your own code, where you have an extra file with additional commands for testing.

It's not sufficient just to make sure each line of code is executed by your tests. In addition, your tests must discriminate between code that executes correctly and code that isn't correct. For example, write tests to make sure that the then and else branches of each if statement are taken under the correct conditions. For a loop, run different tests to make the loop execute zero times, one time, and two or more times. If a piece of code removes an element from a list, try cases where the element to be removed is the first element, last element, only element, and neither first element nor last. Try to find all the places where different pieces of code interact in unusual ways, and exercise the different possible interactions.

Fixing bugs

Whenever you find a bug in your code it means that the test suite wasn't complete. As part of fixing the bug, you should add new tests that detect the presence of the bug. I recommend writing the tests after you've located the bug but before you fix it. That way you can verify that the bug happens before you implement the fix and the bug doesn't happen afterwards, so you'll know you've really fixed something. Use bugs to refine your testing approach: think about what you might be able to do differently when you write tests in the future to keep bugs like this one from going undetected.

Tricky features

I also use tests as a way of illustrating the need for tricky code. If a piece of code has an unusual structure, and particularly if the code is hard to explain, I try to write additional tests that will fail if the code is implemented in the obvious manner instead of using the tricky approach. This way, if someone comes along later, doesn't understand the documentation for the code, decides the complex structure is unnecessary, and changes the code back to the simple (but incorrect) form, the test will fail and the person will be able to use the test to understand why the code needs to be the way it is. Illustrative tests are not a substitute for good documentation, but they provide a useful addition.

Test independence

Try to make tests independent of each other, so that each test can be understood in isolation. For example, one test shouldn't depend on commands executed in a previous test. This is important because the test suite allows tests to be run selectively: if the tests depend on each other, then false errors will be reported when someone runs a few of the tests without the others.

For convenience, you may execute a few statements in the test file to set up a test configuration and then run several tests based on that configuration. If you do this, put the setup code outside the calls to thetest procedure so it will always run even if the individual tests aren't run. I suggest keeping a very simple structure consisting of setup followed by a group of tests. Don't perform some setup, run a few tests, modify the setup slightly, run a few more tests, modify the setup again, and so on. If you do this, it will be hard for people to figure out what the setup is at any given point and when they add tests later they are likely to break the setup.

Miscellaneous

Porting issues

Writing portable scripts in Tcl is actually quite easy as Tcl itself is quite portable. However, issues do arise that may require writing platform specific code. To conditionalize your code in this manner you should use the tcl_platform array to determine platform specific differences. You should avoid the use of theenv variable unless you have already determined the platform you are running on via the tcl_platform array.

As Tcl/Tk has become more cross platform we have added commands that aid in making your code more portable. The most common porting mistakes result from assumptions about file names and locations. To avoid such mistakes always use the file join command and list commands so that you will handle different file separation characters or spaces in file names. In Tk, you should always use provided high level dialog boxes instead or creating your own. The font and menu commands has also be revamped to make writing cross-platform code easier.

Changes files

Each package should contain a file namedchanges that keeps a log of all significant changes made to the package. The changes file provides a way for users to find out what's new in each new release, what bugs have been fixed, and what compatibility problems might be intro- duced by the new release. The changes file should be in chronological order. Just add short blurbs to it each time you make a change. Here is a sample from the Tk changes file:

   5/19/94 (bug fix) Canvases didn't generate proper Postscript for
   stippled text. (RJ)

   5/20/94 (new feature) Added "bell" command to ring the display's
   bell. (JO)

   5/26/94 (feature removed) Removed support for "fill" justify mode
   from Tk_GetJustify and from the TK_CONFIG_JUSTIFY configuration
   option.  None of the built-in widgets ever supported this mode
   anyway. (SS)
   *** POTENTIAL INCOMPATIBILITY ***

The entries in the changes file can be relatively terse; once someone finds a change that is relevant, they can always go to the manual entries or code to find out more about it. Be sure to highlight changes that cause compatibility problems, so people can scan the changes file quickly to locate the incompatibilities. Also be sure to add your initials to the entry so that people scanning the log will know who made a particular change.

(The Tcl and Tk core additionally uses a ChangeLog file that has a much higher detail within it. This has the advantage of having more tooling support, but tends to be so verbose that the shorter summaries in the changes file are still written up by the core maintainers before each release.)

Copyright

The original version of this document is copyright (C) 1997 Sun Microsystems, Inc. Revisions to reflect current community best-practice are public domain.


Powered by Tcl[Index] [History] [Edit] [HTML Format] [Source Format] [LaTeX Format] [Text Format] [XML Format] [*roff Format (experimental)] [RTF Format (experimental)]

TIP AutoGenerator - written by Donal K. Fellows