Exit Code Bitshift
This page explains how Perl packs process results into$? and how to extract the actual exit code using a right bit shift.
You run a command with system(). 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 sits in the upper 8 bits, so you shift right by 8 to pull it out. This catches everyone exactly once, and the learning curve is usually a matter of minutes rather than hours.system("false"); say $?; # 256 say $? >> 8; # 1
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, and the lower 8 bits stay all zeros because the process exited cleanly without a signal or 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, which drops 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, and it reads as idiomatic to anyone maintaining the code. Without the shift, you are comparing against packed wait status values, which turns exit code 1 into 256, exit code 2 into 512, and exit code 127 into 32512, none of which 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, and the table below shows the common signals you might see:
When a process dies by signal, the exit code bits in the upper 8 positions are meaningless, so you check for signals first and then check the exit code.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, and the system sets it when core dumps are enabled and the signal caused one. In practice, you rarely need to check this, but the bit tells you whether the core file exists without guessing when you are debugging a segfault in a C extension or an XS module.
Part 5: THE COMPLETE STATUS CHECK
Here is the full pattern, covering every case:Three cases handle the full range of outcomes, and 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, such as a missing file or a permission denied error.
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, so you apply the same packed wait status and the 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, and 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, since the bitshift version is more common in Perl code and 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, because if something got SIGKILLed, retrying is pointless, but a non-zero exit code might be transient, 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, which 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, which works most of the time, but when it fails, the error is mysterious because -1 >> 8 gives you a bunch of 1-bits due to 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, and then check exit code, since 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}, which 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, sosystem("some_command"); say "Perl \$?: $?"; say "Native: ${^CHILD_ERROR_NATIVE}";
${^CHILD_ERROR_NATIVE} with the POSIX macros is the safest approach for cross-platform code.
For Unix-only code, $? >> 8 is all you need, and it has been the right answer since Perl 1, so it will be the right answer in Perl 50, since 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." // \ \ (| | ) /'\_ _/`\ \___)=(___/
$?, and 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, but Perl gives you the raw integer and trusts you to know what to do with it, and now you do.
perl.gg