Tcl · Module 15
exit, and the Status the Caller Sees
Whatever runs your script — a shell, a Makefile, a scheduler — decides what to do next from one number. Everything you printed is invisible to that decision.
Foundation16 min readtclexitexit-statusscriptscommand-lineerror-handling
Part I · Module 15 · Chapter 15.4 · exit, and the Status the Caller Sees
When your script finishes, the operating system hands one small number back to whatever started it. That number is the exit status, and it is the only thing a caller can act on automatically.
A beautifully formatted report that exits 0 after failing to process anything will be treated as a success by every tool in the chain. The messages you printed are for humans. The status is for programs.
1. What Tcl exits with when you say nothing
Three scripts and their statuses. First, one that decides deliberately:
# check.tcl
set failures 0
foreach value {21.5 34.2 101.3} {
if {$value > 100} { incr failures }
}
puts "checked 3 values, $failures over limit"
if {$failures > 0} { exit 1 }
exit 0$ tclsh check.tcl
checked 3 values, 1 over limit
$ echo $?
1Second, a script with no exit at all:
$ tclsh noexit.tcl
no explicit exit at all
$ echo $?
0Third, a script that raises and does not catch:
$ tclsh boom.tcl
about to fail
something broke
while executing
"error "something broke""
(file "boom.tcl" line 2)
$ echo $?
1So Tcl's defaults are sensible:
| What happened | Status |
|---|---|
ran to the end, no exit | 0 |
| uncaught error | 1 |
exit N | N |
2. $? is the shell's, not Tcl's
echo $? above is shell syntax for "the status of the last command". Other shells and languages have their own spelling — PowerShell uses $LASTEXITCODE, Python's subprocess has .returncode, and Tcl itself reads it out of an error code, as Chapter 15.6 shows.
The status is a property of the process and is the same everywhere. Only the way you ask for it changes.
3. return ends a procedure; exit ends the program
These are not alternatives. They operate on different things.
# ret.tcl
proc work {} {
puts " in work: about to return"
return 5
puts " never reached"
}
puts "result = [work]"
puts "the script carries on after return"$ tclsh ret.tcl ; echo "status $?"
in work: about to return
result = 5
the script carries on after return
status 0Now the same shape with exit:
# ext.tcl
proc work {} {
puts " in work: about to exit"
exit 3
puts " never reached"
}
work
puts "THIS LINE NEVER RUNS"$ tclsh ext.tcl ; echo "status $?"
in work: about to exit
status 3THIS LINE NEVER RUNS did not run. return 5 produced a value that the caller used; exit 3 produced a status and there was no caller left to use anything.
4. exit runs on the spot
exit ends the process immediately. Anything you were going to do afterwards does not happen — no remaining output, no cleanup, no close.
In practice Tcl flushes open channels on exit, so buffered output is not lost. But your own cleanup is: a temporary file you meant to delete two lines later stays on disk. If a program has cleanup to run, it belongs in the with_cleanup shape from Chapter 14.5, before the exit, not after it.
5. Choosing the numbers
Tcl gives you 0 and 1. Anything else is your program's convention, and you should state it where the reader will find it:
# exit status: 0 all files summarised, 1 a file failed, 2 usage error.A convention that works for most command-line tools:
| Status | Means |
|---|---|
| 0 | it worked |
| 1 | it ran correctly and the work failed |
| 2 | you invoked it wrongly |
6. Watching a caller act on it
This is why the number matters. Two shell callers, neither of which reads a word of your output:
$ tclsh check.tcl && echo "ALL OK" || echo "SOMETHING FAILED"
checked 3 values, 1 over limit
SOMETHING FAILED
$ if tclsh check.tcl > /dev/null; then echo run-next-step; else echo stop-here; fi
stop-hereIn the second case the output is thrown away entirely and the decision is still correct. That is the whole point: your status is the interface, and a build system, a CI job, a cron wrapper or a scheduler is reading it whether or not anybody reads your report.
7. The wrap that turns failure into success
A status is one byte. Tcl will accept any integer and the operating system keeps only the low eight bits:
exit 0 -> shell sees 0
exit 1 -> shell sees 1
exit 2 -> shell sees 2
exit 127 -> shell sees 127
exit 255 -> shell sees 255
exit 256 -> shell sees 0
exit 300 -> shell sees 44
exit -1 -> shell sees 255A non-integer is at least loud about it:
expected integer but got "abc"That raises, which — being an uncaught error — exits 1.
8. In the capstone
The report tool states its convention in a comment at the top of the file and honours it everywhere:
# exit status: 0 all files summarised, 1 a file failed, 2 usage error.main returns a number rather than exiting, and the single exit is the last line of the script:
proc ::report::main {argv} {
...
if {[llength $inputs] == 0} {
puts stderr "error: no input file given"
puts stderr [usage]
return 2
}
...
return [expr {$failures > 0 ? 1 : 0}]
}
exit [::report::main $argv]That arrangement is deliberate: main is an ordinary procedure you could call from a test and inspect the return value of, and only the final line turns that value into a process status.
Every path, exercised:
$ report-tool.tcl rt/data/readings.txt
report for rt/data/readings.txt
readings : 4
mean : 51.48
max : 101.30
over limit: pressure-1
[exit status 0]
$ report-tool.tcl
error: no input file given
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]
$ report-tool.tcl missing.txt
error: couldn't open "missing.txt": no such file or directory
[exit status 1]
$ report-tool.tcl rt/data/bad.txt
error: rt/data/bad.txt:2: 'warm' is not a number
[exit status 1]Three different outcomes, three different numbers, and in each case the message explaining it went to stderr while the report went to stdout.
9. Exercises
Exercise 1. A script runs to the end without calling exit. What status does it return?
Exercise 2. What status does an uncaught error produce?
Exercise 3. A procedure does return 2. Does the script exit with status 2?
Exercise 4. Why should a library procedure never call exit?
Exercise 5. exit $failures with 256 failures. What does the caller see, and what should the code be?
Exercise 6. Why distinguish "usage error" from "processing failure" with different numbers?
Exercise 7. Your script prints ERROR: input not found and exits 0. A wrapper runs it in a pipeline. What happens?
Where this fits
Part of the Tcl curriculum.
