מ־awk ל־Perl#

אתם מכירים את awk. אתם חושבים במונחי pattern { action }, מפצלים כל שורה ל־$1, $2, $3 בלי לשאול, סופרים דברים עם a[key]++, וממסגרים את כל המשימה עם BEGIN { } ו־END { }. Perl נכתבה בחלקה כ“awk, אבל גדולה יותר“ - כמעט כל מה שאתם עושים ב־awk יש לו צורת Perl ישירה, ואז Perl ממשיכה הלאה: מבני נתונים אמיתיים, ביטויים רגולריים מלאים, מודולים, ושפה שלא נגמר לה המקום כשהסקריפט גדל.

הרעיון האחד שכדאי לקחת איתכם כבר מהשורה הראשונה: Perl היא כמעט על־קבוצה של awk. לולאת הרשומה, השדות, המערכים האסוציאטיביים, מילות המפתח BEGIN/END, print ו־printf, length/substr/split

  • כולם ממופים, לעיתים קרובות לפי אותו שם. מה שמשתנה הוא ששדות הופכים למערך אמיתי @F שאתם יכולים push ו־pop, המערך האסוציאטיבי הופך להאש Perl שאתם יכולים לקנן, ומנוע הביטויים הרגולריים הוא PCRE ולא POSIX ERE. מדריך זה פותח במיפויים החד־חד־ערכיים ומצביע על המקומות המעטים שבהם האנלוגיה דולפת.

Every runnable example here was executed on pperl, a Rust reimplementation of Perl 5.42; the # ... comments show its real output. Start scripts with:

use v5.42;

שורה אחת זו מפעילה את strict, את warnings, ואת הפונקציה המובנית say (כלומר print עם תו שורה חדשה נגרר), מערך התכונות המודרני המשמש לאורך כל הדרך.

הערה

מתגי שורה־אחת ב־build הזה של pperl התאום הטבעי של awk ב־Perl הוא שורת־הקוד־האחת: perl -ne, perl -ane, ו־perl -F, -ane. מתגים אלה הם perl5 5.42 תקני ומוצגים לאורך מדריך זה כצורה האידיומטית. אך משפחת מתגי הלולאה המשתמעת (-n, -p, -a, -F, -i, -l) עדיין אינה מזוהה על ידי build זה של pperl - רק -e מזוהה. לכן דוגמאות שורת־הקוד־האחת שלהלן מוצגות ללא פלט, וכל אחת מהן מצומדת לצורת סקריפט־מלא עם לולאה מפורשת (while (<$fh>) { ... }) שאכן רצה על pperl היום ונושאת את הפלט המאומת. השתמשו בצורת הסקריפט המלא עד שהמתגים ייכנסו.

כיצד לקרוא את זה#

לולאת השורה/רשומה#

awk מריצה את התוכנית שלכם פעם אחת לכל רשומת קלט, כשהרשומה ב־$0 והפעולה ב־{ ... }. המקבילה של Perl היא לולאה מפורשת שבה הרשומה ב־$_, משתנה הנושא המשתמע. המתג -n משחזר את ”הרץ בלוק זה לכל שורה“ של awk; ה־NR של awk (מספר הרשומה) הוא ה־$. של Perl.

awk '{ print }' input.txt
perl -ne 'print' input.txt

הצורה הניתנת־להרצה, של סקריפט מלא, של אותה לולאה - זו שרצה על pperl היום - פותחת את הקובץ ועוברת עליו במפורש. $_ היא הרשומה הנוכחית, בדיוק כמו ה־$0 של awk:

use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {           # each line lands in $_, like awk's $0
    chomp;
    say "line $. : $_";   # $. is awk's NR
}
# line 1 : alice 85 90
# line 2 : bob 70 60
# line 3 : carol 95 100
# line 4 : alice 50 55

