מצב שגיאה#
ארבעה משתנים מדווחים על שגיאות מארבע שכבות שונות. הם אינם ניתנים להחלפה והם מאוכלסים על ידי מנגנונים שונים לחלוטין; שימוש שגוי באחד במקום באחר הוא הסיבה הנפוצה ביותר לבאגים מסוג ”מדוע טיפול השגיאות שלי לא עובד“ בקוד Perl.
משתנה | מקור | משך חיים |
|---|---|---|
| ה־ | נקבע בכישלון syscall; חסר משמעות אחרת |
| החריגה שנתפסה על ידי ה־ | נקבע ביציאת |
| סטטוס היציאה של תהליך הילד האחרון | נקבע על ידי כל |
| מידע שגיאה מורחב־מערכת הפעלה | זהה ל־ |
הדבר הטוב ביותר שתוכלו לעשות עבור טיפול השגיאות שלכם הוא להפנים איזה משתנה עונה על איזו שאלה:
”האם ה־open/read/connect/syscall שלי נכשל?“ → בדקו תחילה את ערך ההחזרה, ואז את
$!.”האם בלוק ה־
evalשלי תפס חריגה?“ → בדקו את$@.”מה היה קוד היציאה של התוכנית שזה עתה הרצתי?“ → פענחו את
$?.”האם אני על Windows או VMS וזקוק לטקסט שגיאה ספציפי למערכת ההפעלה?“ → הביטו גם ב־
$^E.
$! - שגיאת המערכת#
$! הוא הממשק של Perl ל־errno של ספריית C. יש לו טבע כפול: קראו אותו בהקשר מספרי כדי לקבל את ה־errno השלם, בהקשר מחרוזת כדי לקבל את מחרוזת השגיאה המתאימה:
open(my $fh, '<', '/no/such/file') or do {
print "errno = ", $! + 0, "\n"; # numeric: 2
print "errstr = $!\n"; # string: "No such file or directory"
};
חשוב מכל, $! בעל משמעות רק מיד לאחר כישלון. ה־errno של C אינו מתנקה בהצלחה, ולכן קריאה מאוחרת יותר עשויה לראות ערך מיושן מכל קריאת מערכת קודמת. הצורה האמינה:
if (open my $fh, '<', $path) {
# success - $! is meaningless here
process($fh);
} else {
# ONLY here is $! meaningful
die "open $path: $!";
}
כתיבת הכישלון באותה שורה כמו הפעולה שגרמה לו היא הכתיב הבטוח ביותר - היא אינה משאירה הזדמנות לפעולת Perl מתערבת לדרוס את errno.
%! - בדיקות errno סמליות#
%! הוא האש שמפתחותיו הם שמות סמלי errno כמו ENOENT, EACCES, EAGAIN. מפתח נבדק כאמת בדיוק כאשר $! שווה כרגע לאותו errno:
unless (open my $fh, '<', $path) {
if ($!{ENOENT}) {
return []; # missing file → empty list
}
if ($!{EACCES}) {
die "permission denied: $path";
}
die "open $path: $!"; # anything else: re-raise
}
זה נייד - הערכים המספריים של סמלי errno שונים בין מערכות הפעלה, אך מפתחות %! הם תמיד שמות הסמלים. מאחורי הקלעים, %! מסופק על ידי המודול Errno, שנטען לפי דרישה בפעם הראשונה שאתם משתמשים בהאש.
השמה ל־$!#
כתיבה ל־$! קובעת את errno בספריית C - שימושי לזיוף הודעת שגיאה שקוד במורד הזרם יראה:
$! = 13; # EACCES
print "$!\n"; # "Permission denied"
# Slightly more idiomatic - set errno by symbol:
use Errno qw(:POSIX);
$! = EACCES;
die "synthetic permission failure: $!";
$@ - משתנה החריגה#
נקבע על ידי eval כאשר הבלוק (או המחרוזת) שהוא הריץ זרק חריגה. הוא מכיל או את הערך שהועבר ל־die (שלרוב הוא מחרוזת, אך יכול להיות כל הפניה) או את הודעת שגיאת הפענוח/זמן הריצה מהמפרש.
eval {
die "no port configured\n" unless defined $port;
open(my $sock, '<', "/dev/tcp/$host/$port") or die "connect: $!";
1;
};
if ($@) {
warn "could not open $host:$port - $@";
return;
}
הערכה מוצלחת קובעת את $@ למחרוזת הריקה ''. בדיקת if ($@) היא לכן אמינה: מחרוזת ריקה היא שקר, כל ערך שאינו ריק הוא אמת.
באג הדריסה הקלאסי של $@#
יש כאן סכנה. כל דבר הרץ בין יציאת ה־eval לבדיקת ה־if ($@) שלכם יכול לדרוס את $@. העבריין הנפוץ ביותר הוא מתודת ה־DESTROY של עצם - כאשר עצמים יוצאים מהתחום בסגירת בלוק ה־eval, ה־DESTROY שלהם רץ, ואם DESTROY עצמו מבצע eval כלשהו, החריגה שלכם נעלמה:
eval {
my $obj = MyClass->new;
$obj->might_die;
1;
};
# If MyClass::DESTROY does an eval, $@ is now empty.
warn "got: $@"; # may print nothing!
התיקון המומלץ היסטורית הוא ללכוד את $@ מיד, או להשתמש במודול Try::Tiny, המטפל בכך ובכמה מלכודות קשורות:
my $err;
{
local $@; # save and isolate
eval {
risky_op();
1;
} or $err = $@ || 'unknown error';
}
if ($err) {
warn "got: $err";
}
תכונת ה־try/catch המובנית של Perl 5.34+ היא הכתיב המודרני הנקי יותר - היא אינה משתמשת ב־$@ כלל ואינה מושפעת מבעיית ההורס:
use feature 'try';
try {
risky_op();
} catch ($err) {
warn "got: $err";
}
PetaPerl תומך ב־try/catch באופן מובנה. קוד חדש צריך להעדיף אותו.
eval ו־$!#
eval אינו משמר את $!. אם בלוק ה־eval שלכם ביצע syscall שנכשל ואז ביצע die, $! הוא לכל מה שהפעולה האחרונה קבעה אותו - שבתוך הורס או מטפל local $SIG{__DIE__} יכול להיות כל דבר. אם הודעת השגיאה שלכם זקוקה ל־$!, לכדו אותו בתוך ה־eval:
eval {
open(my $fh, '<', $path) or die "open $path: $!\n";
...
};
ה־\n בסוף הוא מקובל - הוא מדכא את הסיומת ”at line N“ ש־Perl מצרף להודעות die, שזה נכון עבור שגיאות הנראות למשתמש.
$? - שגיאת הילד#
לאחר system, waitpid, wait, פקודת backtick, או סגירת pipe, $? מכיל את סטטוס ה־wait() בן 16 הביטים של תהליך הילד. הוא אינו פשוט קוד היציאה:
system($cmd, @args);
my $status = $?;
my $exit_code = $status >> 8; # exit value passed to exit() in the child
my $signal = $status & 0x7F; # signal that killed the child, or 0
my $coredump = $status & 0x80; # set if the child dumped core
if ($status == -1) {
die "system: $!"; # could not even fork/exec
} elsif ($signal) {
die "$cmd died with signal $signal";
} elsif ($exit_code) {
die "$cmd failed (exit $exit_code)";
}
ההיסט >> 8 הוא מלכודת Perl המצוטטת ביותר שקיימת. רק הבית העליון הוא קוד היציאה; הבית התחתון מחזיק את דגלי האות־וה־coredump. שכחת ההיסט מפיקה ערכים מוכפלים ב־256.
Perl מודרני מספק את המאקרו WEXITSTATUS, WIFSIGNALED, WTERMSIG, WIFEXITED של המודול POSIX, שהם המאחזרים בעלי השם הניידים:
use POSIX ':sys_wait_h';
if (WIFEXITED($?)) { my $code = WEXITSTATUS($?); ... }
if (WIFSIGNALED($?)) { my $sig = WTERMSIG($?); ... }
$? אינו משומר על פני פעולות אחרות. אם אתם זקוקים לערך, לכדו אותו למשתנה מקומי בשורה הבאה - אחרת ה־backtick או ה־system הבא ידרסו אותו.
$? בבלוקי END#
בתוך בלוק END, $? מכיל את קוד היציאה המיועד. השמה לו עוקפת את סטטוס היציאה של הסקריפט:
END {
$? = 0 if $? == 255 && $shutdown_was_clean;
}
$^E - שגיאת מערכת ההפעלה המורחבת#
ב־Unix, $^E זהה בדיוק ל־$!. ב־Windows, הוא מכיל את הערך של GetLastError(), שלעיתים אינפורמטיבי יותר מ־errno (שאליו Win32 ממפה תת־קבוצה גסה). ב־VMS, הוא קוד הסטטוס המקורי של VMS.
PetaPerl הוא ל־Linux בלבד. $^E ו־$! ניתנים להחלפה כאן; המשתנה קיים עבור קוד נייד שעשוי לרוץ גם על perls שאינם Unix.
לשלב אותם יחד - דוגמה מעובדת#
sub copy_file {
my ($src, $dst) = @_;
open my $in, '<', $src or die "open $src for reading: $!";
open my $out, '>', $dst or die "open $dst for writing: $!";
eval {
local $/ = \65536; # block reads, scoped to this eval
while (defined(my $chunk = <$in>)) {
print {$out} $chunk or die "write $dst: $!";
}
close $out or die "close $dst: $!";
1;
} or do {
my $err = $@ || 'unknown error';
unlink $dst; # clean up the partial file
close $in; # may set $! itself
die "copy_file($src → $dst): $err";
};
close $in or warn "close $src: $!";
return 1;
}
שלושה משתני שגיאה בחמש פסקאות. $! מדווח על כל כישלון syscall בשורה שהפעילה אותו. $@ אוסף כל חריגה שחצתה את גבול ה־eval. $? אינו בדוגמה זו משום שאין תהליך ילד - אך אם הפונקציה הייתה מפעילה את cp דרך shell, הייתם מפענחים את $? באותה צורה כמו בעמוד system.
ראו גם#
die- מאכלס את$@. הדרך הרגילה להעלות חריגה מהקוד שלכם.eval- תופס חריגות; קובע את$@.try/catch- החלופה המודרנית ל־eval/$@המתחמקת מסכנת הדריסה.Errno- מקור מפתחות%!; מספק קבועי errno בעלי שם.Try::Tiny- עוטףevalחסון המשמר את$@כראוי.%SIG-$SIG{__DIE__}ו־$SIG{__WARN__}הם ההוּוים שמופעלים כאשרdieו־warnרצים; הם יכולים לכתוב מחדש את$@.אופרטורים לוגיים - הניב
or dieשמניע כמעט את כל הדיווח של$!.