מלכודות והפתעות#

הדרכים שבהן חד־שורתיים של pperl עושים בשקט משהו אחר ממה שמשתמש ה־shell התכוון. כל סעיף נושא שם למלכודת, מראה את התסמין שלה, ונותן את התיקון. קראו ברצף פעם אחת לתחילת זיהוי דפוסים; חזרו כשמתכון מתנהג באופן שגוי.

גיהינום המרכאות#

המקור הבודד הגדול ביותר לאיבוד זמן בעבודה עם חד־שורתיים.

מרכאות בודדות הן ידידיכם#

תחת bash ו־zsh, טקסט בתוך מרכאות בודדות אינו משובץ. ללא הרחבת $VAR, ללא הרצת backticks, ללא החלפת היסטוריה של ! (zsh אינו עושה !; bash כן, וזה נושך). השתמשו במרכאות בודדות עבור תוכניות Perl אלא אם יש לכם סיבה שלא.

# Correct
pperl -ne 'print if /$var/' file              # Perl sees literal $var

# Trap
pperl -ne "print if /$var/" file              # shell expands $var before pperl sees it

הפתעת ה־! ב־bash אינטראקטיבי#

$ pperl -E 'say "hello!"'
bash: !": event not found

הרחבת ההיסטוריה של bash נורית על ! לא־מצוטט (וצורות מסוימות עם escape). דרכי עקיפה:

  • set +H ב־~/.bashrc מנטרל את הרחבת ההיסטוריה.

  • עשו escape ל־! בעת כתיבת הפקודה: "hello\!".

  • שמרו על המחרוזת בודדת־מצוטטת של ה־shell סביב התוכנית - ! אדיש בתוך '...' ברגע שהרחבת ההיסטוריה כבויה.

מרכאה בודדת בתוך מחרוזת בודדת־מצוטטת#

ל־bash אין escape עבורה. העקיפה היא ריקוד הסגירה־פתיחה בן ארבעת התווים: '\'' נקרא כסגירת־מרכאה, מרכאה ליטרלית, פתיחת־מרכאה.

# Want Perl to see:  print "it's here"
pperl -e 'print "it'\''s here\n"'

עבור כל תוכנית עם יותר מאפוסטרוף מוטמע אחד, ויתרו על '...' והשתמשו ב־q/.../ בתוך מרכאות כפולות:

pperl -e "print q/it's here/, qq/\n/"

או העבירו את התוכנית לקובץ.

מפל ה־escape של $ תחת מרכאות כפולות#

אם אתם מצטטים את תוכנית ה־Perl במרכאות כפולות (מפני שאתם רוצים ש־$1 ישובץ מה־shell), המשתנים של Perl עצמה זקוקים ל־\$:

col=3
pperl -lane "\$s += \$F[$col]; END { print \$s }"      # works

זה קריא פעם אחת. אחרי החד־שורתי השלישי כזה ברצף, עברו למשתני סביבה:

col=3
col=$col pperl -lane '$s += $F[$ENV{col}]; END { print $s }'

ראו aliases#parametrised-functions.

Locale וקידוד#

ה־I/O ברירת המחדל של pperl הוא בתים. אם הקלט שלכם הוא UTF-8, Perl עדיין רואה מחרוזת בתים עד שתבקשו אחרת.

התסמין#

$ echo 'héllo' | pperl -lne 'print length'
6                                             # "h" + 2 bytes for é + 3 more = 6
$ echo 'héllo' | pperl -CSD -lne 'print length'
5                                             # correct

התיקון הוא -C (ראו switches):

  • -CSD - STDIN/STDOUT/STDERR ופתיחות קבצים כולם UTF-8.

  • -CS - stdio בלבד.

  • -CA - להתייחס ל־@ARGV כ־UTF-8.

התאמת regex על UTF-8#

ללא -C, \w מתאים לתווי מילה של ASCII בלבד. עם -C, \w מתאים לאותיות/ספרות Unicode כפי שמוגדרות בטבלאות מאפייני ה־Unicode של Perl.

# Count "word" characters in a multilingual file
pperl -CSD -lne '$c += () = /\w/g; END { print $c }' file.txt

Mojibake בדרך החוצה#

אם pperl קורא UTF-8 בצורה נקיה אך הטרמינל מראה תווים מקושקשים, הבעיה רחוקה יותר במורד הזרם - ככל הנראה LC_ALL או הקידוד של הטרמינל עצמו. locale ו־tput מאבחנים זאת; זוהי אינה בעיה של pperl.

עריכה במקום עם -i#

תמיד .bak עד לאישור#

# Destructive if the pattern is wrong
pperl -i -pe 's/OLD/NEW/g' *.conf

