זרימת בקרה

eval#

הרצת קטע קוד Perl כשכל שגיאה קטלנית נלכדת במקום להרוג את התוכנית.

eval הוא מנגנון תפיסת החריגות של Perl וטוען הקוד שלו בזמן ריצה גם יחד. הוא בא בשתי צורות אמיתיות: eval בלוק (eval { ... }), שתופס חריגות שנזרקות על־ידי קוד שכבר הודר, וeval מחרוזת (eval EXPR), שמהדר ומריץ מחרוזת מקור Perl בזמן ריצה. בכל מקרה, die שאחרת היה מסיים את התוכנית הופך להשמה ל־$@ ולחזרה רגילה.

תקציר#

eval BLOCK                  # catch exceptions in compiled code
eval EXPR                   # compile and run source at runtime
eval                        # short for: eval $_

מה מוחזר#

הערך של הביטוי האחרון שהוערך בתוך התוכנית־הקטנה, או הערך שנמסר ל־return מפורש. ההערכה מתרחשת באותו הקשר כמו ה־eval הסובב - void, סקלרי, או רשימה - כך ש־wantarray בתוך eval מדווח על הקשר הקורא, לא ״הקשר eval כלשהו״.

אם משהו בפנים מעלה חריגה (die, חלוקה באפס, שגיאת תחביר ב־eval מחרוזת, כל קטלנית נלכדת אחרת), eval מחזיר undef בהקשר סקלרי ורשימה ריקה בהקשר רשימה, והחריגה נוחתת ב־$@:

my $n = eval { risky() };   # undef on failure, $@ holds the error
if ($@) { ... }

בהצלחה, $@ נקבע למחרוזת הריקה. כל eval שהושלם נוגע ב־$@ - אין אפשרות ״להשאיר אותו בשקט״. אופרטור בקרת זרימה שעוקף את היציאה הרגילה (last, goto החוצה מהבלוק) יכול להשאיר את $@ ללא שינוי, וזהו מקרה קצה ולא תכונה להסתמך עליה.

מצב גלובלי שהוא נוגע בו#

  • $@ - נכתב בכל יציאה רגילה של ה־eval: החריגה הנזרקת בכישלון, המחרוזת הריקה בהצלחה. נקרא כדי לקבוע האם ה־eval תפס משהו.

  • $^S - משקף האם Perl נמצא כעת בתוך eval (או try). undef במהלך פרסור, אמת בעת ביצוע בתוך בלוק eval/try, שקר ברמה העליונה. נקרא בעיקר על־ידי מטפלי $SIG{__DIE__} הרוצים לפעול אחרת עבור חריגות תפוסות מול לא־תפוסות.

  • $SIG{__DIE__} - נורה בכל die, כולל אלו ש־eval מקיף עומד לתפוס. יש להשתמש ב־local $SIG{__DIE__} בתוך eval כדי לדכא הוו מותקן גלובלית למשך הלכידה.

  • $SIG{__WARN__} - לא מושפע מ־eval. אזהרות הנפלטות בתוך הבלוק עדיין עוברות דרך מסלול האזהרות הרגיל; eval אינו מנתב אותן אל $@.

  • $_ - המקור של הצורה חסרת־הארגומנט (eval ללא דבר אחריו הוא בדיוק eval $_).

eval בלוק מול eval מחרוזת#

שתי הצורות נראות דומות אך עושות דברים שונים מאוד.

  • eval בלוק (eval { ... }) מפורסר ומהודר יחד עם התוכנית הסובבת. בזמן ריצה זה רק סמן האומר ״תפוס כל die שנזרק מתוך הבלוק הזה.״ מכיוון שהקוד כבר מהודר, eval בלוק זול ובטוח - המהדר כבר בדק אותו על שגיאות תחביר. זוהי הצורה להישען עליה בכל פעם שהמטרה היא טיפול בחריגות.

  • eval מחרוזת (eval EXPR) מעריך את EXPR בהקשר סקלרי, ואז מפרסר ומבצע את המחרוזת המתקבלת כאילו הייתה בלוק בתחום הלקסיקלי של הקוד הסובב. הפרסור מתרחש בכל פעם שמגיעים ל־eval. שגיאת תחביר במחרוזת הופכת ל־$@ בזמן ריצה, לא לכישלון זמן הידור של התוכנית המקיפה. זוהי הצורה לטעינת קוד הנקבע בזמן ריצה - תכונות אופציונליות, ביטויים שסופקו על־ידי המשתמש, תתי־שגרות הנבנות באופן דינמי - והיא נושאת את העלויות והסכנות שמתבקשות.

