Perl Part I · Module 15 · Command-Line Perl and Script Execution
Exit Codes: Telling the Shell What Happened
Your script prints a clear report and ends with the word FAIL in capitals. The Makefile that called it carries on to the next step, because a Makefile cannot read. The only thing a calling program ever sees is one small number, and getting that number right is the difference between a flow that stops when something breaks and one that keeps going and ships the result. The rule sounds trivial — zero for success, non-zero for failure — and this chapter is long because two measured behaviours break it: a status of 256 arrives as a clean success, and die does not reliably exit 255 the way almost everyone believes.
Foundation14 min readPerlPart ICommand Line
Part I · Module 15 · Chapter 15.4 · Exit Codes: Telling the Shell What Happened
1. Only the Number Is Machine-Readable
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
my %program = (
"clean.pl" => 'print "all checks passed\n"; exit 0;',
"failed.pl" => 'print "FAIL: 3 violations\n"; exit 1;',
"usage.pl" => 'print STDERR "usage: ...\n"; exit 2;',
"fellthru.pl" => 'print "no explicit exit at all\n";',
"warned.pl" => 'warn "something looks odd\n"; print "FAIL printed\n";',
"died.pl" => '$! = 0; $? = 0; die "cannot continue\n";',
"cleanup.pl" => 'END { print "END block ran\n" } print "body\n"; exit 3;',
);
for my $name (sort keys %program) {
open my $w, ">", File::Spec->catfile($dir, $name) or die "cannot write: $!\n";
print $w $program{$name};
close $w;
my $out = `cd '$dir' && perl $name 2>&1`;
my $rc = $? >> 8;
$out =~ s/\s+\z//;
$out =~ s/\n/ | /g;
printf "%-12s exit %-4d %s\n", $name, $rc, $out;
}clean.pl exit 0 all checks passed
cleanup.pl exit 3 body | END block ran
died.pl exit 255 cannot continue
failed.pl exit 1 FAIL: 3 violations
fellthru.pl exit 0 no explicit exit at all
usage.pl exit 2 usage: ...
warned.pl exit 0 something looks odd | FAIL printedTwo rows deserve attention immediately.
warned.pl printed FAIL and exited 0. It even issued a warning. To a person reading the terminal it is obviously a failed run. To make, to a shell &&, to a CI job, it is a complete success — because warn does not touch the exit status. This is the single most common way a broken step gets through a flow.
fellthru.pl exited 0 too. A script that runs off the end of the file exits successfully. So "I did not say it worked" is not the same as "I said it failed"; silence means success.
The other rows are the baseline: exit N sets the status to N, die with a clean errno gives 255, and an END block still runs before the process goes — more on that in section 5.
Why the number is all there is:
# a shell flow: the second command runs only if the first succeeded
check-timing.pl --input setup.rpt && write-report.pl
# make: a non-zero status stops the build
report.txt: setup.rpt
check-timing.pl --input $< > $@Neither of those looks at your output. make stops a recipe on a non-zero status and carries on otherwise, and no amount of red text changes that.
2. Choosing the Number
Figure 15.4.1 — Picking an exit status
The conventions worth following:
0 means success, and nothing else does. There is no second success code.
1 is the general failure. If you have only two outcomes, use 0 and 1 and stop there.
2 is a usage error — bad arguments, missing options, the Chapter 15.3 case. Separating it from 1 lets a wrapper tell "you called me wrongly" from "I ran and the answer was bad".
3 and up are yours to define, and they only earn their keep if something actually acts on them. A documented scheme is useful:
# exit status of check-timing.pl
# 0 no violations
# 1 violations found
# 2 usage error
# 3 input unreadable or emptyStay below 126. The shell uses 126 for "found but not executable", 127 for "command not found", and 128 + N for "killed by signal N". Picking 127 for your own meaning makes your failure indistinguishable from a missing tool.
The distinction the figure puts in the middle is the one people skip: a run that could not reach a verdict is not the same as a run whose verdict is "fail". If the report was unreadable, exiting with the same code as "violations found" tells the flow that the design has violations when in fact nothing was checked. Give it its own code, or at minimum make sure it is not the "found violations" one.
3. die Does Not Reliably Exit 255
Almost everyone believes an uncaught die exits 255. It does — sometimes.
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
my %program = (
"a-clean.pl" => '$! = 0; $? = 0; die "a plain death\n";',
"b-after-f.pl" => '-f "/no/such/file"; die "after a failed -f\n";',
"c-after-open.pl"=> 'open(my $h, "<", "/no/such/file"); die "after a failed open\n";',
"d-eacces.pl" => 'open(my $h, "<", "/etc/sudoers"); die "after a permission error\n";',
"e-child.pl" => 'system($^X, "-e", "exit 7"); $! = 0; die "after a child exited 7\n";',
"f-cleared.pl" => 'open(my $h, "<", "/no/such/file"); $! = 0; $? = 0; die "errno cleared first\n";',
);
for my $name (sort keys %program) {
open my $w, ">", File::Spec->catfile($dir, $name) or die "cannot write: $!\n";
print $w $program{$name};
close $w;
my $err = `cd '$dir' && perl $name 2>&1`;
my $rc = $? >> 8;
$err =~ s/\s+\z//;
printf "%-16s exit %-4d %s\n", $name, $rc, $err;
}a-clean.pl exit 255 a plain death
b-after-f.pl exit 2 after a failed -f
c-after-open.pl exit 2 after a failed open
d-eacces.pl exit 13 after a permission error
e-child.pl exit 7 after a child exited 7
f-cleared.pl exit 255 errno cleared firstSix deaths, five different exit statuses. The rule behind them is documented but rarely read: die uses $! if it is non-zero, otherwise $? shifted down if that is non-zero, otherwise 255.
So the status you get is whatever error number happened to be lying around:
- A clean errno gives 255, the number everyone expects.
- After a failed
-foropenon a missing file, 2 — that isENOENTleaking out of a file test you may not even have noticed doing. - After a permission error, 13 —
EACCES. - After a child process exited 7, the status is 7, because
$!was zero so$?was used instead.
None of those numbers is about your error. They are about the last thing the operating system complained about.
4. The Status Is One Byte
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
for my $value (0, 1, 2, 42, 255, 256, 257, 300, 512, -1) {
my $name = "e$value.pl";
$name =~ s/-/neg/;
open my $w, ">", File::Spec->catfile($dir, $name) or die "cannot write: $!\n";
print $w "exit $value;\n";
close $w;
system("cd '$dir' && perl $name");
my $rc = $? >> 8;
my $note = ($rc == 0 && $value != 0) ? " <-- A FAILURE THAT READS AS SUCCESS" : "";
printf "exit %-5s -> the shell sees %3d%s\n", $value, $rc, $note;
}exit 0 -> the shell sees 0
exit 1 -> the shell sees 1
exit 2 -> the shell sees 2
exit 42 -> the shell sees 42
exit 255 -> the shell sees 255
exit 256 -> the shell sees 0 <-- A FAILURE THAT READS AS SUCCESS
exit 257 -> the shell sees 1
exit 300 -> the shell sees 44
exit 512 -> the shell sees 0 <-- A FAILURE THAT READS AS SUCCESS
exit -1 -> the shell sees 255Only the low eight bits survive. The status travels in a byte, so every value is taken modulo 256:
exit 256arrives as0. A failure reported as a complete success.exit 512arrives as0too, for the same reason.exit 300arrives as44, which is 300 − 256.exit -1arrives as255, the bit pattern read as unsigned.
The practical hazard is not someone typing exit 256. It is a computed status:
exit scalar @violations; # 256 violations -> exits 0 -> "passed"That line is easy to write and reads as sensible. With 256 violations it reports a clean run. The same trap exists in exit $error_count and exit $row_number.
Clamp anything computed:
exit(@violations ? 1 : 0); # best: a verdict, not a count
exit(@violations > 100 ? 100 : scalar @violations); # if you must pass a countThe first form is almost always right. A count belongs in the output, where it can be any size; the exit status is a verdict and should carry one.
5. END Blocks, and Exiting From Deep Inside
Two last behaviours worth knowing.
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
my %program = (
"a-end-on-exit.pl" => <<'PROG',
END { print " END: cleanup ran\n" }
print " body: working\n";
exit 4;
PROG
"b-end-on-die.pl" => <<'PROG',
END { print " END: cleanup ran\n" }
$! = 0; $? = 0;
die " body: fatal\n";
PROG
"c-end-sets-status.pl" => <<'PROG',
END { print " END: overriding the status\n"; $? = 9 }
exit 4;
PROG
"d-nested-exit.pl" => <<'PROG',
sub deep { print " deep: exiting from three levels down\n"; exit 5 }
sub mid { deep() }
sub top { mid() }
top();
print " never reached\n";
PROG
);
for my $name (sort keys %program) {
open my $w, ">", File::Spec->catfile($dir, $name) or die "cannot write: $!\n";
print $w $program{$name};
close $w;
my $out = `cd '$dir' && perl $name 2>&1`;
my $rc = $? >> 8;
print "$name:\n$out";
printf " -> exit %d\n\n", $rc;
}a-end-on-exit.pl:
body: working
END: cleanup ran
-> exit 4
b-end-on-die.pl:
body: fatal
END: cleanup ran
-> exit 255
c-end-sets-status.pl:
END: overriding the status
-> exit 9
d-nested-exit.pl:
deep: exiting from three levels down
-> exit 5An END block runs on the way out, whether the script reached exit or die. That makes it the right place to remove a temporary file or close a log — Chapter 14.5's finally, at whole-program scale. Note it does not run if the process is killed by a signal, which Chapter 14.11 measured from the other side when an unflushed log came back empty.
An END block can change the exit status by assigning to $?. The third row exits 9 although the body said exit 4. That is occasionally useful and much more often a nasty surprise, so if a status is mysteriously wrong, look for an END block — including one inside a module you did not write.
exit leaves immediately, from any depth. The fourth row exits from three calls down without unwinding to main. That makes exit easy to abuse: a subroutine that exits cannot be reused by a caller who wanted to handle the problem. The better shape for anything you might reuse is to die or return a failure and let main choose the status — Chapter 14.9's argument about who gets to decide.
6. What To Remember
- The exit status is the only thing a calling program reads.
makeand&&never see your output. 0is success; everything else is failure. There is no second success code.- Measured:
warndoes not change the status. A script that printedFAILand warned still exited 0. - Measured: falling off the end exits
0. Silence means success. - Use
1for a general failure,2for a usage error, and document anything beyond that. - Stay below 126 —
126,127and128+Nalready mean things to the shell. - "Could not reach a verdict" is not "the verdict is fail" — give it its own code, or a flow will report violations that were never checked.
- Measured:
diedoes not reliably exit 255. It uses$!if non-zero, else$? >> 8, else 255 — giving 2 after a missing file, 13 after a permission error, 7 after a child exited 7. - So
diecan produce a number that collides with your documented scheme. Use an explicitexitwherever a caller acts on the status. - The status is one byte. Measured:
exit 256arrives as0,exit 512as0,exit 300as44,exit -1as255. - Never
exit scalar @violations— 256 violations reports a clean run. Exit a verdict, not a count. ENDblocks run on bothexitanddie, so they are the place for cleanup — but not when the process is killed.- An
ENDblock can overwrite the status by assigning$?. Measured: exited 9 afterexit 4. exitleaves from any depth, which is why a reusable subroutine shoulddieor return instead.
Chapter 15.5 turns to the other channel a script gets its configuration from. Arguments are what this run was told; the environment is what the whole session already knew — where the project lives, which scratch area to use, and which tool version is on the path.
Where this fits
Part of the Perl curriculum.
