Skip to content
VLSI Mentor

Tcl · Module 15

A Layout That Survives Being Inherited

The last thing a script needs is the thing nobody writes: a shape another engineer can pick up, run, configure, debug and change without asking you.

Intermediate22 min readtclprogram-structurecapstoneclimaintainabilitypart-i

Part I · Module 15 · Chapter 15.9 · A Layout That Survives Being Inherited

There is no new command in this chapter. Everything in it has already been taught. What is new is putting it together and being able to say why each piece is where it is.

The test that matters is not "does it work". It is: somebody else has this script now and you have moved to another project. Can they run it, change the limit, find out why it failed, and add a column without breaking anything?

1. The shape of one run

Six steps in order: parse the command line into options and inputs, resolve configuration from defaults then environment then options, validate the inputs, do the work for each input, write the report to standard output, and exit with a status that reflects what happened.How one run of a command-line program goes01PARSE THE COMMAND LINEargv becomes options plus input files02RESOLVE CONFIGURATIONdefault, then environment, then option03VALIDATE THE INPUTSreadable files, numbers that are numbers04DO THE WORKone input at a time, counting failures05WRITE THE REPORTstdout, or the file given by an option06EXIT WITH A STATUS0 worked, 1 work failed, 2 bad invocation
Figure 1 — the six stages of a single run, in order. Each stage can refuse: a bad option stops at stage 1, a missing file at stage 3. The status produced at stage 6 is the only part of this a calling program can see.

Almost every command-line program has this shape. Recognising it is useful in both directions: it tells you what to write next, and when you open somebody else's script it tells you where to look.

2. The whole program

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rt/
├── bin/
│   └── report-tool.tcl      the program: argv, config, status, output
├── lib/
│   ├── parse.tcl            reading and validating a readings file
│   └── report.tcl           turning rows into a summary
└── data/
    ├── readings.txt         sample input
    └── bad.txt              a deliberately malformed one, for testing

Input is two columns — a name and a number:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
temp-inlet   21.5
temp-outlet  34.2
pressure-1  101.3
temp-case    48.9

And the program summarises one or more such files, flagging anything above a limit.

3. The top of the program file

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env tclsh
# bin/report-tool.tcl — summarise one or more readings files.
#
#   usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
#
# exit status: 0 all files summarised, 1 a file failed, 2 usage error.

set here [file dirname [file normalize [info script]]]
source [file join $here .. lib parse.tcl]
source [file join $here .. lib report.tcl]

namespace eval ::report {
    variable verbose 0
    variable limit   100.0
    variable out     ""
}

Six lines of real content, and each one is a decision:

  • The usage line and the status convention are in a comment at the top. Someone opening this file learns how to run it and what its exit codes mean before reading any code. This is the single highest-value thing in the file and the one most often missing.
  • $here makes it runnable from anywhere — 15.1 §6.
  • The library is sourced, not duplicated — 15.7.
  • The three settings are namespace variables with defaults, which is where §5 picks up.

4. main, start to finish

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc ::report::main {argv} {
    variable verbose ; variable limit ; variable out

    if {[info exists ::env(REPORT_LIMIT)]} {
        if {[string is double -strict $::env(REPORT_LIMIT)]} {
            set limit $::env(REPORT_LIMIT)
        } else {
            puts stderr "warning: ignoring REPORT_LIMIT='$::env(REPORT_LIMIT)' (not a number)"
        }
    }

    if {[catch {parse_args $argv} inputs opts]} {
        puts stderr "error: $inputs"
        puts stderr [usage]
        return 2
    }
    if {[llength $inputs] == 0} {
        puts stderr "error: no input file given"
        puts stderr [usage]
        return 2
    }

    set channel stdout
    if {$out ne ""} {
        if {[catch {open $out w} channel]} {
            puts stderr "error: cannot write '$out': $channel"
            return 1
        }
    }

    set failures 0
    foreach path $inputs {
        if {$verbose} { puts stderr "reading $path (limit $limit)" }
        if {[catch {
            set rows    [parse_file $path]
            set summary [summarise $rows $limit]
            puts $channel [format_summary $path $summary]
        } err erropts]} {
            puts stderr "error: $err"
            incr failures
        }
    }

    if {$channel ne "stdout"} {
        close $channel
        if {$verbose} { puts stderr "wrote $out" }
    }
    return [expr {$failures > 0 ? 1 : 0}]
}

exit [::report::main $argv]

That is the figure, in order, with nothing else in it. Five properties are worth naming because each one is a decision you will make again:

  • main returns a number; it never calls exit. The only exit is the last line of the file. So main can be called from a test script and its result inspected — 15.4 §3.
  • The environment is read first, the options second, which is what makes an option beat an environment variable without any special code — 15.5 §6.
  • One bad file does not stop the run. The catch is inside the loop, so the remaining inputs are still processed and failures records what happened. A batch tool that dies on the first bad file is a tool people stop using.
  • Everything diagnostic goes to stderr, the report goes to $channel. They never mix.
  • The status is computed from what actually happened, not assumed.

5. Why "no globals" is not pedantry

The program has exactly three pieces of mutable state — verbose, limit, out — and all three are namespace variables declared in namespace eval ::report, reached with variable rather than global.

The reason is not tidiness. A global in Tcl is visible and writable by everything in the same interpreter. For a standalone script that is merely risky. In Part II your script is sourced into a vendor tool's interpreter alongside the tool's own commands and whatever else the site has loaded — and a global called limit or out or verbose is an excellent way to collide with something you have never heard of.