(קובץ הקלט החזיק alice 85 90, bob 70 60, carol 95 100, alice 50 55.) המלכודת: שורה הנקראת עם <$fh> שומרת על תו השורה החדשה הנגרר שלה, בניגוד ל־$0 של awk, שממנו מופרד מפריד הרשומה. קראו ל־chomp כדי לקצץ אותו (chomp הוא הדבר הקרוב ביותר ל“awk כבר הסירה עבורכם את תו השורה החדשה“). מספר הרשומה הוא $. = ה־NR של awk; כשאתם קוראים מספר קבצים עם <>, $. ממשיך לספור לאורכם, כך שאיפוס ה־FNR של awk (מונה לכל קובץ) הוא דבר שאתם מסדרים בעצמכם על ידי בדיקת eof.

שדות ופיצול#

זה הלב של awk, ו־Perl נותנת לכם את אותה תמונה עם תפנית אחת: השדות חיים במערך אמיתי @F, כך שה־$1 של awk הוא ה־$F[0] של Perl (מבוסס־אפס), $0 הוא $_, ו־NF הוא הספירה scalar @F. תחת המתג -a Perl מפצלת אוטומטית כל שורה אל @F בדיוק כפי ש־awk מפצלת אוטומטית אל $1..$NF; המקבילה הניתנת־להרצה היא split מפורש אחד.

awk '{ print $1, NF }' input.txt          # first field, field count
awk -F, '{ print $1 }' input.txt          # comma separator (FS)
perl -ane 'print "$F[0] @{[scalar @F]}\n"' input.txt   # -a fills @F
perl -F, -ane 'print "$F[0]\n"' input.txt              # -F sets the split pattern

צורת הסקריפט המלא עושה את הפיצול בעצמה. split חשוף ללא ארגומנטים מפצל את $_ על רווחים לבנים ומשמיט רווחים מובילים - בדיוק ה־FS של ברירת המחדל של awk:

use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    my @F = split;                          # awk's automatic field split
    say "$F[0] has ", scalar(@F) - 1, " scores"; # $F[0]=$1, @F=NF
}
# alice has 2 scores
# bob has 2 scores
# carol has 2 scores
# alice has 2 scores

חשבון בין שדות הוא פעולת awk היומיומית print $1, $2+$3, והוא נקרא כמעט זהה:

use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    my @F = split;
    say "$F[0]: ", $F[1] + $F[2];   # awk: print $1, $2+$3
}
# alice: 175
# bob: 130
# carol: 195
# alice: 105

ה־$NF של awk (השדה האחרון) הוא האינדקס השלילי $F[-1] של Perl, ו־FS שאינו ברירת המחדל הוא ארגומנט התבנית ל־split:

use v5.42;
open my $fh, '<', '/tmp/data.csv' or die "open: $!";  # alice,85,90 ...
while (<$fh>) {
    chomp;
    my @F = split /,/;        # awk -F,
    say $F[-1];               # awk's $NF: last field
}
# 90
# 60
# 100

כדי לחבר שדות בחזרה עם מפריד (ה־OFS של awk), קבעו את $, (מפריד שדה הפלט ש־print/say מכניס בין ארגומנטי הרשימה שלו):

use v5.42;
open my $fh, '<', '/tmp/data.csv' or die "open: $!";
while (<$fh>) {
    chomp;
    my @F = split /,/;
    local $, = "|";           # OFS
    say @F;                   # join the list with OFS
}
# alice|85|90
# bob|70|60
# carol|95|100

