Perl deel II

ArticleCategory: [Es gibt verschiedene Artikel Kategorien]

Software Development

AuthorImage:[Ein Bild von Dir]

[Guido Socher]

TranslationInfo:[Author and translation history]

original in en Guido Socher

en to nl Floris Lambrechts

AboutTheAuthor:[Eine kleine Biographie über den Autor]

Guido is al jaren een Linux fan en Perl hacker. Tegenwoordig houdt hij zich bezig met de renovatie van zijn huis en met tuinieren.

Abstract:[Here you write a little summary]

Het eerste deel was een algemeen overzicht van Perl. In dit tweede deel gaan we ons eerste echt bruikbare programma schrijven.

ArticleIllustration:[This is the title picture for your article]

[Illustratie]

ArticleBody:[The article body]

Een basis voor je programma

Perl is op zijn best als het gebruikt wordt voor het schrijven van kleine, gespecialiseerde programma's. Om de ontwikkeling van nieuwe programma's te versnellen is het handig om al een basis bij de hand te hebben met een minimum aan functionaliteit. Deze code biedt simpele interpretatie van opdrachtregel opties en heeft al een subroutine om een help-bericht weer te geven.

!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
# uncomment strict om de Perl compiler zeer streng te maken
# wat betreft declaraties
#use strict;
# globale variabelen:
use vars qw($opt_h);
use Getopt::Std;
#
&getopts("h")||die "ERROR: Onbekende optie. -h voor help\n";
&help if ($opt_h);
#
#>>jouw code<<
#
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
print "help-bericht\n";
exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

