Perl -- Practical Extraction and Report Language
Perl je programovací jazyk, který byl původně určen pro zpracování
textových souborů, provádění textových manipulací nad nimi a tisk složitých
výstupů, něco jako rozšířený sed
či awk
. Jeho schopnosti jsou ale nyní mnohonásobně širší. Je vhodný při
nejrůznějších úkolech, které se objevují při správě systému a sítě, je to
jazyk, ve kterém je psáno nejvíce cgi skriptů, postupně přibývají moduly
pro práci s daty z nejrůznějších oblastí, například interface k vývojovým
prostředím či databázím.
Syntaxe v sobě sdružuje prvky C, sed
, awk
či shell. Většinu věcí je možno psát intuitivně bez dlouhého rozmýšlení,
protože konstrukce, na kterou jsme zvyklí, je s velkou pravděpodobností
podporována. Autor Larry Wall chtěl při návrhu jazyka dosáhnout toho, aby
se v něm daly psát hezké programy, což samozřejmě jde. Primárně je Perl
interpretačním beztypovým jazykem, ale kromě pole a asociativního pole
(hash) zde jsou i třídy a moduly.
Domovskou stránku jazyka Perl najdeme na Webu na adrese http://www.perl.com/. Zde jsou informace o tom, jak Perl získat, jsou zde FAQ i například přehled dostupných perlovských modulů. Ty je možné získat z CPANu (Comprehensive Perl Archive Network), který je mirrorován na mnoha místech na světě, na Fakultě informatiky na URL http://www.fi.muni.cz/ftp/pub/perl/.
Tento text má sloužit jako stručný průvodce po možnostech jazyka Perl, nikoli jako referenční příručka či manuálová stránka. Snažil jsem se zahrnout co možná nejvíce příkladů, na kterých bych mohl popisované vlastnosti ilustrovat. Pokud hledáte uspořádanější výklad, zkuste man perl, man perlfaq, http://www.perl.com/, http://www.perl.org/, newsovou skupinu comp.lang.perl.misc či některou z knih nakladatelství O'Reilly http://oreilly.com/.
Příklady zde uvedené většinou demonstrují pouze jeden rys Perlu, nemají často ošetřeny chybové stavy a netestují návratové hodnoty, proto je velmi pravděpodobné, že by se v nestandardních podmínkách chovaly jinak, než jak je popsáno. Proto je prosím berte jako upozornění na popisovanou vlastnost jazyka a před nasazením v set-uid programu řádně ošetřete. Zařadil jsem hodně komentářů, které vysvětlují používané Perlovské obraty a mohou nasměrovat čtenáře k dalším podrobnostem v manových stránkách.
V celém textu tiše předpokládám verzi 5.004 (a více).
Jednoduché skripty
Začneme tradičně:
$ perl -e 'print "Hello, world\n";'
na příkazovém řádku shellu dá očekávaný výstup. Dolar na začátku je prompt
shellu, potom spouštíme program perl. Říkáme mu, že má provést program,
uvedený na příkazovém řádku (-e
= expression). Program (skript) můžeme také uložit to souboru:
$ cat test.pl print "Hello, world\n";
a potom perl test.pl bude provádět obsah tohoto souboru. Pokud na prvním řádku našeho perlového
skriptu uvedeme cestu k programu perl (#!/usr/bin/perl
) a přidělíme skriptu práva pro provádění (chmod +x test.pl), pak příkazem test.pl spustíme
/usr/bin/perl se jménem našeho skriptu jako prvním parametrem. Perl může být v lokální
instalaci uložen i jinde, pak je potřeba cestu k němu odpovídajícím
způsobem změnit. Pokud spustíme pouze perl bez uvedení jména skriptu, bude
očekávat řádky skriptu na standardním vstupu.
Následující skript funguje jako jednoduchá verze programu grep:
#!/usr/bin/perl $search = shift @ARGV; while (<>) { print if /$search/; }
Na řádku 2 jsme do proměnné $search
uložili nultý prvek pole
@ARGV, které jsme zároveň posunuli o jeden prvek doleva. Z toho vyplývá, že
jména skalárních proměnných začínají znakem dolar. Skalární proměnné mohou
obsahovat čísla či řetězce (a několik dalších typů), numerická či textová
interpretace hodnoty závisí na kontextu. Jednorozměrné pole se označuje
znakem zavináč (@
), jeho prvky indexujeme pomocí hranatých závorek [
, ]
a pokud neřekneme jinak, indexuje se od nuly. U jmen proměnných se
rozlišují velká a malá písmena a stejné jméno je možné použít zároveň jak
pro skalár, tak pro pole, tak pro jiný typ.
Speciální pole @ARGV
obsahuje parametry z příkazového řádku,
tedy nyní je v proměnné $search
uložen první parametr a v
@ARGV
zbývající. Příkaz while
je cyklus a konstrukce <>
říká čti řádek ze souborů, které jsou uvedeny jako parametry, pokud žádné
nejsou, čti ze standardního vstupu. Načtený řádek je v příkazu while
uložen do speciální proměnné $_
a čtení <>
vrací true, dokud jsou nějaká data na vstupu. Přesněji, čtení vrací načtený
řádek, který je považován za pravdivý, pokud je nenulový. Perl nemá
booleovský typ, číslo různé od nuly případně neprázdný řetězec jsou chápany
jako hodnota true, nula a prázdný řetězec jako false.
Příkaz print bez parametrů vytiskne obsah $_, ale v našem případě pouze
tehdy (if
), pokud /$search/
vrátí true. Konstrukce /.../
znamená vyhledávání regulárního výrazu a pokud není uvedeno jinak,
prohledává se retězec v proměnné $_.
Když teď náš skript spustíme s parametry if
a grep.pl
:
$ chmod +x grep.pl $ grep.pl if grep.pl
říkáme, že chceme všechny řádky z našeho programu, které obsahují podřetězec if:
$search = shift @ARGV; print if /$search/;
Skript jsme také mohli volat grep.pl if < grep.pl
, pak by konstrukce <>
automaticky četla ze standardního vstupu.
Skript můžeme pochopitelně přepsat do tvaru, který bude méně mást programátory zvyklé na jazyk C:
#!/usr/bin/perl $search = $ARGV[0]; shift @ARGV; while ($line = <>) { if ($line =~ /$search/) { print $line; } }
Zde si nejprve uložíme do proměnné $search
nultý prvek pole
@ARGV. Když přistupujeme k prvkům pole, jsou to již skaláry, a proto je
potřeba psát $ARGV[0] s dolarem na začátku. Poté provedeme posun pole
parametrů. Následuje cyklus, zde ale explicitně přiřazujeme do proměnné
$line. A v testu if
operátorem =~
říkáme, který řetězec ($line) se má prohledávat.
Vedle příkazu /.../
existuje i s/.../.../
jako substitute, tedy náhrada podřetězce. Příkaz
$ perl -e 'while (<>) { s/:M012:3:/:M012:4:/; print; }'
provede ve všech řádcích vstupu změnu trojky na čtverku (v daném kontextu :M012:3:
).
Perl akceptuje množství parametrů na příkazovém řádku, například
$ perl -ne 's/:M012:3:/:M012:4:/; print;'
je ekvivalentní předchozímu příkladu, stejně tak jako
$ perl -pe 's/:M012:3:/:M012:4:/;'
Parametry -p
(print) a -n
(no print) obalí skript cyklem
while (<>) { }
-p
navíc ještě na konci cyklu automaticky provede tisk $_. Výše uvedený grep.pl
můžeme jednorázově zapsat i jako
$ perl -ne 'print if /reg_výraz/;'
kde regulární výraz uvedeme přímo do těla příkazu, nikoli jako argument skriptu.
Dalším užitečným parametrem, který v této části zmíníme, je -i (in place).
Vztahuje se na vstupní soubory zpracovávané konstrukcí
<>
, Perl každý z nich nejprve přejmenuje a výstup směruje do původního
souboru. Můžeme také zadat příponu pro vytváření záložních kopií. Pokud
bychom chtěli provést výše uvedenou substituci ve všech souborech
v aktuálním adresáři, použili bychom v shellu příkaz jako
$ while i in * ; do i sed 's/:M012:3:/:M012:4:/' $i > $i.new ; mv $i.new $i ; done
zatímco v perlu nám stačí
$ perl -i -pe 's/:M012:3:/:M012:4:/;' *
a navíc máme zachována přístupová práva a startovali jsme na to celé pouze
jeden proces. Na tomto místě velmi doporučuji nejprve skript vyzkoušet bez -i
a prohlédnout si vygenerovaný výstup, protože bez chyb píše jednořádkové
kódy jen málokdo a Perl Váš soubor opravdu přepíše.
Ukažme si nyní, jak můžeme očíslovat řádky ze vstupního souboru:
$ perl -ne 'print ++$i, ": $_";'
Pro každý řádek vstupu (to říká volba -n) se provede příkaz v apostrofech
(volba -e). Ten obsahuje pouze print, který má dva argumenty: proměnnou $i, kterou nejprve zvýší o jedna,
a text, který se má vytisknout. Proměnná $i je na začátku nedefinovaná, což
je v numerickém kontextu to samé jako nula. Text je uzavřen v uvozovkách,
proto se v něm expandují proměnné, v našem případě $_. Ta obsahuje načtený
řádek vstupu. Všimněme si, že za ni při tisku nepřidáváme znak \n
. Perl totiž standardně znak oddělující řádky neořezává, proto ho také
nemusíme dávat zpět. Pokud vyznačíme řetězec uvozovkami, provede se v něm
expanze proměnných, takže místo : $_
se vytiskne dvojtečka a obsah proměnné $_. Pokud chceme expanzi zabránit,
použijeme apostrofy, na příkazové řádce shellu bychom je ale museli
opentlit backslashi. K expanzi se v textu ještě vrátíme.
Následuje variace na to samé téma:
$ perl -ne 'BEGIN { $i = 1; } printf "%d: %s", $i++, $_;'
Zde jsme použili konstrukci BEGIN
. Ta se provede vždy před zbytkem skriptu, a i u volby -n nebo -p se
provádí ještě před první iterací.
Poslední možnost využívá parametr -p:
$ perl -pe 'print ++$i, ": ";'
Námi dodaná funkce print vytiskne číslo řádku a dvojtečku, bez znaku konce řádku, o tisk zbytku řádku se postará perl sám.
Regulární výrazy
Perl kromě základních regulárních výrazů definuje i celou řadu rozšíření.
Například \w
označuje jakýkoli symbol, který může být součástí slova. Tedy například
$ perl -e '$_ = <>; s/(\w)/$1:/g; print;'
nejprve načte jeden řádek ze vstupu do proměnné $_
a potom za
každý znak, který může být součástí slova (písmeno, číslice, podtržítko),
přidá dvojtečku. Symbol $1
je naplněn hodnotou, která byla
nalezena mezi prvním (zde jediným) párem kulatých závorek. Modifikátor /g
říká, že touto náhradou má postupně projít celý řetězec. Pokud není uvedena
konstrukce =~
, pracuje operátor
s//
standardně nad proměnnou $_:
$ perl -e '$_ = <>; s/(\w)/$1:/g; print;' General protection G:e:n:e:r:a:l: p:r:o:t:e:c:t:i:o:n:
Za každým písmenem je dvojtečka, za mezerou nikoli. Výhoda \w
oproti zdánlivě ekvivalentní konstrukci s/([A-Za-z0-9_])/$1:/g;
spočívá v tom, že pokud použijeme v programu direktivu
use locale;
pak interpretuje sprvávně nastavené locales. Takže správné výsledky dává i
$ echo $LC_CTYPE cs $ perl -e 'use locale; $_ = <>; s/(\w)/$1:/g; print;' maličký ježeček m:a:l:i:č:k:ý: j:e:ž:e:č:e:k:
a můžeme si ověřit, že pokud nastavíme locales na defaultní hodnotu, výstup se patřičně změní:
$ ( LC_CTYPE=C; perl -e 'use locale; $_ = <>; s/(\w)/$1:/g; print;' ) maličký ježeček m:a:l:i:čk:ý j:e:že:če:k:
V US-ASCII nejsou česká písmena s diakritikou považována za písmena, proto
se za nimi dvojtečka neobjeví. Zde je nutné poznamenat, že ve verzi 5.003
respektovala konstrukce \w
locales bez nutnosti zadávání use locale
, ale od verze 5.004 tato vlastnost bohužel není podporována :-(
Pozitivní naproti tomu je, že při use
locale
a správně nainstalovaných locales se tyto uplatní na všech místech, kde to
očekáváme.
Nyní zadání změníme: chceme vložit dvojtečku pouze dovnitř slov, nikoli
i za ně. Zkusíme-li dát hledání dvou písmen vedle sebe
s/(\w)(\w)/$1:$2/g;
, není výsledek příliš uspokojivý:
m:al:ič:ký j:ež:eč:ek
. Při vyhledávání jsou totiž nalezeny dva znaky (\w)(\w)
, ty jsou správně uloženy do proměnných $1
a $2
a mezi ně vložena dvojtečka, ale při dalším průchodu prohledávání začíná za
až druhým znakem. Použijeme-li perlovou rozšířenou syntaxi s/(\w)(?=\w)/$1:/g;
, bude už vše správně:
m:a:l:i:č:k:ý j:e:ž:e:č:e:k
. Zde (?=reg_výraz)
znamená dopředné prohledávání s nulovou šířkou, které neovlivní další
průchod. Jiná možnost je s/(\w)\B/$1:/g;
se symbolem \B
, který označuje bezrozměrnou nehranici mezi slovy (hranice slova je pak \b
).
Ukažme si nyní program, který ze zdrojového textu jazyka C odstraní komentáře.
#!/usr/bin/perl -w while (<>) { $longer and s!^.*?\*/!! and $longer = 0; $longer and $_ = ''; s!/\*.*?\*/!!g; s!/\*.*!! and $longer = 1; print; }
Parametrem -w
říkáme perlu, že chceme, aby nás informoval o podezřelých konstrukcích
v našem programu a možných problémech. U delších programů se velmi
doporučuje ho používat a význam má i u takto krátkého skriptu. Povšimněme
si, že zde nahrazujeme pomocí
s!...!!
. Pokud bychom použili místo vykřičníku lomítko, museli bychom skutečné
lomítko céčkového komentáře opentlit backslashem. Perl proto dovoluje jako
oddělovač v substituci cokoli jiného. Je možno použít i závorky a pak
píšeme intuitivně levou a pravou, například s(...)[...]g;
je naprosto v pořádku. Pro úplnost dodejme, že i při vyhledávání pomocí /reg_výraz/
tuto možnost máme také, pak je ale nutné uvést písmeno m
(match), které se jinak při použití lomítek může vynechat: m#reg_výraz#
.
Za zmínku stojí i zápis .*?
. Běžné .*
totiž vyhledá nejdelší odpovídající řetězec, což my zde nepotřebujeme.
Pokud by totiž v našem C-programu byly řádky
/* Poznámka */ /*/ Další poznámka */
a my použili normální hladový přístup, hledání prvního řetězce by se
zastavilo až u /*/
a my bychom našli celý řetězec
/* Poznámka */ /*/
a druhý komentář bychom už neodstranili. Přidáme-li otazník, je vždy
nalezen nejkratší odpovídající podřetězec, což se nám velmi často velmi
hodí.
V úvodu jsem sliboval podobnost se syntaxí jazyka C a nyní zde vidíme
Pascalské and
. To není omyl, &&
můžeme pochopitelně použít také, s jedinou výhradou: and
(a ostatní slovní logické spojky) mají velmi nízkou prioritu. Proto při
jejich používání neriskujeme, že by se vyhodnocovaly v rámci příkazů, které
spojují. Pokud bychom například první dvě and
nahradili &&
, perl by si postěžoval Can't modify logical and in scalar assignment
, protože přiřazení má nižší prioritu než logické &&
a my bychom se tímto snažili do &&
přiřadit nulu, což nejde. Samozřejmě, že se tomu můžeme vyhnout i tím, že
výraz patřičným způsobem ozávorkujeme nebo ho přepíšeme do tvaru if {...} elsif {...}
else {...}
.
Skript neošetřuje výskyt komentářů v řetězcích --- to ponechávám za domácí cvičení.
Perl má velmi málo omezení, co se týče velikosti zpracovávaných dat. Obsah celého dvoumegabajtového UNIXového jádra můžeme načíst do jednoho řetězce a jsme při tom omezeni v podstatě jen dostupnou pamětí a swapem. Výše uvedený comment stripper může mít i takovouto podobu:
#!/usr/bin/perl -w undef $/; $a = <>; $a =~ s!/\*.*?\*/!!gs; print $a;
Proměnná $/ je ekvivalent RS
v awk
, určuje, čím jsou odděleny vstupní řádky. Defaultně je nastavena na \n
, můžeme ji libovolně změnit (třeba na velké A, pokud bychom to
potřebovali), a můžeme jí také dát nedefinovanou hodnotu. Perl potom nemá
jak vstup do řádků rozdělit a proto ho načte jako jeden dlouhý řetězec. Nad
ním nám potom stačí jediný příkaz a pak zbývá už jen tisk. Při pozornějším
zkoumání objevíme znak s
na konci substituce, ten říká, že se má celý řetězec brát jako jeden řádek.
Pole a hashe
Následující příklad ukazuje využití polí a asociativních polí. Chceme-li spočítat slova ve vstupním textu, můžeme použít tento cyklus:
while ($line = <>) { @a = split /\W+/, $line; $total += @a; } print "Slov celkem: $total\n";
Na druhém řádku přiřazujeme do pole @a seznam, vrácený funkcí
split
. Ta rozdělí obsah řetězce $line, přičemž oddělovači jsou nenulové sekvence
nepísmen (opak \w
). V @a
jsou potom slova ze vstupního řádku. Pole @a nyní
přičítáme k proměnné $total. To, že sčítáme proměnné různých typů (pole
a skalár) vůbec nevadí, naopak. Perl zjistí, že nalevo je skalár a převede
pole na počet jeho prvků, tedy počet slov v řádku. Přetypování na skalár je
možno explicitně provést i pomocí scalar(@s)
.
Při používání možná objevíme jednu vadu na kráse: pokud řádek začíná
mezerou (či jiným neznakem), split
považuje prázný podřetězec před ní za slovo, které (byť má nulovou délku)
se promítne do počtu slov. Můžeme to opravit například tak, že k
$total
přičteme jen počet těch prvků, které jsou nenulové.
Třetí řádek přepíšeme na
$total += grep /./, @a;
kde grep
vezme na vstupu pole @a a vrátí z něho jen ty prvky, které obsahují aspoň
jeden znak. Prvky jsou grep
u předávány v proměnné $_
a ten je propustí pouze pokud
operátor vyhledání regulárního výrazu uspěje, vrátí true. Tento nový seznam
je potom převeden na skalár (počet prvků) a tato hodnota je přičtena
k proměnné $total.
Nyní bychom chtěli vědět, kolikrát se které slovo v textu vyskytuje. K tomu
využijeme slibovaný hash. Hash je pole, jehož prvky nejsou indexovány svou
pozicí (od nuly) jako běžné pole, ale řetězcem. Celý hash se označuje
procentem (například %names), jeho jednotlivé prvky jsou potom skaláry,
tedy začínají znakem dolar, indexy jsou uzavřeny ve složených závorkách:
$firstname{``login''} je prvek asociativního pole indexovaný řetězcem login
, $firstname{$login} je prvek indexovaný hodnotou proměnné $login.
Nyní můžeme přistoupit k vlastnímu programu. V cyklu while
čteme řádky ze vstupu do proměnné $_, následný split
nemá uvedenou vstupní proměnnou, takže defaultně předpokládá $_, a vrací
seznam slov jako v předchozím příkladu. Příkazem for
nyní projdeme tímto seznamem, postupně jsou jeho všechny prvky přiřazeny do
proměnné $word. Na dalším řádku testujeme, jestli je obsah
$word
neprázdný, pokud ano, zvýšíme počet v příslušném prvku
asociativního pole o jedna.
while (<>) { for $word (split /\W+/) { $counts{$word}++ if $word; } } for (sort keys %counts) { print "$_: $counts{$_}\n"; }
Jsme tedy v polovině skriptu a máme polovinu práce hotovou: asociativní
pole %counts
obsahuje pro každé slovo, které se v textu
objevilo, počet jeho výskytů. Nyní tento hash projdeme a vytiskneme vždy
slovo (index v hashi) a počet (příslušnou hodnotu). Seznam všech indexů
dostaneme funkcí keys
, ten ještě setřídíme sort
em. Opět použijeme cyklus for
, nyní neuvádíme proměnnou, takže se předpokládá $_.
Můžeme ale chtít víc. Například setřídit slova od nejčetnějších po nejméně
častá. Funkci sort
můžeme říci, podle jakého kritéria (nevyhovuje-li nám defaultní) má třídit.
Pokud změníme druhou část programu na
for (sort {$counts{$b} <=> $counts{$a}} keys %counts) { printf "%5d: %s\n", $counts{$_}, $_; }
udělá sort
následující: do proměnných $a a $b
dosadí vždy porovnávané
hodnoty. Standardně by provedl ekvivalent $a cmp $b
, tedy textové porovnání. My chceme porovnávat numericky (<=>
), a neporovnáváme indexy do pole (prvky keys
), ale odkazované hodnoty ($counts{$a}). Navíc jsme prohodili pořadí, takže
nejvyšší budou první. A na závěr jsme použili formátování pomocí
printf
místo obyčejného print
, takže počty jsou pěkně zarovnány doprava. (Návyky z céčka se zde rozhodně
neztratí.)
Jiný příklad: chceme zjistit, zda se v souboru /etc/passwd
nevyskytují uživatelé se stejným loginem vícekrát.
#!/usr/bin/perl open FILE, "/etc/passwd" or die; while (<FILE>) { chomp; ($login, $pw, $uid, $gid, $name, $home, $sh) = split /:/; if (defined $passwd{$login}) { print "$passwd{$login} x ", "$login: $name, uid $uid, shell $sh"; } else { $passwd{$login} = "$name, uid $uid, shell $sh"; } } close FILE;
Na začátku otevíráme soubor /etc/passwd
pro čtení. FILE
je filehandle, kterým se dále budeme na takto otevřený soubor odkazovat.
Pokud bychom chtěli naopak do nějakého souboru zapisovat, dali bychom na
začátek jeho jména znak >
, podobně jako v shell
u, pomocí |
(svislítka) můžeme nastartovat další process a zapisovat do roury:
open MAIL, "| mail $user"
Pokud open
neuspěje, zavolá se die
, předčasné vyskočení z programu. V cyklu čteme z otevřeného souboru,
vstupy se ukládají jak jsme zvyklí do $_. Funkce chomp
odřízne z konce řádku znak
\n
, případně znak nastavený v proměnné $/. Často se můžeme setkat i s
variantou chop
, která při odřezávání posledního znaku netestuje, zda se jedná o oddělovač
vstupních řádků.
Dále provedeme split
vstupu podle dvojteček, výsledný seznam přiřadíme do seznamu proměnných
uvedených vlevo. Podíváme se, je-li již daný login v hashi %passwd
zařazen (defined $passwd{$login}
), pokud ano, vytiskneme hlášení o nalezené shodě, pokud ne, uložíme jméno,
číslo uživatele a jeho shell do hashe pod jeho loginem.
Pokud bychom tento skript pustili s paramatrem -w
, dostali bychom několik hlášení o proměnných použitých jen jednou (např.
$pw
).
Příkaz die
, použitý výše, vypíše na standardní chybový výstup (STDERR
)
Died at test_passwd line 2.
Můžeme mu ale dát i svůj vlastní komentář, například
open FILE, "/etc/passwd" or die "Error reading passwd file";
nám při chybě vypíše
Error reading passwd file at test_passwd line 2.
Vidíme, že Perl automaticky připojuje informaci o jménu skriptu a řádku.
Pokud nám to nevyhovuje, přidáme na konec námi specifikovaného hlášení znak
nového řádku (\n
) a tím dáme najevo, že tuto informaci nepožadujeme. Další obrat často
používaný v této souvislosti je proměnná $!, která obsahuje číslo chyby
(hodnotu
errno
), ale v kontextu řetězce dostaneme chybové hlášení:
open FILE, "/etc/passwd" or die "Error reading file: $!\n";
Error reading file: No such file or directory
Následující skript je jednoduchým ekvivalentem UNIXového příkazu
pwconv
. Ten má za úkol odstranit ze souboru /etc/passwd
zakódovaná hesla a přesunout je do souboru /etc/shadow
, který je pro svět nečitelný. Pozor: tento skript přepisuje základní
systémové soubory! Snažil jsem se, aby v něm nebyly chyby, ale radši ho
podrobte pečlivé kontrole, než ho spustíte s rootovskými právy.
#!/usr/bin/perl -w $PASSWD = "/etc/passwd"; $SHADOW = "/etc/shadow"; open(SHADOW, $SHADOW) || print STDERR "Error reading $SHADOW: $!"; while (<SHADOW>) { ($login) = split /:/; $shadow{$login} = $_; } close SHADOW; open(PASSWD, $PASSWD) || die "Error reading $PASSWD"; umask 0133; unlink "$PASSWD.$$"; open(OUT, "> $PASSWD.$$") || die "Error writing $PASSWD.$$"; umask 0377; unlink "$SHADOW.$$"; open(SHADOWOUT, "> $SHADOW.$$") || die "Error writing $SHADOW.$$"; while (<PASSWD>) { ($login, $passwd, $rest) = split /:/, $_, 3; print OUT "$login:x:$rest"; $sh = "$login:${passwd}:::::::\n"; $sh = delete $shadow{$login} if (defined $shadow{$login}); print SHADOWOUT $sh; } close SHADOWOUT; close OUT; close PASSWD; rename "$PASSWD.$$", "$PASSWD"; rename "$SHADOW.$$", "$SHADOW"; if (%shadow) { print STDERR "Removed from shadow ", join (", ", keys %shadow), "\n"; }
Nebudeme zde rozebírat program krok za krokem, zaměříme se jen na věci které jsou nové a mohou být zajímavé.
Proč je $login
na řádku ($login) = split /:/;
v závorkách? Napravo od rovnítka je seznam, který vrátí příkaz split
. Pokud bychom vlevo měli skalární proměnnou bez závorek, byl by tento
seznam převeden na skalár a do proměnné $login
by se dostal
počet prvků, nikoli první prvek pole. Tím, že jsme jej obalili závorkami,
jsme nalevo vytvořili seznam, a seznam do seznamu můžeme přiřadit. Seznam
vlevo má jen jeden prvek, přiřadí se tedy jen první.
Druhý split
voláme se třemi parametry, ten poslední je počet, na kolik chceme řádek
rozdělit. Kdybychom zde tu trojku neměli, vytvořil by se napravo seznam o
osmi prvcích, z nichž první tři by se přiřadily a o ostatní bychom přišli.
V cyklu při čtení z /etc/passwd
jsme nepoužili chomp
. V tomto případě by to bylo zbytečné, protože bychom znak nového řádku
stejně přidávali zpět. Proměnná $$ obsahuje stejně jako v shell
u číslo procesu a často se tudíž používá k vytvoření jednoznačného jména
pomocného souboru. Funkce delete
odstraní a vrátí prvek z asociativního pole. Funkce join
spojí prvky seznamu uvedeným řetězcem, je to opak funkce split
. Funkce
umask
, unlink
a rename
jsou ekvivalenty systémových funkcí.
Ve speciálním hashi %ENV
jsou uloženy proměnné prostředí
procesu. Funkci příkazu set
můžeme napodobit skriptem:
for (sort keys %ENV) { print "$_=$ENV{$_}\n"; }
V cyklu for
se do proměnné $_
přiřadí jméno proměnné (klíč v asociativním
poli %ENV), print
em pak tiskneme nejprve tento klíč a za rovnítkem odpovídající hodnotu
z hashe.
Následujcící skript je jednoduchou verzí příkazu whereis
. V adresářích, uvedených v proměnných $PATH
a $MANPATH, najde
všechny soubory, které odpovídají prvnímu argumentu.
(@ARGV) || die "Usage: whereis substr\n"; $string = shift; @path = split /:/, $ENV{PATH} if defined $ENV{PATH}; push @path, split /:/, $ENV{MANPATH} if defined $ENV{MANPATH}; @path = map { $_ eq "" ? "." : $_ } @path; unless (@path) { die "No PATH or MANPATH found\n"; } for $dir (@path) { next unless -d $dir; for (<$dir/$string*>) { print "$_\n"; } }
Na prvním řádku testujeme, jestli jsou uvedeny nejaké argumenty. Pokud ano,
uložíme první z nich příkazem shift
do proměnné $string. V následujících třech řádcích programu uložíme do pole
@path
adresáře posbírané z proměnných $PATH
a
$MANPATH. Test if defined
$ENV{PATH}
by zde být nemusel, protože split
na nedefinované hodnotě vrátí prázdný seznam. Zajímavý je push
, který přidá seznam, vrácený split
em na konec uvedeného pole. Existuje i jeho opak pop
, a také opak shift
u
unshift
.
Následuje map
, který provede uvedenou operaci postupně nad všemi prvky pole, výsledný
seznam přiřazujeme zpět do pole @path. Používáme zde ternárního operátoru,
který změní prázdné řetězce na tečky, hodnoty jednotlivých prvků pole
přicházejí v proměnné $_. Toho využijeme v dalším --- /bin::/usr/bin
vlastně znamená
/bin:.:/usr/bin
a pokud pak budeme testovat existenci souboru
adresář/jméno
, je ./jméno
korektní, zatímco /jméno
znamená soubor v kořenovém adresáři.
Pokud jsme ze žádné z obou proměnných nebyli schopni získat ani jedno jméno
adresáře, skončíme se smutným hlášením. Jinak tento seznam adresářů
projdeme cyklem for
. Pokud adresář zadaného jména neexistuje (test -d
jako v shell
u, existuje i -f
, -r
,
-M
a mnoho dalších), provede se next
, skok na další iteraci cyklu. Jinak se vykoná konstrukce <$dir/$string*>
, která rozexpanduje jména souborů. Nalezené soubory vytiskneme.
Možná zdokonalení? Program by mohl akceptovat více argumentů a vypsat nalezené soubory pro každý argument zvlášť. Také ze seznamu adresářů odstraníme vícenásobné výskyty jednoho jména:
(@ARGV) || die "Usage: whereis substr\n"; @path = grep { -d $_ } grep { !$uniq{$_}++ } map { $_ eq "" ? "." : $_ } (split(/:/, $ENV{PATH}), split(/:/, $ENV{MANPATH})); unless (@path) { die "No PATH or MANPATH found\n"; } for $string (@ARGV) { print "$string: ", join(" ", grep { -f $_ } map { (<$_/$string*>) } @path), "\n"; }
Na druhém až čtvrtém řádku provádíme manipulaci se seznamem, který sestává
z výsledku činnosti dvou split
ů. Můžeme si to představit jako rouru v shellu, kde předáváme výsledek
jednoho procesu dalším, pouze zde data putují odprava, přes map
a grep
y, aby mohla být nakonec přiřazena do pole.
Tedy seznam adresářů prochází map
em, který změní prázdné řetězce na tečku a vrací seznam, který jde do grep
u. Ten zde nedělá hledání v řetězci, jak jsme viděli výše, ale provede blok
a řídí se podle jeho výsledné hodnoty. Hodnotou bloku je výsledná hodnota
posledního provedeného příkazu. Zde je jen jeden a ten otestuje, jaká
numerická hodnota je přiřazena danému jménu adresáře v hashi %uniq. Pokud
je toto první výskyt v seznamu, byla hodnota nulová, a tudíž negace vrátí
true a prvek bude propuštěn. Předtím je ale jeho výskyt zaznamenán
inkrementem. Poslední grep
v řadě zamezí testování adresářů, které vůbec neexistují.
Manipulace se seznamy se opakuje i v druhé části skriptu. Upozorníme zde na
blok u map
u, který podle hodnoty $_
a $string
najde soubory
v daném adresáři --- k jedné hodnotě, jménu adresáře, může map
poslat na výstup seznam několika výstupních prvků, nejen jeden (či naopak
žádný). Závorka u join
je nutná proto, aby se "\n"
stal argumentem příkazu print
a nebyl zpracován pomocí map
. Čtenář jistě již tuší, že otevírací závorka by se mohla se stejným
efektem vyskytovat i před či za grep
em nebo před či za příkazem map
.
Funkce
V perlu můžeme definovat svoje uživatelské funkce. Typický postup je:
sub sum { my $total; for (@_) { $total += $_; } $total; } print sum(3, 7, 8), "\n";
Definice funkce začíná klíčovým slovem sub
, za kterým následuje její jméno a ve složených závorkách tělo funkce.
V něm deklarujeme lokální proměnnou $total
pomocí my
(lexikální deklarace). Taková proměnná se chová stejně jako auto proměnná
v jazyce C, je viditelná pouze ve svém bloku a blocích vnořených. Existuje
i tzv. dynamický scoping, který uloží starou hodnotu globální proměnné
a při návratu z funkce (bloku) ji zase obnoví, deklaruje se pomocí local
.
Proměnné jsou defaultně brány jako globální, což je výhoda u malých
a krátkých programů, u velkých programů to může vést k nechtěným výsledkům,
a proto se doporučuje používat pragmu use strict
, která na takové případy upozorní.
Cyklem for
projdeme pole @_, ve kterém se funkcím předávají argumenty. Návratovou
hodnotou funkce je hodnota posledního výrazu, v našem případě součet
$total. Funkci můžeme také ukončit příkazem
return
.
Parametry se funkcím předávají odkazem. Změníme-li tedy hodnotu v @_, změní se tím i hodnota ve volající funkci:
@a = (2, 4, 6); sub inc { for (@_) { $_++; } } inc(@a); print join(",", @a), "\n";
výsledek bude 3,5,7
. Pokud bychom si ale argumenty funkce uložili do pomocného lokálního pole,
budeme už pak pracovat s jejich kopiemi.
sub inc { my @data = @_; for (@data) { $_++; } @data; }
a proměnné v globálním @a zůstanou nezměněny. Návratovou hodnotou naší funkce je seznam z pole @data, a s tímto seznamem můžeme ve volající funkci libovolně manipulovat, například přiřadit do pole:
@b = inc (@a); print join(" ", @b), "\n";
Stejně jako se v uvozovkách expandují skalární proměnné, je možno expandovat i pole:
print "@b\n";
je ekvivalentní předchozímu příkladu --- výsledkem jsou všechny prvky pole oddělené mezerou, resp. hodnotou proměnné $``.
Volby příkazové řádky perlu
Následuje seznam parametrů, které perl akceptuje na příkazové řádce. Je
možné je spojovat, například místo -l -a -n -e můžeme psát -lane
. Bližší popis man perlrun.
- -0číslice
- Oktalové číslo začínající nulou, které určuje znak pro oddělovač řádků, resp. vstupních záznamů ($/). Speciální hodnoty jsou 00 pro odstavcový mód (jeden či více prázdných řádků), 0777 znamená čtení celého vstupu naráz.
- -a (autosplit)
-
Při použití s -n nebo -p dělá automaticky
split
vstupu do pole @F. Defaultně rozděluje na mezeře, je možno změnit pomocí -F. - -c (check)
-
Perl načte skript a provede pouze syntaktickou kontrolu, skript nespouští
až na nezbytné
BEGIN
,END
ause
. - -d (debug)
- Spustí perl debugger.
- -D číslo nebo -D seznam (debugging flags)
-
Číslo nebo písmena v seznamu říkají, jaké ladící informace se budou
zobrazovat. Perl musí být přeložený s volbou
-DDEBUGGING
. - -e příkaz (execute)
-
Na místě příkazu je řádka perlového skriptu. Musí obsahovat správně
středníky. Více řádek se zadá dalšími volbami
-e
. Je vhodné obalit příkaz apostrofy, abychom předešli expanzi metaznaků shellem. Je-li na příkazovém řádku volba-e
, perl pak už nehledá jméno skriptu. - -Fregulární výraz
- Regulární výraz pro autosplit pomocí -a.
- -h
- Vytiskne seznam voleb příkazové řádky.
- -ipřípona (in place)
-
Soubory zpracovávané konstrukcí
<>
budou změněny a zůstanou na místě. Pokud uvedeme příponu, dostanou ji staré verze souborů. - -I adresář (include files)
- Říká C preprocesoru (volba -P), kde má hledat include soubory.
- -loktalové číslo (line-end processing)
-
Při použití s -n či -p dělá automaticky
chomp
a nastavuje $\ na uvedené oktalové číslo, takže tento znak je při tiskuprint
em přidán zpět. Pokud číslo není uvedeno, nastavuje $\ na $/. - -Mmodul nebo -mmodul
-
Provede na začátku skriptu
use modul;
, resp.use modul ();
Pokud dáme ještě před jméno modulu mínus (-
), znamená tono
. - -n (no print)
-
Obalí skript cyklem
while (
, tedy skript se provede pro každý vstupní řádek. Bloky<>
) { ... }BEGIN
aEND
jsou provedeny mimo tento cyklus. - -p (print)
-
Obalí skript cyklem
while (
, tedy jako -n a navíc se automaticky ještě tiskne $_.<>
) { ... } continue { print; } - -P (preprocesor)
- Před tím, než je zpracován perlem, je skript prohnán C preprocesorem.
- -s (switch parsing)
-
Vyhledá na příkazové řádce přepínače, odstraní je z
@ARGV
a nastaví proměnné s odpovídajícími jmény. - -S (search through script)
-
Bude hledat skript podle proměnné prostředí
PATH
. Je možné použít na systémech, které perlu předají jen jméno skriptu bez cesty. - -T (taint checks)
- Zapíná přísnější kontrolu podezřelých nebo nečistých programátorských konstrukcí. Všechny vstupy z venku jsou považovány za ne bezpečné a s takovými perl nedovolí udělat ne bezpečné výstupní operace.
- -u (undump)
-
Poté, co perl náš skript zkompiluje, vypíše core. Ten můžeme příkazem
undump
převést zpět do binární executable podoby. - -U (unsafe)
- Povoluje perlu, aby dělal ne bezpečné operace.
- -v (version)
- Vytiskne verzi a patchlevel našeho perl programu.
- -V (values)
- Vytiskne hlavní konfigurační hodnoty, s nimiž byl perl zkompilován. Můžeme také použít -V:proměnná.
- -w (warnings)
- Tiskne varování o některých syntakticky méně čistých věcech ve skriptu, které mohly vzniknout překlepnutím. U rozsáhlejších programů je to základní volba.
- -x adresář
-
Odstraní vše až po první řádek začínající
#!
a obsahující slovoperl
. Adresář (je-li uveden) určuje, kam se má perl před spuštěním skriptu přepnout. Užitečné, pokud chceme spustit skript uložený v delší zprávě.
Předdefinované perlovské proměnné
Některé proměnné v perlu mají speciální význam, někdy i dost dalekosáhlý, ovlivňující jeho celkové chování při běhu našeho programu. Pomocí některých z nich můžeme snadno dosáhnout věcí, které bychom jinak museli dělat opisem a složitými konstrukcemi. Proto je dobré alespoň tušit, jaké možnosti se v nich skrývají, zbytek se dá dohledat v man perlvar.
- $_
- Proměnná asi nejzákladnější. Je to defaultní místo, kde se vyhledává, defaultní hodnota, která se tiskne, pokud neřekneme našim funkcím jinak.
- $?
-
Status vrácený posledním voláním funkce
system
, zpětnými apostrofy (``
) či uzavřením roury. - $!
-
V numerickém kontextu aktuální hodnota proměnné
errno
, v textovém kontextu aktuální systémové chybové hlášení. - $@
-
Chybové hlášení perlu z posledního příkazu
eval
, nuluje se, pokudeval
proběhl bez chyby. - $]
- Verze perlu, například 5.003.
- $$
- Číslo procesu.
- $<
- Reálné UID procesu.
- $>
- Efektivní UID procesu.
- $(
- Reálné GID procesu.
- $)
- Efektivní GID procesu.
- $[
- Index, od kterého se indexují normální pole a znaky v řetezcích, defaultně 0.
- $0
-
Název našeho programu. Pokud ji změníme, změní se text například ve výpisu
příkazem
ps
. - $1, $2, ... $číslo
- Obsahuje vyhledané podřetězce z posledního úspěšného hledání či substituce, odpovídající částem regulárního výrazu uzavřeným v závorkách.
- $&
- Ten řetězec, který byl nalezen posledním úspěšným hledáním.
- $` a $'
- Podřetězec, který předcházel a následoval v posledním úspěšně prohledávaném řetězci před a za nalezeným vzorkem.
- $+
- Text v poslední nalezené závorce posledního hledání.
- $*
-
Je-li tato proměnná nastavena na 1, předpokládá se při prohledávání
řetězce, že obsahuje více řádků, 0 říká, že řetězce obsahuje jediný řádek.
Je lepší používat volbu
/m
u jednotlivých hledání. - $.
-
Číslo aktuálního vstupního řádku posledně čteného filehandlu. Konstrukce
<>
nezavírá jednotlivé soubory, a proto se tato hodnota při čtení nového souboru pomocí<>
nenuluje. - $/
-
Oddělovač vstupních řádků, defaultně
\n
. Nastavení na''
znamená čtení po odstavcích (oddělovačem je jeden čí více prázdných řádků), pokud chceme číst celý vstup naráz, dámeundef $/
. - $\
-
Oddělovač výstupních řádků. Defaltně není nastaven, takže
print
tiskne přesně to, co mu napíšeme. - $|
- Má-li hodnotu 1, vypíná bufferování výstupu. Je vhodné např. při zápisu do roury, kdy výstup chceme dále zpracovávat.
- $,
-
Oddělovač v seznamu pro příkaz
print
, tedy řetězec, kterým se nahradí každá čárka v seznamu. Defaultně není nastaven. - $"
- Oddělovač prvků pole, které zapíšeme do řetězce uzavřeného uvozovkami. Defaultně mezera.
- $;
- Oddělovač, kterým se nahradí čárka v indexu do hashe. Takto můžeme emulovat vícerozměný hash v jednorozměrném.
- $#
-
Výstupní formát pro tisk čísel, defaultně nastaven na
%.20g
. - $%
- Aktuální číslo stránky aktuálního výstupu.
- $=
- Aktuální délka stránky aktuálního výstupu.
- $-
- Kolik řádků zbývá na stránce aktuálního výstupu.
- $~
- Název aktuálního výstupního formátu.
- $^
- Název aktuálního výstupního formátu pro hlavičku stránky.
- $:
-
Seznam znaků, na nichž je možné zalomit řádek při použití výstupního
formátu, defaultně obsahuje
\n -
. - $^A
-
Aktuální hodnota akumulátoru funkce
write
, nastavovaná funkcíformline
. - $^L
-
Určuje, jak formáty dělají formfeed, defaultně
\f
. - $^D
- Aktuální hodnota debuggovacích příznaků.
- $^F
- Nejvyšší hodnota systémového file descriptoru, běžně 2.
- $^I
- Aktuální přípona pro in-place změny souborů.
- $^P
- Interní příznak perl debuggeru.
- $^T
- Čas startu běhu skriptu, v sekundách od 1. ledna 1970.
- $^W
- Aktuální hodnota přepínače -w.
- $^X
- Jméno perl binárního souboru.
- $ARGV
-
Jméno aktuálního souboru čteného konstrukcí
<>
. - @ARGV
- Obsahuje argumenty z příkazového řádku skriptu.
- @INC
-
Seznam míst, kde perl bude hledat soubory načítané pomocí
do
,require
nebouse
. - %INC
-
Pro každý soubor načtený pomocí
do
neborequire
obsahuje k jeho jménu jeho místo uložení v systému. - %ENV
- Obsahuje aktuální nastavení proměnných prostředí.
- %SIG
- Používá se pro nastavení signal handlerů.
Perlovské funkce a klíčová slova
- Funkce pro práce s poli či skaláry
- chomp, chop, chr, crypt, hex, index, lc, lcfirst, length, oct, ord, pack, q/STRING/, qq/STRING/, reverse, rindex, sprintf, substr, tr///, uc, ucfirst, y///
- Regulární výrazy, prohledávání textových řetězců
- m//, pos, quotemeta, s///, split, study
- Numerické funkce
- abs, atan2, cos, exp, hex, int, log, oct, rand, sin, sqrt, srand
- Pole
- pop, push, shift, splice, unshift
- Seznamy
- grep, join, map, qw/STRING/, reverse, sort, unpack
- Hashe
- delete, each, exists, keys, values
- Vstupně/výstupní funkce
- binmode, close, closedir, dbmclose, dbmopen, die, eof, fileno, flock, format, getc, print, printf, read, readdir, rewinddir, seek, seekdir, select, syscall, sysread, syswrite, tell, telldir, truncate, warn, write
- Funkce pro práci s daty s udanou délkou
- pack, read, syscall, sysread, syswrite, unpack, vec
- Soubory, adresáře, filehandly
- -X, chdir, chmod, chown, chroot, fcntl, glob, ioctl, link, lstat, mkdir, open, opendir, readlink, rename, rmdir, stat, symlink, umask, unlink, utime
- Klíčová slova pro řízení běhu programu
- caller, continue, die, do, dump, eval, exit, goto, last, next, redo, return, sub, wantarray
- Klíčová slova pro definování rozsahu platnosti
- caller, import, local, my, package, use
- Další (nezatříděné) funkce
- defined, dump, eval, formline, local, my, reset, scalar, undef, wantarray
- Procesy a skupiny procesů
- alarm, exec, fork, getpgrp, getppid, getpriority, kill, pipe, qx/STRING/, setpgrp, setpriority, sleep, system, times, wait, waitpid
- Moduly
- do, import, no, package, require, use
- Objekty
- bless, dbmclose, dbmopen, package, ref, tie, untie, use
- Práce se sockety
- accept, bind, connect, getpeername, getsockname, getsockopt, listen, recv, send, setsockopt, shutdown, socket, socketpair
- Funkce pro komunikaci (System V)
- msgctl, msgget, msgrcv, msgsnd, semctl, semget, semop, shmctl, shmget, shmread, shmwrite
- Informace o uživatelích a skupinách
- endgrent, endhostent, endnetent, endpwent, getgrent, getgrgid, getgrnam, getlogin, getpwent, getpwnam, getpwuid, setgrent, setpwent
- Informace o síti
- endprotoent, endservent, gethostbyaddr, gethostbyname, gethostent, getnetbyaddr, getnetbyname, getnetent, getprotobyname, getprotobynumber, getprotoent, getservbyname, getservbyport, getservent, sethostent, setnetent, setprotoent, setservent
- Práce s časem
- gmtime, localtime, time, times