המלכודת: שדות הם מבוססי־אפס. ה־$1 של awk הוא $F[0], $2 הוא $F[1], ו־$0 (הרשומה כולה) הוא $_, לא איבר של @F. NF הוא scalar @F (או $#F + 1); השדה האחרון הוא $F[-1], לא $F[NF]. ו־split עם רווח אחד כמחרוזת (split ' ') הוא מצב הרווח־הלבן־וקיצוץ המיוחד; split / / (ביטוי רגולרי עם רווח אחד) הוא מילולי ואינו מקצץ רווחים מובילים. split פשוט ללא ארגומנט הוא מצב ברירת המחדל של awk שאתם רוצים רוב הזמן.

ביטויים רגולריים והחלפה#

ה־sub, gsub, match, ו־~ של awk ממופים כולם אל תחביר הביטויים הרגולריים היחיד של Perl: =~ קושר תבנית למחרוזת, s/// מחליף, s///g הוא גלובלי (gsub מול sub של awk). מנוע Perl הוא PCRE, על־קבוצה ממש של POSIX ERE של awk, כך שכל תבנית awk עובדת ו־Perl מוסיפה הפניות לאחור, lookaround, לכודות בעלות שם, ואת הדגלים /e, /r, /x של־awk אין מקבילה אליהם.

awk '/^alice/ { sub(/alice/, "ALICE"); print }' input.txt
perl -ne 'if (/^alice/) { s/alice/ALICE/; print }' input.txt

צורת הסקריפט המלא:

use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    next unless /^alice/;   # awk: /^alice/ { ... }
    s/alice/ALICE/;         # awk: sub(/alice/, "ALICE") (first match)
    print;
}
# ALICE 85 90
# ALICE 50 55

השתמשו ב־s///g עבור ה־gsub של awk (כל התאמה), וקבוצות לכידה נוחתות ב־$1, $2 בדיוק כפי שה־match של awk ממלא את RSTART/RLENGTH בתוספת מערך ה־gawk. הדגל /e מריץ Perl שרירותי בהחלפה, מה ש־awk אינה יכולה לעשות:

use v5.42;
my $s = "items: 3 and 4";
(my $t = $s) =~ s/(\d+)/$1 * 10/ge;   # /e: replacement is Perl code
say $t;                               # items: 30 and 40

length, substr, index, ופונקציות האותיות הגדולות/קטנות ממופות לפי שם - שימו לב לאינדוקס:

use v5.42;
my $s = "alice";
say length $s;          # 5         (awk length($s))
say substr $s, 0, 3;    # ali       (awk substr($s,1,3) - awk is 1-based!)
say uc $s;              # ALICE     (awk toupper($s))

המלכודת: substr הוא מבוסס־אפס בעוד ש־substr של awk הוא מבוסס־אחד, כך ש־substr($s,1,3) של awk הוא substr($s,0,3) של Perl. ו־s/// עורך את היעד שלו במקום ומחזיר את ספירת ההחלפות, לא את המחרוזת החדשה - כך ש־my $new = ($s =~ s/a/b/) שם מספר ב־$new. העתיקו תחילה ((my $t = $s) =~ s/.../.../) או השתמשו בדגל /r כדי לקבל מחרוזת רעננה. מחלקות תווים של POSIX ([[:digit:]]) עובדות, אך הקיצורים של Perl (\d, \w, \s) קצרים יותר ומה שתראו בקוד Perl.

כתובות וטווחים#

כלל awk הוא pattern { action }: הפעולה רצה רק על רשומות התואמות את התבנית. ב־Perl תבנית זו היא פשוט תנאי על המשפט - if בתחביר סיומת הבודק את $_. /re/ חשוף תואם כנגד $_, בדיקה מספרית משתמשת ב־$. (ה־NR של awk), והטווח /start/,/end/ של awk הוא אופרטור ה־flip-flop של Perl, /start/ .. /end/.

awk 'NR==2'              input.txt       # print record 2
awk '/banana/'           input.txt       # print matching records
awk '/bob/,/carol/'      input.txt       # range, inclusive
perl -ne 'print if $. == 2'            input.txt
perl -ne 'print if /banana/'           input.txt
perl -ne 'print if /bob/ .. /carol/'   input.txt

ה־flip-flop של הסקריפט המלא, אמת מהשורה התואמת את התבנית השמאלית עד השורה התואמת את הימנית, כולל - בדיוק טווח שתי־התבניות של awk:

use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    print if /bob/ .. /carol/;   # awk: /bob/,/carol/
}
# bob 70 60
# carol 95 100