We bekijken deze code. &getopts() is een subroutine uit de bibliotheek Getopt::Std om de opdrachtregel opties in te lezen. Het zet de variabelen met de naam $opt_<optie> in de toestand die overeenkomt met de opties die via de command line worden doorgegeven. Alle opties in de command line beginnen met een "-" (minteken) en komen ná de programmanaam en vóór de andere argumenten (dit is een algemene Unix regel). Het argument van &getopts (de "h" in dit geval) definieert de opties die op de opdrachtregel mogelijk zijn. Als die optie zelf ook nog een argument heeft, moet er een dubbelpunt staan na de letter van de optie. Bv &getopts("d:x:h") zegt dat dit programma drie opties -d, -x en -h heeft, waarvan -d en -x gegeven worden met een extra argument. Dus "-d iets" is een geldige optie, maar "-d -x iets" is fout omdat -d niet gevolgd wordt door een argument.
Als op de opdrachtregel de optie -h gegeven wordt, wordt de variabele $opt_h gevuld en &help if ($opt_h); roept dan de subroutine help aan. De uitdrukking sub help{ declareert de subroutine.
Het is op dit moment niet echt noodzakelijk dat je elk detail van de code begrijpt. Beschouw dit gewoon als een basis waar je de eigenlijke functionaliteit aan kan toevoegen.

De basis gebruiken

We beginnen met een klein omzettingsprogramma, een uitbreiding van ons basisprogramma. numconv, zoals we het zullen noemen, converteert hexadecimale getallen naar gewone decimale en omgekeerd.
numconv -x 30 geeft het hex equivalent van 30.
numconv -d 1A geeft de decimale versie van hex 1A.
numconv -h geeft een help tekst.
Er is in Perl een functie hex() die hexadecimale getallen converteert naar decimale. We gebruiken de functie printf() om decimale getallen naar hex om te zetten. Wanneer de juiste code is ingevoegd in ons basisprogramma, krijgen we dit:


#!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
# uncomment strict om de perl compiler zeer streng te maken
# wat betreft declaraties
#use strict;
# globale variabelen:
use vars qw($opt_d $opt_x $opt_h);
use Getopt::Std;
#
&getopts("d:x:h")||die "ERROR: Onbekende optie. -h voor help\n";
&help if ($opt_h);
if ($opt_d && $opt_x){
    die "ERROR: -x en -d zijn niet tegelijk te gebruiken.\n";
}
if ($opt_d){
    printf("decimaal: %d\n",hex($opt_d));
}elsif ($opt_x){
    printf("hex: %X\n",$opt_x);
}else{
    # fout gebruik. -d of -x moet gegeven zijn:
    &help;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
    print "converteer een getal naar hex of decimaal.
GEBRUIK: numconv [-h] -d hex_getal
         numconv [-h] -x decimaal_getal

OPTIES: -h deze help
VOORBEELD: numconv -d 1af
\n";
    exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

Klik hier om de (Engelstalige) broncode van numconv te downloaden.
In de volgende hoofdstukken bekijken we dit programma stap voor stap en proberen we de werking te begrijpen.

If-statements

In perl zijn er twee vormen van het if-statement:
uitdrukking if (voorwaarde);
of
if (voorwaarde) BLOK [[elsif (voorwaarde) BLOK ...] else BLOK]

BLOK staat voor een aantal opdrachten die tussen {accolades} staan. Je kan dus bijvoorbeeld schrijven:

printf("hallo\n") if ($i);

if ($i == 2){
   printf("i is 2\n");
}elsif ($i == 4){
   printf("i is 4\n");
}else{
   printf("i is noch 2 noch 4\n");
}

Net als in C is het mogelijk om de verkorte notatie && en || te gebruiken.
printf("hallo\n") if ($i);
is dus hetzelfde als
($i) && printf("hallo\n");
Vooral de || zoals gebruikt in ons basisprogramma vertaalt makkelijk naar mensentaal.
&getopts("d:x:h")||die "ERROR\n";
"Lees de opties of stop". De functie die()printf gevolgd door exit. Het geeft een boodschap en beëindigt het programma.
&getopts("d:x:h")||die "ERROR\n";
kan ook geschreven worden als
die "ERROR\n"; if (! &getopts("d:x:h"));
waar de ! staat voor een logische NOT voorwaarde. Dit is ook weer hetzelfde als
die "ERROR\n"; unless (&getopts("d:x:h"));
unless is hetzelfde als 'if-not' en is beter leesbaar dan if(!..)

Zoals je ziet zijn er in Perl meerdere manieren om een if-statement te schrijven. Natuurlijk heb je ze niet allemaal nodig; gebruik gewoon degene die je zelf het handigst vindt.

Variabelen

In het eerste perl artikel zagen we dat scalaire variabelen (de $-variabelen) gebruikt konden worden zonder ze te declareren. Ze worden gemaakt op het moment dat ze voor het eerst gebruikt worden. Dit is zeer handig bij kleine programma's, maar bij grotere projecten kan het tot moeilijk te ontdekken fouten leiden. Variabelen declareren geeft de compiler de mogelijkheid om extra controles te doen op typfouten.
Bij "use strict;" moet je alle variabelen declareren.
Beschouw de volgende -correcte- code:

#!/usr/bin/perl
use strict;
my $i=1;
print "i is $i\n";

Dit programma is correct en geeft "i is 1". Als we echter per ongeluk j in plaats van i typen:

#!/usr/bin/perl
#
$i=1;
print "i is $j\n";

Deze code geeft geen fouten in perl en produceert "i is ". De perl module "use strict;" dwingt de compiler om te klagen over een dergelijk programma. Als "strict" gebruikt wordt, moet alles gedeclareerd worden, anders krijg je een foutmelding.

#!/usr/bin/perl
use strict;
my $i=1;
print "i is $j\n";

Dit leidt tot het volgende bericht en maakt het makkelijker om de typfout te vinden:

Global symbol "$j" requires explicit package name at ./vardec line 4.
Execution of ./vardec aborted due to compilation errors.
Exit 255

Variabelen kunnen we declareren met my of, zoals we eerder zagen, met use vars qw():
use vars qw($opt_h);

Globale variabelen worden gemaakt met use vars. Deze variabelen zijn globaal, zelfs in alle ingevoegde bibliotheken.
Variabelen die enkel gelden in het huidige programma bestand (dus globaal voor alle subroutines in dat bestand) worden gedeclareerd met my in het begin van het programma (buiten een subroutine).
Variabelen locaal in de huidige subroutine worden in de subroutine gedeclareerd met my.

Mensen met ervaring in shell programmeren zullen het $-teken waarschijnlijk liever weglaten. Dit is niet mogelijk in perl; niet bij het declareren en niet bij het gebruiken. Voor een scalaire variabele moet gewoon áltijd een $-teken staan.

Bij het declareren van een variabele kan je er ook meteen een waarde aan toewijzen. my $myvar=10; declareert de variabele $myvar en zet zijn beginwaarde op 10.

Subroutines

De "help" subroutine hebben we hierboven al gebruikt in het numconv programma. Subroutines worden gebruikt om eigen functies te maken. Ze zijn dus zeer nuttig bij het structureren van een programma.
Een subroutine kan overal in het programma geschreven worden: of het nu voor of na de aanroep ervan geschreven staat, maakt niet uit. Een subroutine beging met sub naam(){... en wordt aangeroepen (i.e. uitgevoerd) met $retwaarde=&naam(...argumenten...). De $retwaarde is de waarde die de functie als 'return' geeft, met andere woorden de waarde van de laatst uitgevoerde bewerking in die subroutine. De argumenten die je de subroutine geeft worden doorgegeven in de speciale array @_. In het derde deel zullen we hier dieper op ingaan. Nu volstaan we met de opmerking dat in de subroutine de waarde van scalaire variabelen in de array @_ gelezen kunnen worden met shift. Hier is een voorbeeld:

#!/usr/bin/perl
use strict;
my $resultaat;
my $b;
my $a;
$resultaat=&telop_en_vermenigvuldig(2,3);
print "2*(2+3) is $resultaat\n";

$b=5;$a=10;
$resultaat=&telop_en_vermenigvuldig($a,$b);
print "2*($a+$b) is $resultaat\n";

# tel twee getallen bij elkaar op en vermenigvuldig met twee:
sub telop_en_vermenigvuldig(){
    my $locala=shift;
    my $localb=shift;
    ($localb+$locala)*2;
}

Een echt programma

Nu we al heel wat perl syntax en taalelementen gezien hebben, is het tijd om een echt programma te schrijven.
Perl was ontworpen om tekstbestanden te kunnen manipuleren met zeer weinig programmeerwerk. Ons eerste Perl programma gaat een lijst met afkortingen bekijken en alle dubbelen in de lijst opsporen. Dubbel zijn alle afkortingen die meer dan één keer voorkomen. De lijst heeft de volgende vorm:

Met Perl kan je makkelijk tekstbestanden manipuleren
AC Access Class
AC Air Conditioning
AFC Automatic Frequency Control
AFS Andrew File System
...
<

Je kan de lijst hier downloaden. De syntax van dit bestand is:

Hoe lezen we nu zo'n tekstbestand? Hier is de Perl code om een tekst regel voor regel te lezen:


....
open(FD,"abb.txt")||die "ERROR: kan bestand abb.txt niet lezen\n";
while(){
   #doe iets
}
close FD;
....

De 'open' functie neemt een file descriptor als eerste argument en de naam van het bestand als tweede. File descriptors zijn een speciaal soort variabelen. Je zet ze gewoon in de 'open' functie, je gebruikt ze in de functie die de data uit het bestand inleest en tenslotte geef je ze door aan 'close', waardoor het bestand gesloten wordt. Het bestand lezen kan met <FD>. De <FD> kan als argument aan een while lus gegeven worden, wat resulteert in het regel voor regel lezen van het bestand.
In Perl worden file descriptors volgens traditie volledig in hoofdletters geschreven.

Waar gaan de gegevens naartoe? Perl heeft een aantal impliciete variabelen. Dit zijn variabelen die je niet hoeft te declareren, ze zijn er altijd. Een voorbeeld van zo'n variabele is $_. Hierin staat het nummer van de regel die op dat moment gelezen wordt in de while lus. Even proberen (download de code):

#!/usr/bin/perl
use strict;
my $i=0;
open(FD,"abb.txt")||die "ERROR: kan bestand abb.txt niet lezen\n";
while(<FD>){
   # verhoog de regel teller met 1
   # Je kent waarschijnlijk de ++ uit C:
   $i++;
   print "regel $i is $_";
}
close FD;
De impliciete variabele $_ bevat de huidige regel.

Zoals je ziet hebben we dit niet geschreven als print "regel $i is $_\n". De $_ variabele bevat namelijk niet allen de huidige regel van het tekstbestand, maar daarna ook nog een newline karakter (enter-teken).

Nu kunnen we het bestand lezen. Om het volledige programma te schrijven moeten we nog twee dingen leren:

  1. Hoe we een afkorting lezen vanaf het begin van de regel.
  2. Hoe Perl hash tables werken.

Reguliere expressies zijn zeer handig bij het zoeken naar een bepaald patroon in een tekst. We zijn op zoek naar alle letters vanaf het begin van de regel tot aan de eerste spatie. Met andere woorden, ons zoekpatroon is "begin van de regel-->een aantal karakters, geen spatie-->een spatie". Geschreven met de reguliere expressies in perl is dit ^\S+\s. Als we die uitdrukking nu in een m//; plaatsen, zal perl deze bewerking uitvoeren op de $_ variabele (die de huidige regel bevat). De \S+ in de reguliere expressie staat voor een aantal tekens behalve spatie. Als we haakjes zouden plaatsen rond de \S+, dan zou de tekst "not space characters" in de variabele $1 geplaatst worden. We passen nu ons programma aan:

#!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
use strict;
# globale variabelen:
use vars qw($opt_h);
my $i=0;
use Getopt::Std;
#
&getopts("h")||die "ERROR: Onbekende optie. -h voor help.n";
&help if ($opt_h);
#
open(FD,"abb.txt")||die "ERROR: kan bestand abb.txt niet lezen\n";
while(<FD>){
    $i++;
    if (m/^(\S+)\s/){
        # $1 bevat nu het eerste woord (\S+)
        print "$1 is de afkorting op regel $i\n";
    }else{
        print "regel $i begint niet met een afkorting\n";
    }
}
close FD;
#
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
     print "help tekst\n";
     exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

De match operator (m/ /) retourneert 1 als de reguliere expressie met succes is toegepast op de huidige regel. Daardoor kunnen we hem gebruiken binnen een if-statement. Zet voor dat je $1 gebruikt een if-statement rond de match operator om te controleren of de gegevens wel correct zijn.

 

Hash Tables

Nu we het bestand en de afkortingen kunnen inlezen missen we nog een manier om te controleren of we de afkorting al eerder zijn tegengekomen. We gebruiken een nieuw data type: Hash Tables. Hash Tables zijn arrays die kunnen geïndexeerd worden met een string. Als je de volledige Hash Table nodig hebt schrijf je gewoon een % voor zijn naam. Om een individuele waarde te lezen gebruik je $naam{"index_string"}. Dit is dezelfde $ als bij andere scalaire variabelen omdat een veld in een Hash Table eigenlijk een gewone scalaire variabele bevat. Een voorbeeld:

#!/usr/bin/perl -w
my %htab;
my $index;
# zet de gegevens in de Hash Table:
$htab{"iets"}="waarde_van_iets";
$htab{"ietsanders"}=42;
# lees de gegevens terug:
$index="iets";
print "%htab op index \"$index\" is $htab{$index}\n";
$index="ietsanders";
print "%htab op index \"$index\" is $htab{$index}\n";

De uitvoer van dit programma is:

%htab op index "iets" is waarde_van_iets
%htab op index "ietsanders" is 42

Nu kunnen we ons programma afwerken:

 1  #!/usr/bin/perl -w
 2  # vim: set sw=4 ts=4 si et:
 3  #
 4  use strict;
 5  # globale variabelen:
 6  use vars qw($opt_h);
 7  my %htab;
 8  use Getopt::Std;
 9  #
10  &getopts("h")||die "ERROR: Onbekende optie. -h voor help\n";
11  &help if ($opt_h);
12  #
13  open(FD,"abb.txt")||die "ERROR: kan bestand abb.txt niet lezen.\n"
14  print "Afkortingen met meerdere betekenissen in bestand abb.txt:\n";
15  while(<FD>){
16      if (m/^(\S+)\s/){
17          # we gebruiken de afkorting als index voor de hash:
18          if ($htab{$1}){
19              # deze afkorting is dubbel:
20              if ($htab{$1} eq "_herhaald_"){
21                  print; # hetzelfde als print "$_";
22              }else{
23                  # dit is de tweede keer dat we deze
24                  # afkorting tegenkomen
25                  print $htab{$1};
26                  # print de huidige regel:
27                  print;
28                  # markeer als herhaald: komt meer dan 1 keer voor
29                  $htab{$1}="_herhaald_";
30              }
31          }else{
32              # we lezen deze afkorting voor de eerste keer:
33              $htab{$1}=$_;
34          }
35      }
36  }
37  close FD;
38  #
39  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
40  sub help{
41          print finddup -- Zoek afkortingen met meerdere betekenissen in het
42  bestand abb.txt. De regels in dit zijn van het formaat:
43  afkorting betekenis
44  \n";
45          exit;
46  }
47  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
48  __END__
Dit programma is hier te vinden.

Hoe werkt het? We lezen het bestand regel voor regel en slaan de regels op in de Hash %htab (zie regel 33). De index van de Hash is de afkorting. Bij elk nieuw element in de Hash wordt gecontroleerd of de index nog niet bezet is. (regel 18). Als er al iets staat, zijn er twee mogelijkheden:

  1. Dit is de eerste dubbele
  2. We zijn al meerder dubbelen van deze afkorting tegengekomen
Om beide mogelijkheden te onderscheiden schrijven we het woord "_herhaald_" als waarde in de Hash om aan te geven dat we al een dubbele gevonden hebben (regel 29).

Het kan handig zijn om de code te downloaden en er wat mee te spelen.

Wat in Deel III?

Na dit tweede deel ken je al wat details van de Perl taal, maar we hebben nog niet alle data types gezien. Ondertussen vraag je je misschien af of het verplicht is om de bestandsnaam (bv. abb.txt) altijd vooraf zelf in te typen. Je weet al dat je de bestandsnaam kan meegeven als een opdrachtregel optie (b.v. zoekdubbel -f abb.txt). Probeer het programma zo eens aan te passen! In het volgende deel bespreken we het datatype array en leren we meer algemeen gebruik te maken van de opdrachtregel.