Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh check.tcl
checked 3 values, 1 over limit
$ echo $?
1

Second, a script with no exit at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh noexit.tcl
no explicit exit at all
$ echo $?
0

Third, a script that raises and does not catch:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh boom.tcl
about to fail
something broke
    while executing
"error "something broke""
    (file "boom.tcl" line 2)
$ echo $?
1

So Tcl's defaults are sensible:

What happenedStatus
ran to the end, no exit0
uncaught error1
exit NN

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh ret.tcl ; echo "status $?"
  in work: about to return
result = 5
the script carries on after return
status 0

Now the same shape with exit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# ext.tcl
proc work {} {
    puts "  in work: about to exit"
    exit 3
    puts "  never reached"
}
work
puts "THIS LINE NEVER RUNS"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh ext.tcl ; echo "status $?"
  in work: about to exit
status 3

THIS 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# exit status: 0 all files summarised, 1 a file failed, 2 usage error.

A convention that works for most command-line tools:

StatusMeans
0it worked
1it ran correctly and the work failed
2you 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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-here

In 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 255

A non-integer is at least loud about it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.