המלכודת: pattern חשוף ללא פעולה ב־awk משמעו ”הדפס את הרשומה“; ב־Perl הלולאה מפורשת, כך שאתם כותבים את ה־print (או say) בעצמכם. ובבדיקה בוליאנית .. הוא אופרטור ה־flip-flop (טווח), לא אופרטור טווח־הרשימה (1 .. 10 בונה רשימה). Perl מבחינה ביניהם לפי הקשר: בעמדה הסקלרית של if, .. הוא בורר טווח־השורות בעל־המצב המשקף את addr1,addr2 של awk.

קלט/פלט, צינורות, עריכה במקום#

awk קוראת אוטומטית כל קובץ הנקוב בשורת הפקודה שלה ויש לה getline לקריאות מפורשות. ה־<> של Perl (אופרטור היהלום) קורא כל קובץ ב־@ARGV בתורו - אותה ברירת מחדל - ו־<$fh> הוא ה־getline המפורש. כתיבה לפקודה או קריאה ממנה (print | "cmd" ו־"cmd" | getline של awk) היא מטפל קובץ של צינור.

awk '{ print > "out.txt" }' input.txt      # redirect output
awk '{ print | "sort" }'     input.txt     # pipe to a command
use v5.42;
open my $out, '>', '/tmp/out.txt' or die "write: $!";
open my $in,  '<', '/tmp/scores.txt' or die "read: $!";
while (<$in>) {
    print {$out} $_;        # awk: print > "out.txt"
}
close $out;

לצינורות, open my $fh, '|-', 'sort' כותב לקלט הפקודה ו־open my $fh, '-|', 'ls', '-l' קורא את פלט הפקודה - ה־| "cmd" של awk בשני הכיוונים.

ל־awk אין עריכה־במקום מובנית; אתם נשענים על העברת קובץ־tmp או על gawk -i inplace של GNU. Perl מאייתת זאת -i על שורת־קוד־אחת:

perl -i -pe 's/red/crimson/' file.txt      # edit in place

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

use v5.42;
my $path = '/tmp/inplace.txt';        # holds: red green blue

open my $in, '<', $path or die "read: $!";
my @lines = <$in>;                    # slurp all lines (list context)
close $in;

s/e/E/g for @lines;                   # transform each line's $_

open my $out, '>', $path or die "write: $!";
print {$out} @lines;                  # write them back
close $out;
# file now holds: rEd / grEEn / bluE

המלכודת: open מחזיר שקר בכישלון במקום להפסיק, לכן הניב or die "...: $!" הוא חובה - $! היא מחרוזת שגיאת המערכת, הכישלון השקט של awk שנעשה מפורש. ו־print {$out} ... זקוק לסוגריים המסולסלים סביב מטפל הקובץ כשהוא משתנה; הצורה החשופה print FH ... (ללא פסיק) היא למטפלים בעלי שם, מה שהוא מעידה נפוצה של היום הראשון.

שורות־קוד־אחת ומתגים#

שורת־הקוד־האחת היא המגרש הביתי של awk וגם של Perl. משפחת המתגים שמשחזרת את awk: -e מספק את התוכנית, -n עוטף אותה בלולאה לכל־רשומה (ה־{ } המשתמע של awk), -p עושה את אותו דבר עם הדפסה אוטומטית בסוף כל מחזור, -a מפצל אוטומטית אל @F (פיצול השדה האוטומטי של awk), -F קובע את תבנית הפיצול (ה־FS של awk), ו־-l מטפל בסיומות שורה (נוחות ה־ORS/RS של awk). BEGIN { } ו־END { } הם אותן מילות מפתח כמו awk, הרצות לפני ואחרי הלולאה.

awk '/error/ { print }'          log.txt        # print error records
awk '{ s += $NF } END { print s }' input.txt    # sum the last field
awk -F, '{ print $1 }'           input.txt      # first CSV field
perl -ne 'print if /error/'                     log.txt
perl -ane 'END { print "$s\n" } $s += $F[-1]'   input.txt
perl -F, -ane 'print "$F[0]\n"'                 input.txt

