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
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
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 testingInput is two columns — a name and a number:
temp-inlet 21.5
temp-outlet 34.2
pressure-1 101.3
temp-case 48.9And the program summarises one or more such files, flagging anything above a limit.
3. The top of the program file
#!/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.
$heremakes 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
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:
mainreturns a number; it never callsexit. The onlyexitis the last line of the file. Somaincan 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
catchis inside the loop, so the remaining inputs are still processed andfailuresrecords 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:
$ 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:
$ 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:
$ 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:
$ 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:
$ report-tool.tcl
error: no input file given
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]$ 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:
$ 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-1Standard 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.
