<!-- category: hidden-gems -->
Exit Code Bitshift
You run a command withsystem(). It fails. You check $? and get
the number 256.
256 is not an exit code. The process exited with 1.
Perl gives you the wait status, not the exit code. The actual exit code is packed into the upper 8 bits. You have to shift right by 8 to get it out.system("false"); say $?; # 256 say $? >> 8; # 1
This catches everyone exactly once. Some people learn about it in five minutes. Others spend five hours staring at "256" wondering why their process returned a value that no program in history has ever set as an exit code.
Part 1: WHAT $? ACTUALLY CONTAINS
When a child process dies, Unix packs three things into a single 16-bit integer:Laid out as bits:BITS WHAT ------ ---------------------------------- 15-8 Exit code (0-255) 7 Core dump flag (0 or 1) 6-0 Signal number (0-127)
So when a process exits normally with code 1, you get:+--------+---+-------+ | exit | C | signal| | code | D | number| +--------+---+-------+ 15 8 7 6 0
That is where 256 comes from. The exit code 1 is sitting in bits 15 through 8. The lower 8 bits are all zeros because the process exited cleanly (no signal, no core dump).exit code = 1 (binary: 00000001) core dump = 0 signal = 0 Packed: 00000001_0_0000000 = 256 decimal
Part 2: THE >> 8 PATTERN
The bit shift$? >> 8 slides everything 8 positions to the right,
dropping the signal and core dump bits entirely:
This is the standard pattern. You will seesystem("grep", "-q", "needle", "haystack.txt"); my $exit_code = $? >> 8; if ($exit_code == 0) { say "Found it"; } elsif ($exit_code == 1) { say "Not found"; } else { say "Error occurred (exit code: $exit_code)"; }
$? >> 8 in virtually
every Perl program that manages child processes. It is idiomatic.
It is expected. It is non-negotiable.
Without the shift, you are comparing against packed wait status values. Exit code 1 becomes 256. Exit code 2 becomes 512. Exit code 127 becomes 32512. None of these numbers mean anything to a human reading your code.
Part 3: CHECKING FOR SIGNALS
If the lower 7 bits are non-zero, the process was killed by a signal rather than exiting on its own:Thesystem("long_running_command"); my $signal = $? & 127; # mask off the lower 7 bits if ($signal) { say "Process killed by signal $signal"; } else { my $exit_code = $? >> 8; say "Process exited with code $exit_code"; }
& 127 (which is & 0x7F) masks out just the signal bits.
Common signals you might see:
When a process dies by signal, the exit code bits (upper 8) are meaningless. Check for signals first, then check exit code. Order matters.SIGNAL NUMBER MEANING ------ ------ ------------------------- SIGHUP 1 Hangup (terminal closed) SIGINT 2 Interrupt (Ctrl+C) SIGKILL 9 Kill (cannot be caught) SIGSEGV 11 Segmentation fault SIGTERM 15 Termination request SIGPIPE 13 Broken pipe
Part 4: THE CORE DUMP BIT
Bit 7 tells you whether a core dump was produced:Thesystem("./buggy_program"); my $signal = $? & 127; my $core_dump = $? & 128; # bit 7 if ($signal) { say "Killed by signal $signal"; say "Core dump generated" if $core_dump; }
& 128 (which is & 0x80) checks just bit 7. If the system
has core dumps enabled and the signal caused one, this bit is set.
In practice, you rarely need to check this. But when you are debugging a segfault in a C extension or an XS module and need to know whether the core file exists, this bit tells you without guessing.
Part 5: THE COMPLETE STATUS CHECK
Here is the full pattern, covering every case:Three cases. The specialsub run_and_check { my (@cmd) = @_; system(@cmd); if ($? == -1) { # system() itself failed (couldn't exec) die "Failed to execute '@cmd': $!\n"; } elsif ($? & 127) { # killed by signal my $signal = $? & 127; my $core = $? & 128 ? ' (core dumped)' : ''; die "Command '@cmd' died with signal $signal$core\n"; } else { # normal exit my $exit_code = $? >> 8; if ($exit_code != 0) { warn "Command '@cmd' exited with code $exit_code\n"; } return $exit_code; } }
-1 check handles the situation where
system() could not even start the command (file not found,
permission denied, etc.). In that case $! has the error reason.
Part 6: BACKTICKS AND QX
Backticks andqx set $? the same way system() does:
The difference is that backticks capture STDOUT whilemy $output = `ls -la /nonexistent 2>&1`; my $exit_code = $? >> 8; if ($exit_code != 0) { say "ls failed with code $exit_code"; say "Output was: $output"; }
system()
lets it pass through. But $? works identically in both cases.
Same packed wait status. Same >> 8 pattern.
Same thing with qx:
And withmy $date = qx~date +%Y-%m-%d~; chomp $date; die "date command failed\n" if $? >> 8;
open in pipe mode:
Note thatopen my $pipe, '-|', 'find', '/tmp', '-name', '*.log' or die "Can't open pipe: $!\n"; while (<$pipe>) { chomp; say "Found: $_"; } close $pipe; # $? is set after closing the pipe if ($? >> 8) { warn "find exited with code " . ($? >> 8) . "\n"; }
$? is set when you close the pipe handle, not when
you open it.
Part 7: THE POSIX MODULE
If bit-shifting feels too low-level, the POSIX module provides named macros:These are the same C macros that every Unix programmer knows. They do exactly whatuse POSIX qw(:sys_wait_h); system("some_command"); if (WIFEXITED($?)) { my $code = WEXITSTATUS($?); say "Exited normally with code $code"; } elsif (WIFSIGNALED($?)) { my $sig = WTERMSIG($?); say "Killed by signal $sig"; if (WCOREDUMP($?)) { say "Core dump produced"; } }
>> 8, & 127, and & 128 do, but with
names that explain the intent.
Use whichever you prefer. The bitshift version is more common in Perl code. The POSIX version is more common in code written by people who grew up on C.MACRO EQUIVALENT MEANING ----------------- ---------------- ---------------------- WIFEXITED($?) ($? & 127) == 0 Did it exit normally? WEXITSTATUS($?) $? >> 8 What was the exit code? WIFSIGNALED($?) ($? & 127) != 0 Killed by signal? WTERMSIG($?) $? & 127 Which signal? WCOREDUMP($?) $? & 128 Core dump?
Part 8: PRACTICAL PROCESS MANAGEMENT
A wrapper that runs commands, retries on failure, and gives clean diagnostics:Notice how signal deaths are never retried. If something got SIGKILLed, retrying is pointless. But a non-zero exit code might be transient (network timeout, busy server), so those get retried.#!/usr/bin/env perl use strict; use warnings; use feature 'say'; sub run_with_retry { my (%opts) = @_; my @cmd = @{ $opts{cmd} }; my $retries = $opts{retries} // 3; my $delay = $opts{delay} // 5; for my $attempt (1 .. $retries) { say "Attempt $attempt: @cmd"; system(@cmd); if ($? == -1) { die "Cannot execute '@cmd': $!\n"; } if ($? & 127) { my $sig = $? & 127; die "Command killed by signal $sig, not retrying\n"; } my $exit_code = $? >> 8; return 0 if $exit_code == 0; # success warn "Exit code $exit_code, attempt $attempt of $retries\n"; sleep $delay if $attempt < $retries; } return $? >> 8; # return last exit code on failure } # usage my $result = run_with_retry( cmd => [qw(curl -sf http://example.com/api/health)], retries => 5, delay => 10, ); if ($result) { die "Health check failed after retries (exit code $result)\n"; } say "Service is up";
Part 9: THE $? == -1 TRAP
Whensystem() returns -1, it means Perl could not even fork
and exec the command. This is different from the command running
and failing:
Some people skip this check and go straight tosystem("nonexistent_command_xyz"); if ($? == -1) { # The command was never executed say "Error: $!"; # No such file or directory }
$? >> 8. That
works most of the time. But when it fails, the error is
mysterious. -1 >> 8 gives you a bunch of 1-bits (because Perl
uses arithmetic shift on signed integers), and you end up with
an exit code that makes no sense.
Always check for -1 first. Then check signals. Then check exit code. That order handles every case cleanly.
system(@cmd) | +------+------+ | | | $?=-1 $?&127 $?>>8 | | | exec signal exit failed killed code
Part 10: THE CHILD_ERROR_NATIVE
For the truly curious, there is also${^CHILD_ERROR_NATIVE}.
This gives you the raw wait status as the operating system
reported it, before Perl does any munging.
On Unix, these are usually identical. But on Windows, they can differ because Windows does not use the same wait status format. If you are writing cross-platform code,system("some_command"); say "Perl \$?: $?"; say "Native: ${^CHILD_ERROR_NATIVE}";
${^CHILD_ERROR_NATIVE}
with the POSIX macros is the safest approach.
For Unix-only code, $? >> 8 is all you need. It has been the
right answer since Perl 1. It will be the right answer in Perl
- Some patterns earn their permanence.
The wait status is a 40-year-old Unix convention that Perl faithfully exposes through$? = wait status | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ |15 |14 |13 |12 |11 |10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | exit code | C | signal number | +---------------------------+---+-------------------------------+ ^ | core dump bit >> 8 = exit code & 127 = signal & 128 = core dump .--. |o_o | "Shift right by 8. |:_/ | That's the whole trick." // \ \ (| | ) /'\_ _/`\ \___)=(___/
$?. It is not a Perl quirk. It is
a Unix quirk that Perl refuses to hide from you.
Other languages wrap this in objects and method calls. Perl gives you the raw integer and trusts you to know what to do with it. Now you do.
perl.gg