Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
what-the-shell-sees.pl
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 printed

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

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

Five steps for choosing a script's exit status: check whether the arguments were usable, whether the work completed, whether the verdict passed, then avoid the two measured traps of a status above 255 and of letting die choose the number.At the end of a run1Arguments unusable?exit 2 — a usage error2Could not finish?exit 1 — no verdict reached3Verdict is a fail?exit 1, or your own agreed code4Status under 256?256 arrives as 0 — measured5Exit on purposeexit 0 — never leave it to die
Work through these in order at the end of a run. The first three decide between the conventional statuses; the last two are the measured traps. A run that could not reach a verdict is a separate outcome from one that reached a failing verdict, and giving them the same status hides the difference from everything downstream.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# exit status of check-timing.pl
#   0  no violations
#   1  violations found
#   2  usage error
#   3  input unreadable or empty

Stay 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.

Azvya Education Pvt. Ltd.VLSI Mentor
die-is-not-255.pl
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 first

Six 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 -f or open on a missing file, 2 — that is ENOENT leaking 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

Azvya Education Pvt. Ltd.VLSI Mentor
one-byte.pl
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 255

Only the low eight bits survive. The status travels in a byte, so every value is taken modulo 256:

  • exit 256 arrives as 0. A failure reported as a complete success.
  • exit 512 arrives as 0 too, for the same reason.
  • exit 300 arrives as 44, which is 300 − 256.
  • exit -1 arrives as 255, the bit pattern read as unsigned.

The practical hazard is not someone typing exit 256. It is a computed status:

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exit(@violations ? 1 : 0);                      # best: a verdict, not a count
exit(@violations > 100 ? 100 : scalar @violations);   # if you must pass a count

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
end-and-exit.pl
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 5

An 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. make and && never see your output.
  • 0 is success; everything else is failure. There is no second success code.
  • Measured: warn does not change the status. A script that printed FAIL and warned still exited 0.
  • Measured: falling off the end exits 0. Silence means success.
  • Use 1 for a general failure, 2 for a usage error, and document anything beyond that.
  • Stay below 126 — 126, 127 and 128+N already 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: die does 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 die can produce a number that collides with your documented scheme. Use an explicit exit wherever a caller acts on the status.
  • The status is one byte. Measured: exit 256 arrives as 0, exit 512 as 0, exit 300 as 44, exit -1 as 255.
  • Never exit scalar @violations — 256 violations reports a clean run. Exit a verdict, not a count.
  • END blocks run on both exit and die, so they are the place for cleanup — but not when the process is killed.
  • An END block can overwrite the status by assigning $?. Measured: exited 9 after exit 4.
  • exit leaves from any depth, which is why a reusable subroutine should die or 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.