# Safe
pperl -i.bak -pe 's/OLD/NEW/g' *.conf
# ...verify...
rm *.conf.bak

תבנית שגויה עם -i חשוף מחליפה את תוכן הקובץ במשהו חסר תועלת. אין שיחזור מלבד בקרת גרסאות. השתמשו ב־-i.bak, אשרו, מחקו.

מלכודת הקובץ הריק#

-i כותב את מה ש־-p (או -n בתוספת prints מפורשים) פולט. אם התוכנית אינה מדפיסה דבר, הקובץ הופך לריק.

# Wrong: -i with -n and no print - empties the file
pperl -i -ne '/KEEP/ && print' file.txt      # prints only lines matching KEEP

# Same thing, probably what you meant: keep the matching lines
pperl -i -ne 'print if /KEEP/' file.txt

שניהם נכונים. הנקודה: תחת -n, אתם הבעלים של כל בית של פלט.

עריכה במקום על פני קבצים רבים עם הסתייגויות בעלות#

-i יוצר קובץ חדש ומשנה שם. על Linux, משמעות הדבר היא שהקובץ החדש יורש את הבעלות וה־umask של המשתמש הקורא. קונפיגורציות בבעלות root הנערכות על־ידי משתמשים לא־root ייכשלו; קונפיגורציות בבעלות root הנערכות על־ידי root ישמרו את המצב אך עלולות לאבד את הקשר SELinux. הכירו את מודל האיומים שלכם לפני שאתם מכוונים את -i ל־/etc.

אופרטור היהלום <> ופתיחה קסומה#

<> (״היהלום״) הוא מה ש־-n / -p קוראים ממנו. הוא קורא מכל ארגומנט ב־@ARGV כשם קובץ, ונופל בחזרה ל־STDIN אם @ARGV ריק.

ה־״-״ הוא STDIN#

אם ארגומנט הוא "-", <> קורא מ־STDIN באותו זמן. שימושי לשזירה:

echo prefix | pperl -ne 'print' - trailer.txt
# prints: prefix, then contents of trailer.txt

מטה־תוים בשמות קבצים (ומדוע הם פחות חשובים ממה שהיו פעם)#

היסטורית, <> חשוף התייחס ל־">foo" כ־״לפתוח את foo לכתיבה״ - open דו־ארגומנטי שקרא את המצב משם הקובץ. ה־<> של Perl המודרני משתמש ב־open תלת־ארגומנטי באופן פנימי, כך ששמות קבצים המתחילים ב־<, >, |, או המכילים pipes מטופלים כשמות קבצים פשוטים. אין משטח תקיפה על שמות קבצי קלט עם pperl המודרני.

שם הקובץ נמצא ב־$ARGV#

pperl -lne 'print "$ARGV: $_" if /\bERROR\b/' *.log

$ARGV מתעדכן כשכל קובץ נפתח. "-" הוא הערך המיוחד עבור STDIN.

$. ממשיך לספור על פני קבצים#

$. אינו מאופס כש־<> עובר לקובץ הבא. אם אתם רוצים מספרי שורות לכל קובץ:

pperl -lne 'close ARGV if eof; print "$ARGV:$.: $_" if /TODO/' *.pl

close ARGV if eof מאפס את $. כשהקובץ הנוכחי מסתיים.

מלכודות של מפריד רשומה#

-l מסיר ואז משיב - פעם אחת#

-l עושה chomp לקלט וקובע את $\ למפריד שעבר chomp לפלט. בסדר תחת -n / -p. אבל -l המופעל פעמיים (נאמר, הוספתם -l אחרי -0) מבלבל אנשים:

# Read NUL-delimited input, output newline-terminated
pperl -0 -lpe ''                              # correct: -l sets $\ = "\n"

# But if you write -l0 Perl parses that as -l with argument 0:
pperl -l0 -pe ''                              # $\ becomes NUL - probably NOT what you wanted

שימו את -l אחרי -0 (ללא ארגומנט ל־-l) כשאתם רוצים את הצמד הסטנדרטי.

מצב פסקה שומר על שורות ריקות גוררות#

-00 (מצב פסקה) קובע $/ = "". Perl קורא עד וכולל מתחם השורה הריקה. ללא -l, המתחם נשאר מחובר לכל פסקה; הדפסה חזרה משמרת את השורה הריקה. עם -l, המתחם עובר chomp ו־$\ = "\n" מוסיף שורה חדשה אחת בפלט - פסקאות מתכווצות.

$ printf 'a\n\nb\n\nc\n' | pperl -00 -pe ''
a

b

c
$ printf 'a\n\nb\n\nc\n' | pperl -00 -lpe ''
a
b
c