שתי הצורות רצות בתחום הלקסיקלי של הקוד הסובב: משתני my חיצוניים נראים, והשמות למשתני חבילה, הגדרות תת־שגרות, והגדרות פורמט שנעשו בתוך ה־eval נשמרים לאחר חזרתו.

החוזה של $@#

לאחר ש־eval מסתיים באופן רגיל, $@ תמיד אחד מ:

  • המחרוזת הריקה - הבלוק רץ עד הסוף ללא חריגה.

  • מחרוזת המסתיימת בתו שורה חדשה - die "message\n" או חריגת מחרוזת שהנקרא כבר סיים.

  • מחרוזת עם מיקום מצורף (... at FILE line N.\n) - die "message" ללא תו שורה חדשה מסיים, או שגיאת זמן ריצה ש־Perl עצמו העלה.

  • הפניה, בדרך כלל עצם חריגה מבורך - die שנקרא עם הפניה.

יש לבדוק את $@ מיד לאחר ה־eval ולא במקום אחר. כל קריאת מתודה, בדיקת ref, בדיקת isa, או התאמת regex הנעשית במהלך הטיפול יכולה בעצמה להפעיל eval פנימי (למשל, במהלך autoloading או פתרון overload) ולדרוס את $@ לפני שתסיימו לבחון אותו. יש להעתיק למשתנה לקסיקלי ראשון:

eval { work() };
if (my $err = $@) {         # safe snapshot
    if (ref $err && $err->isa('MyApp::Error')) { handle($err) }
    else                                         { die $err      }
}

eval מקונן ו־local $@#

ה־eval החיצוני רואה מה ש־$@ מחזיק כאשר הבלוק החיצוני מסתיים - לא מה שהחזיק באמצע הדרך. אם לקוד הפנימי יש eval משלו, ה־eval הפנימי הזה קובע את $@ (למחרוזת הריקה בהצלחה), מה שיכול לדרוס שגיאה שהקוד החיצוני רצה לשמר. ההגנה היא local $@ סביב העבודה הפנימית:

eval {
    # ... some work that might fail ...
    {
        local $@;           # shield outer $@
        eval { optional_step() };
        # ignore whatever optional_step did to $@
    }
    die "still needed\n" if $condition;
};
warn "outer caught: $@" if $@;

לפני Perl 5.14, $@ הושם לפני שמשתנים מקומיים שוחזרו, כך שקוד שרצה לסנן שגיאות נאלץ להבים את הערך דרך משתנה זמני. ב־5.14+ (ש־5.42 יורש) הסדר תוקן והמשתנה הזמני אינו נחוץ.

אבטחת eval מחרוזת#

eval מחרוזת מריץ כל מה שנמצא בביטוי. אם חלק כלשהו מהמחרוזת הגיע מקלט לא־אמין - קובץ, עמית רשת, טופס משתמש - ה־eval מוסר לתוקף את מפרש Perl המלא. כללי אצבע:

  • אין להעביר אף פעם קלט משתמש דרך eval EXPR ללא שינוי. ״זה רק מספר״ אינו ערובה - "1; system 'rm -rf /'" הוא גם רק מחרוזת.

  • אם באמת נחוץ קוד שנבחר בזמן ריצה, יש להגביל את הקלט מול רשימת־התרים קפדנית לפני השיבוץ, ולהעדיף ניתוב מונחה־נתונים (האש של הפניות קוד) על־פני יצירת מקור.

  • ערכי נקודה צפה המשובצים במקור שבירים. תחת use locale מפריד העשרוני עשוי לא להיות נקודה, וערכים כגון "NaN" או "Infinity" מומרים למחרוזת של אותיות שהפרסר קורא כ־barewords, לא כמספרים.

  • מסנני מקור שמופעלים בתוך eval מחרוזת דולפים החוצה אל תחום הקובץ הסובב (אלא אם נעשה שימוש ב־evalbytes). זוהי מוזרות ותיקה, לא תכונה.