::report::limit cannot collide with anything that is not also called ::report::limit. That is the entire argument, and it costs one namespace eval block.

6. Running it

The default limit of 100 flags one reading:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ report-tool.tcl data/readings.txt
report for data/readings.txt
  readings : 4
  mean     : 51.48
  max      : 101.30
  over limit: pressure-1
[exit status 0]

An option changes it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ report-tool.tcl -limit 30 data/readings.txt
report for data/readings.txt
  readings : 4
  mean     : 51.48
  max      : 101.30
  over limit: temp-outlet, pressure-1, temp-case
[exit status 0]

So does the environment:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ REPORT_LIMIT=30 report-tool.tcl data/readings.txt
report for data/readings.txt
  readings : 4
  mean     : 51.48
  max      : 101.30
  over limit: temp-outlet, pressure-1, temp-case
[exit status 0]

7. Failing usefully

A batch where one file is malformed — the good one is still reported, the bad one is diagnosed by file and line, and the status reflects it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ report-tool.tcl data/readings.txt data/bad.txt
report for data/readings.txt
  readings : 4
  mean     : 51.48
  max      : 101.30
  over limit: pressure-1
error: data/bad.txt:2: 'warm' is not a number
[exit status 1]

A usage mistake is a different status, with the correct form shown:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ report-tool.tcl
error: no input file given
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ report-tool.tcl -bogus data/readings.txt
error: unknown option: -bogus
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]

Three outcomes, three statuses: 0 worked, 1 ran and the work failed, 2 the command was wrong. A wrapper can act on each differently, and a human gets a message that names the file and the line.

8. The channels really are separate

Redirect stdout away entirely and the diagnostics still arrive:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh bin/report-tool.tcl -verbose -out /tmp/summary.txt data/readings.txt > /dev/null
reading data/readings.txt (limit 100.0)
wrote /tmp/summary.txt
[exit status 0]
$ cat /tmp/summary.txt
report for data/readings.txt
  readings : 4
  mean     : 51.48
  max      : 101.30
  over limit: pressure-1

Standard output went to /dev/null, the report went to the file named by -out, and both progress messages appeared on the terminal from stderr. Nothing mixed. That is Chapter 14.9 §2 holding up under a real redirection.

9. The acceptance test

Not "does it run" — these four:

Can another engineer run it? The usage line is in a comment at the top and printed on any mistake. report-tool.tcl with no arguments teaches you the command.

Can they configure it? Three ways, with a documented precedence: the built-in default, REPORT_LIMIT, or -limit. The most specific wins, and a malformed environment value says so rather than silently taking effect.

Can they diagnose a failure? data/bad.txt:2: 'warm' is not a number names the file, the line and the value. The exit status says which kind of failure it was. -verbose adds progress on stderr without touching the report.

Can they change it safely? The logic is in lib/ and loads with no side effects, so a ten-line test script exercises parse_file and summarise directly — 15.7 §4. Adding a column means editing format_summary and nothing else.

10. A checklist worth keeping

For any Tcl program you expect someone else to run:

  • A usage line at the top of the file and printed on a mistake.
  • The exit-status convention written down next to it.
  • [file dirname [file normalize [info script]]] for anything the script loads.
  • Procedures in a library file that does nothing when sourced.
  • Inputs validated at the boundary, with messages that name the file, line and value.
  • Report on stdout, diagnostics on stderr, always.
  • An exit status computed from what happened — and never exit $count.
  • State in a namespace, not in globals.
  • One exit, on the last line.

11. What you can do now

Part I is finished. Concretely, you can:

read Tcl and predict what it does, because you know how a command is parsed and that substitution happens once (Module 2); work with strings, lists and dictionaries (Modules 5–7); write procedures with defaults and variable arguments, and control what they can see (Modules 10–11); read and write files and navigate paths (Module 12); match and extract with regular expressions (Module 13); diagnose a failure from its message and stack trace rather than by guessing (Module 14); and assemble all of it into a program with a command line, configuration, an honest exit status and a layout somebody else can pick up.

That last one is the difference between knowing Tcl and being able to use it.

12. What changes in Part II

Very little about the language, and that is the useful part.

Part II runs your Tcl inside an EDA tool rather than inside tclsh. The interpreter is the same interpreter. proc, foreach, lindex, catch, regexp, open and exit behave exactly as they do here. The program structure in this chapter is the structure those scripts use.

What changes is the command set. The tool embeds Tcl and adds its own commands — for querying a design, for setting constraints, for reporting results — in exactly the way Chapter 1.3 described a host application doing. Those commands are the subject of Part II, and they are the part you cannot practise in tclsh.

Three things from Part I become more important there, not less:

  • info commands (14.10) — you did not choose that interpreter, so ask it what it has rather than assuming.
  • Namespaces and no globals (§5) — your script shares an interpreter with the tool and with everything else the site loads.
  • Exit status (15.4) — those scripts are launched by build systems and schedulers that read nothing else.

13. Exercises

Exercise 1. Name the six stages of a typical command-line program run.

Exercise 2. Why does main return a number instead of calling exit?

Exercise 3. The environment is read before the options are parsed. What does that achieve?

Exercise 4. The catch is inside the input loop rather than around it. What would change if it were outside?

Exercise 5. Why namespace variables rather than globals, in one sentence about Part II?

Exercise 6. Someone asks you to add a median column. Which files do you touch, and how do you check it before running the program?

Exercise 7. A wrapper script needs to retry only on recoverable failures. Which exit status should it retry on, and which should it not?

Where this fits

Part of the Tcl curriculum.