דעו איזה אתם רוצים.

-0777 ו־$.#

תחת -0777, כל קובץ הוא רשומה אחת. $. עולה לכל רשומה, ולכן $. הוא מספר הקבצים שעובדו, לא מספר השורות.

pperl -0777 -e 'while (<>) { print "file $.: length=", length, "\n" }' *.txt
# file 1: length=1024
# file 2: length=2048

פרשנות -F לארגומנט שלו#

-F מקבל regex. גרסאות Perl לפני 5.10 דרשו מכם לכתוב את המתחמים; ה־pperl המודרני מקבל את שתי הצורות:

pperl -F:      -lane '...'                    # bare: Perl quotes it
pperl -F/:/    -lane '...'                    # explicit delimiters
pperl -F'\t'   -lane '...'                    # tab - regex form

הטעות הנפוצה היא טאב ליטרלי ב־shell שבו הטרמינל בלע אותו:

pperl -F'	' -lane '...'                   # fragile: tab may be a space in your paste

Write -F'\t' unless you are pasting from known-good source.

קדימות אופרטור סביב print#

ל־print יש קדימות נמוכה יותר מרוב האופרטורים. זה מכשיל אנשים שרפלקס ה־awk שלהם שגוי עבור Perl:

# Wrong: prints into a filehandle named "..."
pperl -ne 'print "out.txt" $_'

# Right: parentheses or commas
pperl -ne 'print $_, "\n"'
pperl -ne 'print("foo\n")'

# Wrong: the binary || applies to print's argument list
pperl -ne 'print if $_ || "never"'            # works but obscure

# One particular case worth memorising:
pperl -ne 'print (1+1)*2'                     # prints 2, not 4
# ^ print(1+1) is the function call; *2 is applied to its return value

כשאתם בספק, שימו סוגריים סביב הארגומנטים של print או סיימו את התוכנית בנקודה־פסיק מפורשת.

tr אינו regex#

tr / y מקבלים מערכי תווים, לא תבניות. tr/\d/X/ אינו מתאים לספרות - הוא מחליף תווי backslash-d ליטרליים.

pperl -pe 'tr/0-9/X/'                         # replace digits with X (character range)
pperl -pe 's/\d/X/g'                          # replace digits with X (regex)

השתמשו ב־tr לעבודת מחלקת תווים (המרת רישיות, מחיקה, טווחים) וב־s/// כשאתם זקוקים למנגנון regex כלשהו.

תחום BEGIN ו־END#

בלוקי BEGIN { ... } ו־END { ... } הם חלק מהתוכנית, לא מגוף הלולאה. משתנים שהוצהרו עם my בתוך BEGIN אינם נראים לגוף הלולאה. השתמשו ב־our או משתנים גלובליים עבור מצב שצריך לשרוד מ־BEGIN לתוך הלולאה:

# Wrong: $greeting is not in scope inside the loop
pperl -ne 'BEGIN { my $greeting = "Hi"; } print "$greeting $_"'

# Right: declare without my, or use our
pperl -ne 'BEGIN { $greeting = "Hi"; } print "$greeting $_"'

בפועל, רוב בלוקי ה־BEGIN עושים הקמה עם תופעות לוואי ($\ = "\n", קביעת פורמט) ולא קישורים.

die והקונבנציה של \n#

die ללא \n גורר מוסיף " at -e line N.\n" להודעתו. עם \n גורר, הוא מדפיס את ההודעה כפי שהיא. בחרו בכוונה:

# Developer diagnostic - want the location
pperl -E 'die "unreachable" if $x > 100'

# User-facing message - don't leak program structure
pperl -E 'die "bad input\n" unless /\d+/'

אותו כלל חל על warn.

שורה חדשה חסרה בסוף קובץ#

אם השורה האחרונה של קובץ קלט אינה כוללת שורה חדשה גוררת, -l עדיין עושה chomp למה שיש (שום דבר), ול־$_ עדיין חסר \n. -p מדפיס אותה מחדש ללא שורה חדשה סופית, והשורה הבאה של פלט ה־shell נכנסת לתוכה. נדיר שזו בעיית נכונות; לעיתים קרובות בעיית תצוגה.

pperl -pe '' no-final-newline.txt             # output lacks trailing \n

לקריאה נוספת#

  • switches - ההרחבות המכאניות של כל דגל שנזכר כאן.

  • perlvar - ההגדרה המלאה של כל משתנה מיוחד שהמלכודות שלמעלה תלויות בו ($/, $\, $., $ARGV, @ARGV).

  • perlop - אופרטור היהלום, tr, וצורות המרכאות שהוזכרו למעלה.