ל־eval בלוק אין אף אחת מהבעיות הללו - הקוד הודר עם שאר התוכנית.

BEGIN בתוך eval מחרוזת#

בלוקי BEGIN { ... } המוטמעים ב־eval מחרוזת מבוצעים מיידית, לפני שמורץ שאר הקוד של ה־eval, בהתאם להתנהגותם ביחידת הידור רגילה. יש לקבוע את ${^MAX_NESTED_EVAL_BEGIN_BLOCKS} ל־0 כדי לגרום לכל BEGIN מוטמע לזרוק במקום זאת:

local ${^MAX_NESTED_EVAL_BEGIN_BLOCKS} = 0;
eval $untrusted;            # any BEGIN inside $untrusted dies

זה אינו הופך eval מחרוזת לבטוח - זה רק מסיר התקפה ספציפית אחת על תופעות לוואי של זמן הידור.

try/catch#

תחת use feature 'try' (יציב מאז 5.40, זמין ב־5.42), המבנה try/catch/finally מספק תחביר ייעודי לטיפול בחריגות:

use feature 'try';

try {
    risky();
}
catch ($e) {
    warn "caught: $e";
}
finally {
    cleanup();
}

הבדלים מ־eval { ... }; if ($@) { ... }:

  • הערך הנתפס נכרך למשתנה לקסיקלי טרי $e (או כל שם שניתן), לא ל־$@. אין צורך בריקוד הצילום my $err = $@.

  • $@ אינו נקבע כתופעת לוואי - לבלוק ה־try אין השפעה גלובלית עליו לאחר מכן.

  • try אינו לולאה בעצמו, אך אופרטורי בקרת זרימה מתנהגים באנלוגיה ל־eval BLOCK: return בתוך try חוזר מתת־השגרה המקיפה, ו־next/last/redo מכוונים ללולאה חיצונית.

  • finally רץ בכל מסלול יציאה ואינו יכול בעצמו להשתמש ב־return/next/last/redo. הוא מסומן כניסיוני ב־5.42 ופולט אזהרה תחת experimental::try.

יש להישען על try/catch בקוד חדש. האידיום eval־פלוס־$@ נשאר נכון ועדיין הכלי הנכון בכל פעם שהקוד הסובב חייב לרוץ על Perl ישנים יותר או לא יכול לדרוש את שומר התכונות.

דוגמאות#

לכידת שגיאת זמן ריצה והמשכת הריצה:

my $q = eval { $x / $y };
if ($@) { warn "division failed: $@"; $q = 0 }

זיהוי האם תכונה זמינה ללא יציאה באתחול:

my $have_socket = eval { socket(my $s, PF_INET, SOCK_STREAM, 0); 1 };
die "no sockets on this build" unless $have_socket;

טעינת מודול שיש לסבול את היעדרו:

my $json = eval { require JSON::XS; JSON::XS->new } //
           do { require JSON::PP; JSON::PP->new };

תפיסת חריגה מבורכת, זריקה מחדש של כל דבר אחר:

eval {
    parse($input);
};
if (my $err = $@) {
    if (ref($err) && $err->isa('MyApp::ParseError')) {
        report($err);
    }
    else {
        die $err;
    }
}

eval מקונן עם local $@ כך שצעד אופציונלי פנימי אינו מוחק אבחנה חיצונית:

eval {
    do_main_work();
    { local $@; eval { maybe_cleanup() } }   # ignore cleanup errors
    die "main still failed\n" if $something_else_wrong;
};
warn $@ if $@;

דיכוי הוו $SIG{__DIE__} המותקן גלובלית למשך לכידה פרטית:

eval {
    local $SIG{__DIE__};            # don't trigger hooks while we probe
    $answer = $x / $y;
};
warn $@ if $@;

הוו $SIG{__DIE__} שמתעד רק חריגות לא־תפוסות, באמצעות $^S כדי להבדיל בין שני המקרים:

$SIG{__DIE__} = sub {
    return if $^S;              # inside eval/try - let it propagate
    log_fatal(@_);              # top level - record before exit
};

ניתוב הנבנה בזמן ריצה (eval מחרוזת בשימוש מכוון, הקלט אינו מסופק על־ידי משתמש):

for my $op (qw(add sub mul div)) {
    eval "sub ${op}_of { \$_[0] "
       . { add=>'+', sub=>'-', mul=>'*', div=>'/' }->{$op}
       . " \$_[1] }";
    die $@ if $@;
}