אלה מוצגים ללא פלט מכיוון שמתגי הלולאה המשתמעת אינם מזוהים על ידי build זה של pperl עדיין (ראו את ההערה בראש). כל אחד ממופה לצורת לולאה מפורשת מאומתת: perl -ne 'CODE' הוא while (<$fh>) { CODE }, -ane מוסיף my @F = split בראש, ובלוקי BEGIN/END עובדים בסקריפט מלא בדיוק כמו ב־awk. הצורה הניתנת־להרצה ”סכימת עמודה עם BEGIN/END“:

use v5.42;
my $n = 0;
my $sum = 0;
BEGIN { say "start" }              # runs before the loop, like awk BEGIN
END   { say "lines=$n sum=$sum" }  # runs after, like awk END
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    my @F = split;
    $n++;
    $sum += $F[1];                 # awk: sum += $2
}
# start
# lines=4 sum=300

המלכודת: עד שהמתגים ייכנסו ל־pperl, כתבו את הלולאה המפורשת (היא רצה היום וזה ממילא מה שהמתגים מתרחבים אליו), ועיינו במדריך שורות־הקוד־האחת לקטלוג המתגים המלא ולעשרות מתכונים - זה הדבר בעל המנוף הגבוה ביותר לקריאה בהגעה מרקע של awk. ל־Perl יש גם תת־שגרות מלאות, הפניות, מודולים, ותכנות מונחה־עצמים אם הסקריפט שלכם גדל מעבר לשורת־קוד־אחת; ראו את מדריך התכנות מונחה־העצמים ואת שאר מדריכי ההגעה־מ.

זרימת בקרה#

מילות הבקרה של awk ממופות אל אלה של Perl כמעט שם־לשם. ה־next של awk (דלג לרשומה הבאה) הוא ה־next של Perl; break/continue בלולאות awk הם last/next של Perl; ה־for (k in arr) של awk הופך ל־for של Perl על keys. הסוגריים המסולסלים ו־; זהים; Perl מוסיפה את צורות הסיומת הנקראות כמו תבניות awk.

awk '{ if ($2 < 70) next; print }' input.txt
use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
while (<$fh>) {
    my @F = split;
    next if $F[1] < 70;        # awk: if ($2 < 70) next
    print;
}
# alice 85 90
# carol 95 100

המלכודת: אופרטורי ההשוואה של Perl באים בשתי משפחות ואתם בוחרים לפי הטיפוס של האופרנדים, בעוד של־awk יש רק </== לשניהם. מספרים משתמשים ב־==, <, >, <=>; מחרוזות משתמשות ב־eq, lt, gt, cmp. awk מכריעה השוואה מספרית־מול־מחרוזתית לפי הערכים בזמן ריצה; Perl מכריחה אתכם לבחור את האופרטור. שימוש ב־== על מחרוזות (או eq על מספרים) הוא באג שקט ותכוף - "abc" == "xyz" הוא אמת ב־Perl (שניהם הופכים מספרית ל־0), מה שכמעט אף פעם אינו מה שהתכוונתם אליו.

משתנים ונתונים#

זה המיפוי המשמח ביותר במדריך: המערך האסוציאטיבי של awk הוא האש Perl, והניבים היומיומיים כמעט זהים. ה־count[$1]++ של awk הוא ה־$count{$F[0]}++ של Perl; for (k in count) הוא for my $k (keys %count); if (k in count) הוא exists $count{$k}. ההסטה האחת היא הסיגיל - ההאש כולה היא %count, אך ערך אחד הוא $count{$key}.

awk '{ total[$1] += $2 + $3 }
     END { for (k in total) print k, total[k] }' input.txt
