איך לקרוא ל-system() בפרל בלי להישרף
כל סקריפט פרל שקיים די זמן מגיע בסופו של דבר לקרוא לתוכנית חיצונית. וכמעט כל אחד מהם עושה זאת לא נכון באותן שלוש דרכים: הוא בונה מחרוזת פקודה, הוא מתעלם מערך ההחזרה, והוא מניח למעטפת לפרש נתונים שמעולם לא התכוון שיפורשו. הנה הגרסה שאינה נושכת.
צורת הרשימה, לא המחרוזת
ל-system יש שתי מוסכמות קריאה, וההבדל ביניהן הוא כל הסיפור.
# Wrong: one string, handed to /bin/sh
system("cp $src $dst");
# Right: a list, executed directly
system("cp", $src, $dst);
צורת המחרוזת מעבירה את כל השורה למעטפת. המעטפת מפצלת ברווחים, מרחיבה תבניות, מפרשת מירכאות, ומכבדת
כל תו מיוחד שהיא מכירה. אם $src הוא my file.txt אתם מעתיקים שני קבצים שאינם קיימים. אם הוא
; rm -rf ~ יש לכם אחר צהריים גרוע בהרבה.
צורת הרשימה מדלגת על המעטפת לגמרי. פרל קוראת ל-execvp עם הארגומנטים בדיוק כפי שנתתם אותם. שם קובץ
עם רווח הוא ארגומנט אחד. שם קובץ עם נקודה-פסיק הוא ארגומנט אחד. דבר אינו מפורש, מפני שדבר אינו מנותח.
הכלל פשוט: אם יש לכם יותר מרכיב אחד, השתמשו בצורת הרשימה. הפעם היחידה שבה צורת המחרוזת ניתנת להגנה היא כשאתם באמת רוצים את המעטפת, לצינור או להפניה, וגם אז עדיף לעשות את ההפניה בפרל.
בדקו את ערך ההחזרה, ובדקו אותו נכון
system אינה מתה בכישלון. היא חוזרת, וערך ההחזרה אינו מה שאנשים מצפים לו.
my $rc = system("make", "all");
$rc הוא מצב ההמתנה הגולמי, אותו ערך שנוחת ב-$?. הוא אורז שלושה דברים: קוד היציאה בבית הגבוה,
האות שהרג את הילד בשבע הסיביות הנמוכות, ודגל של core dump. ילד שיצא במצב 1 נותן לכם 256. ילד שנהרג
על ידי SIGSEGV נותן לכם 11. ילד שכלל לא ניתן היה להפעיל נותן לכם -1.
אז פרקו אותו:
my $rc = system("make", "all");
if ($rc == -1) {
die "failed to execute make: $!";
}
elsif ($rc & 127) {
die sprintf("make died with signal %d%s",
($rc & 127), ($rc & 128) ? ", core dumped" : "");
}
elsif ($rc >> 8) {
die sprintf("make exited with status %d", $rc >> 8);
}
זה ארוך, ולכן איש אינו כותב אותו, ולכן סקריפטים ממשיכים בשקט אחרי שהקומפילציה נכשלה. עטפו אותו פעם אחת:
sub run {
my @cmd = @_;
my $rc = system(@cmd);
return if $rc == 0;
my $desc = join(" ", @cmd);
die "failed to execute [$desc]: $!\n" if $rc == -1;
die sprintf("[%s] died with signal %d\n", $desc, $rc & 127) if $rc & 127;
die sprintf("[%s] exited with status %d\n", $desc, $rc >> 8);
}
run("make", "all");
run("cp", $src, $dst);
עכשיו פקודה שנכשלה עוצרת את הסקריפט עם הודעה שאומרת מה נכשל ואיך. קריאה חיצונית שאינה נבדקת היא קריאה חיצונית שהוכרזה כלא רלוונטית, ואילו הייתה לא רלוונטית לא הייתם מריצים אותה.
לכידת פלט
system מחזירה את המצב ומניחה לילד לכתוב ל-stdout שלכם. כשאתם רוצים את הפלט עצמו, הרפלקס הוא
גרשיים הפוכים:
my $out = `ls $dir`;
לגרשיים ההפוכים יש בדיוק את בעיית צורת המחרוזת: כל הדבר עובר דרך המעטפת. גרסאות חדשות יותר של פרל
מקבלות צורת רשימה ל-readpipe, אבל התשובה הניידת היא לפתוח צינור מרשימה:
open(my $fh, "-|", "ls", "-l", $dir)
or die "cannot run ls: $!";
my @lines = <$fh>;
close($fh) or die "ls failed with status " . ($? >> 8);
open בשלושה ארגומנטים עם -| כמצב מריץ את הפקודה עם רשימה, בלי מעטפת, ונותן לכם ידית קובץ על
הפלט שלה. close מחזירה שקר אם הילד נכשל, ו-$? מחזיק את המצב, כך שאתם מקבלים את הבדיקה בחינם.
לכל דבר מעורב יותר, גם stdout וגם stderr, הזנת stdin, פסקי זמן, השתמשו ב-IPC::Open3 מההפצה הבסיסית
או ב-IPC::Run מ-CPAN. אל תבנו מחרוזת צינור ותמסרו אותה למעטפת.
ציטוט אינו פתרון
מישהו יאמר: פשוט צטטו את המשתנים.
system("cp '$src' '$dst'"); # still wrong
מה אם $src מכיל גרש בודד? אתם מבריחים אותו. מה אם הוא מכיל שורה חדשה, או קו נטוי הפוך, או סימן דולר
בתוך מירכאות כפולות? כללי הציטוט של המעטפת סבוכים ושונים בין מעטפות, וכל שכבת הברחה שאתם מוסיפים היא
עוד מקום לטעות בו. String::ShellQuote עושה זאת כמו שצריך אם אתם חייבים, אבל אינכם חייבים: צורת
הרשימה גורמת לכל הבעיה להיעלם, ובעיות שנעלמות מנצחות בעיות שנפתרו.
הסיכום
- השתמשו בצורת הרשימה של
systemאלא אם אתם מזמנים את המעטפת בכוונה. - בדקו תמיד את ערך ההחזרה, ופרקו אותו: -1 פירושו לא ניתן היה להפעיל, סיביות נמוכות פירושן אות, בית גבוה פירושו מצב יציאה.
- עטפו את הבדיקה פעם אחת וקראו לעטיפה בכל מקום.
- כדי ללכוד פלט, פתחו צינור עם
openורשימה, ובדקו אתclose. - אל תנסו לצטט את דרככם החוצה מצורת המחרוזת.
דבר מזה אינו חכם. זה מה ש-perldoc -f system אומר כבר עשרים שנה. רק שהדרך הלא נכונה היא שורה אחת
והדרך הנכונה היא חמש, וחמש מפסידות לאחת עד הפעם הראשונה שלשם קובץ יש רווח.