מקרי קצה#

  • ללא ארגומנט - eval; הוא eval $_. לעתים רחוקות זה מה שרוצים; יש לכתוב eval { ... } או eval EXPR במפורש.

  • נקודה־פסיק סופית - ניתן להשמיט מסוף EXPR או BLOCK. eval "1+2" ו־eval "1+2;" שקולים.

  • eval BLOCK אינו לולאה - next, last, ו־redo אינם יכולים לעזוב או להפעיל מחדש את הבלוק. הם מחפשים לולאה מקיפה וייקרקרו אם אין כזו.

  • return בתוך eval BLOCK חוזר מתת־השגרה המקיפה, לא מה־eval - בלוק ה־eval אינו גוף פונקציה. כדי להפיק ערך מהבלוק, יש לשים את הערך כביטוי הסופי.

  • אזהרות אינן נלכדות. warn בתוך eval עדיין כותב ל־STDERR. יש לנתב אותן אל $@ רק אם תקבעו את $SIG{__WARN__} לעשות זאת בעצמכם.

  • זרימת בקרה החוצה מ־eval - goto, last, או next שמדלגים החוצה מהבלוק עוקפים את היציאה הרגילה של ה־eval ולכן עלולים להשאיר את $@ לא־נגוע. אין לבנות לוגיקה שתלויה בכך.

  • מרכאות על EXPR - eval $x מול eval "$x" מתנהגות זהה: שתיהן מריצות את הקוד המכיל ב־$x. המרכאות הכפולות מוסיפות רעש חזותי, לא סמנטיקה.

  • eval '$x' מול eval { $x } - שתיהן מחזירות את הערך של $x. צורת הבלוק מהודרת עם התוכנית הסובבת; עדיף אותה אלא אם הקוד באמת צריך להיבנות בזמן ריצה.

  • eval '' בתוך החבילה DB - אינו רואה את התחום הלקסיקלי הסובב הרגיל אלא את התחום של הקורא הראשון שאינו DB. רק כותבי דבאגרים נתקלים בזה.

  • כישלונות טעינת XS - חלק מבעיות הממשק הבינארי במהלך require של מודול XS הן קטלניות גם בתוך eval, אלא אם $ENV{PERL_DL_NONLAZY} נקבע. הפרשי גרסה בין המפרש הרץ ל־.so בנוי־מראש הם המקרה הנפוץ. pperl אינו טוען XS; האזהרה חלה כאשר קוד שעובר eval מאצֵל את הטעינה ל־perl5 קלאסי.

הבדלים מהמעלה־הזרם#

תאימות מלאה עם Perl 5.42 מהמעלה־הזרם.

ראו גם#

  • die - המקבילה: זורק את החריגה ש־eval תופס. כלל תו השורה החדשה המסיים על ההודעה מעצב את מה שנוחת ב־$@.

  • do - do FILE קורא ומעריך קובץ באותה דרך ש־eval מחרוזת קורא ומעריך מחרוזת; שימושי עבור קובצי הגדרה הכתובים ב־Perl.

  • return - בתוך eval BLOCK חוזר מתת־השגרה המקיפה, לא מהבלוק; יש להשתמש בביטוי מסיים כדי למסור ערך בחזרה מה־eval עצמו.

  • require - לרוב עטוף ב־eval כדי לבחון מודולים אופציונליים ללא יציאה בזמן הידור.

  • evalbytes - eval מחרוזת שמתייחס לארגומנט שלו כמחרוזת בתים ומאפשר למסנני מקור לעבוד באופן רגיל; לשימוש כאשר סמנטיקת רמת־בית חשובה.

  • $@ - היכן שחריגה נתפסת נוחתת, מנוקה למחרוזת הריקה ב־eval מוצלח. עמוד משפחת השגיאות מכסה גם את הסכנה הקלאסית של דריסת $@.

  • $^S - אומר למטפלי $SIG{__DIE__} ו־$SIG{__WARN__} האם eval או try פעיל כעת.

  • $SIG{__DIE__} - הוו טרום־זריקה; נורה גם עבור חריגות ש־eval מקיף עומד לתפוס, וזו הסיבה הרגילה לlocal־איזציה שלו למשך הלכידה.