use v5.42;
open my $fh, '<', '/tmp/scores.txt' or die "open: $!";
my %total;
while (<$fh>) {
    my @F = split;
    $total{$F[0]} += $F[1] + $F[2];   # awk: total[$1] += $2 + $3
}
for my $name (sort keys %total) {     # awk END loop over the array
    say "$name $total{$name}";
}
# alice 280
# bob 130
# carol 195

הניב הדו־קבצי NR==FNR - טען מפתחות מהקובץ הראשון, ואז בדוק חברות בשני - הוא לולאה מפורשת אחת לכל קובץ ב־Perl, מה שברור יותר משורת־הקוד־האחת של awk שהוא מחליף:

use v5.42;
my %seen;
open my $f1, '<', '/tmp/data.csv' or die "open: $!";
while (<$f1>) {
    my ($k) = split /,/;          # first field only
    $seen{$k} = 1;
}
close $f1;
open my $f2, '<', '/tmp/scores.txt' or die "open: $!";
while (<$f2>) {
    my ($k) = split;
    print if $seen{$k};           # awk: ($1 in seen)
}
# alice 85 90
# bob 70 60
# carol 95 100
# alice 50 55

המלכודת: להאש אין סדר מובנה (ה־for (k in arr) של awk גם הוא לא־מסודר, כך שזה לא יפתיע אתכם), לכן עברו sort keys %h כשאתם רוצים סדר יציב. חברות היא exists $h{$k}, לא ה־k in arr של awk - ועצם קריאת $h{$k} כדי לבדוק אותו תייצר את המפתח לכדי קיום בהקשרים מסוימים, מה ש־exists לעולם אינו עושה. מספרים ומחרוזות מומרים אוטומטית ב־Perl כמו ב־awk, אך זכרו את הפיצול == מול eq מתוך זרימת בקרה.

מלכודות בהגעה מ־awk#

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

  • שדות הם מבוססי־אפס. ה־$1 של awk הוא $F[0], $2 הוא $F[1], והרשומה כולה $0 היא $_. NF הוא scalar @F; השדה האחרון הוא $F[-1], לא $F[NF].

  • <$fh> שומר על תו השורה החדשה הנגרר, בניגוד ל־$0 של awk. chomp כדי לקצץ אותו.

  • substr הוא מבוסס־אפס, בעוד שזה של awk מבוסס־אחד: substr($s,1,3) של awk הוא substr($s,0,3) של Perl.

  • s/// מחזיר ספירה, לא את המחרוזת החדשה. my $x = $s =~ s/a/b/ שם את ספירת ההחלפות ב־$x. העתיקו תחילה, או השתמשו בדגל /r כדי לקבל מחרוזת חדשה.

  • שתי משפחות השוואה. מספרים משתמשים ב־==/</<=>; מחרוזות משתמשות ב־eq/lt/cmp. ה־==/< היחיד של awk מפוצל לשניים, נבחר לפי אופרטור, לא לפי ערך. "abc" == "xyz" הוא אמת (שניהם הופכים מספרית ל־0).

  • split חשוף הוא מצב ברירת המחדל של awk. split (ללא ארגומנטים) מפצל את $_ על רווחים לבנים ומקצץ רווחים מובילים. split / / (ביטוי רגולרי עם רווח אחד) הוא מילולי ואינו מקצץ - השתמשו ב־split פשוט עבור ה־FS של ברירת המחדל של awk.

  • חברות היא exists $h{$k}, לא k in arr. בדיקת מפתח על ידי קריאת $h{$k} יכולה לייצר אותו אוטומטית; exists לעולם אינו עושה זאת.

  • open אינו מפסיק בכישלון. השתמשו ב־open my $fh, '<', $path or die "...: $!". $! היא מחרוזת שגיאת המערכת.

  • מתגי הלולאה המשתמעת (-n/-p/-a/-F/-i/-l) עדיין אינם מזוהים על ידי build זה של pperl. רק -e עובד היום. כתבו את הלולאה המפורשת while (<$fh>) { my @F = split; ... }, שהיא בדיוק מה שהמתגים מתרחבים אליו, עד שהם ייכנסו.