Wer jahrzehntelang programmiert, eignet sich im Lauf der Zeit mehr Tipps und Tricks an. Der Perlmeister macht heute mal seine Schatzkiste auf.
Bei jedem neuen Perl-Projekt, und ich fange jede Woche mehrere davon an, gilt es erst einmal, die Arbeitsumgebung auszustampfen. Schließlich soll später nicht ein heilloser Haufen von Spaghettiskripts herumliegen, die keiner mehr pflegen will oder wegen Erinnerungslücken auch nur kann. Auf dem CPAN stehen eine Reihe von Template-Generatoren bereit, neulich bemerkte ich App::Skeletor, das sich durch Erweiterungen in Form von Template-Modulen an die lokalen Bedürfnisse anpassen lässt. Kurzerhand schrieb ich Skeletor::Template::Quick, um das Originalmodul an meine (und hoffentlich auch anderer Leute) Bedürfnisse anzupassen, und stellte es ebenfalls aufs CPAN.
Wer seine Autoren-Info wie in Abbildung 1 gezeigt
in der Datei ~/.skeletor.yml
im Home-Verzeichnis ablegt und nach der
Installation des Template-Moduls vom CPAN
den Befehl skel Foo::Bar
aufruft, bekommt
blitzschnell eine Handvoll vordefinierter Dateien für eine neue
CPAN-Distribution in ein neues Verzeichnis Foo-Bar
eingepflanzt.
Alternativen zu Skeletor wären das Perl beiliegende Tool h2xs
oder
das CPAN-Modul Module::Starter.
Abbildung 1: Der Skeletor erzeugt Rohgerüst für ein neues CPAN-Modul |
Um zukünftige Nutzer eines neu geschriebenen Moduls, die es auf dem CPAN
finden, gleich als Mitstreiter zu rekrutieren, enthält das ebenfalls
miterzeugte, Perl-typische Makefile.PL einen Link auf das Github-Repo mit dem
Source-Code. Auf search.cpan.org
steht der Verweis dann später gleich neben
dem Link zum Herunterladen des Moduls, und der Autor freut sich auf Github über
Pull-Requests zur Verbesserung des Codes (Abbildung 2).
Abbildung 2: Auf dem CPAN-Server steht unter "Repository" ein Link zum Github-Projekt des Moduls (Bitte großen roten Pfeil auf den Github-Link in den Screenshot einfügen) |
Der Template-Generator erzeugt im neu angelegten Verzeignis alles, um gleich
loszulegen, von den Modul-Dateien (lib/Foo/Bar.pm
) bis zu Beispielskripts
wie eg/foo-bar
, über eine Testsuite (t/001Basic.t
) ist im neu erzeugten
Verzeicnis alles vorhanden. Die Codedateien verfügen nicht nur über praktische
Code-Snippets sondern geben auch Templates für Dokumentation vor. Und das ist
ganz wichtig, denn zu dokumentieren, wie man den erstellten Code benutzt,
sollte niemals nur als lästige Pflicht gelten, die man solange aufschiebt, bis
keine Zeit mehr dazu ist.
Es mag verrückt klingen, aber wenn ich neuen Code entwerfe, schreibe immer zuerst auf, wie ich mir vorstelle, dass meine Zielgruppe meine technischen Wunderwerke nutzt. Meist sind es objektorientierte Module, und vor der ersten Zeile der Implementierung schreibe ich in den Abschnitt SYNOPSIS im POD-Bereich des Source-Codes, wie die neue Klasse instanziiert wird und was nachher aufgerufene Methoden machen:
my $m = MyModule->new; $m->dosomething( 42 );
Diese leichte Übung hilft oft, ohne viel Aufwand herauszufinden, ob das erdachte Interface wirklich so eine schlaue Idee ist. Wenn es sich nur umständlich handhaben lässt oder auch nur verkehrt anfühlt, lässt es sich blitzschnell korrigieren, denn noch kein Code ist nachzubessern.
Als vor etwa zehn Jahren das Test-Driven-Development (TDD, [2]) aufkam, nach dem Entwickler immer erst einen Testfall schreiben und erst anschließend das neue Feature einfügen, sprangen viele begeistert auf den Zug auf. Im Pair-Programming enstand neuer Code, und jedem neuen Feature ging ein Testfall voraus, der vor der Komplettierung fehlschlug (roter Balken) und nach erfolgreichem Abschluss durchlief (grüner Balken). Irgendwann war aber aus dem Verfahren die Luft raus, und viele Programmierer kehrten zum alten Trott zurück. Zwei Dinge habe ich behalten: Änderungen im Code erstelle ich nach der Theorie des "minimum viable product", klopfe erstmal die gewünschte Funktion rein, teste, und nutze anschließend Refactor-Methoden, um das Projekt sauber zu halten.
Immer wenn ich einen Fehler finde und im Code berichtige, versuche ich, auch einen Testfall einzufügen, der ohne den Bugfix Alarm schlägt und nach der Behebung ruhig durchläuft. Das ist unbezahlbar, um Regressionen zu vermeiden, die unvermeidlich dann auftreten, wenn der Code komplexer oder das Projekt dem zehnten Release entgegen geht. Dann hat sich so nach dem Verfahren "steter Tropfen höhlt den Stein" mit verhältnismäßig geringem Aufwand eine erstaunlich umfangreiche Testsuite gebildet, die niemand, auch nicht der sorgfältigste Programmierer, als Ganzes ad hoc aus dem Boden stampfen könnte.
Beim Thema Entwicklungsumgebung scheiden sich bekanntlich die Geister. Der eine bevorzugt ein komfortables Monstertool wie Eclipse mit Mausbedienung, das auch noch die Programmsyntax erkennt und alle Variablen und Funktionen derart miteinander verlinkt, dass der Entwickler per Klick zwischen Definition und Verwendung auch zwischen unterschiedlichen Dateien hin- und herspringen kann.
Abbildung 3: Vier Source-Dateien und eine Testsuite offen im vim-Editor mit Reitern. |
Was praktische IDEs angeht bin ich als alter Hase nicht sehr verwöhnt, lege
aber Wert auf blitzartige Performance. Ich nutze immer noch den bekannten
vim
-Editor mit einem kleinen Trick, um zwischen mehreren Dateien eines
Projektes hin- und herzuschalten: Mit vim -p file1 file2 ...
aufgerufen,
stellt vim
alle auf der Kommandozeile übergebenen Dateien als Reiter dar,
zwischen denen man mit gt
(goto tab, nach rechts) und gT
(nach links)
wechseln kann (Abbildung 3). Damit es noch schneller geht, habe ich gt
mit
vims
map
-Kommando auf L gelegt und gT
auf H (.vimrc
auf [3]). Das kann
ich mir leicht merken, denn das kleine "h" bewegt in vim
den Cursor nach
links und das kleine "l" nach rechts, also fährt man zwischen den Reitern
einfach mit den entsprechenden Großbuchstaben hin und her. Wer so viele Dateien
gleichzeitig offen hat, kann nicht alle gleichzeitig mit "ZZ" oder ":wq"
verlassen, sondern muss :qall
eingeben oder letzteres, wie ich,
mit map
auf "Q" legen.
Damit Code nicht nur in der Entwicklungsumgebung sondern später auch in der Produktion bei allen Anwendern funktioniert, muss der Release-Prozess zwei Dinge sicherstellen: Der erzeugte Artifact darf sich den Source-Code ausschließlich aus dem Git-Repository holen, und sich nicht auf lokal herumliegende Dateien verlassen, damit sich der Build immer und überall reproduzieren lässt. Und bevor das Erzeugnis in die Wildnis entlassen wird, muss es in einem Reinraum die beliegende Testsuite bestehen, der die Sicht des Endanwenders simuliert.
Dazu setzen professionelle Entwicklungsschmieden Build-Server ein, die mit Jenkins oder ähnlichen Tools automatisch aufwachen, falls neue Sourcen im Git-Repository vorliegen, sich diese schnappen, den Build starten, die Tests ablaufen lassen, im Erfolgsfall ein Artifact wie einen Tarball oder ein .rpm-Paket schnüren, und dieses auch gleich noch in einem Rutsch zum Distributions-Server schieben. Open-Source-Projekte nutzen hierzu auch oft Travis-CI ([6]), ein exzellenter Build-Hoster, der per Knopfdruck einen Build-Server für ein Github-Projekt aufsetzt und sich mit simplen dreizeiligen Konfigurationen im Source-Code begnügt.
Für den Hausgebrauch tut's auch eine virtuelle Umgebung wie eine mit
Ansible provisionierte Vagrant-VM ([4]) oder ein Docker-Container ([5]),
der den Artifact erzeugt und testet. Das Skript cpan-upload
aus dem Modul CPAN::Upload vom CPAN, schiebt danach einen mit make tardist
geschnürten Tarball aufs CPAN.
1 FROM ubuntu 2 3 RUN apt-get -y update 4 RUN apt-get -y install cpanminus 5 RUN apt-get -y install make 6 RUN apt-get -y install libwww-perl
Listing 1 zeigt eine Docker-Konfiguration, die einen auf Ubuntu
basierten Reinraum erzeugt.
Der erste Aufruf von Dockers build
-Kommando holt sich das relativ
schlanke Ubuntu Basis-Image vom Docker Mutterschiff und setzt noch weitere
Layers drauf, entsprechend den Anweisungen der im gleichen Verzeichnis
liegenden Datei Dockerfile
:
$ docker build -t testimg .
Die Anweisungen im Dockerfile veranlassen Docker dazu, mit apt-get update
Ubuntus Paketmanager auf die neuesten Repo-Versionen einzunorden sowie eine
Reihe von Paketen zur Build-Unterstützung wie make
zu installieren. Spätere
Aufrufe des gleichen build
-Kommandos sparen sich den Aufwand, denn docker
stellt fest, dass das zwischengespeicherte Image aufgrund seiner Hashsumme noch
den Anweisungen des unmodifizierten Dockerfiles entspricht.
Das Build-Skript in Listing 2 führt nach dem build
-Kommando, das ein neues
Container-Image erzeugt, ein run
-Kommando aus, das basierend auf dem Image
einen Container startet. Die Option -v
sorgt dafür, dass das
Source-Verzeichnis des Moduls im Container unter /mybuild
schreib- und
lesbar eingeblendet wird. Da das build
-Skript unter adm/build
im
Git-Repository des Moduls steht, findet Perls FindBin
erst heraus, in
welchem Verzeichnis sich das aufgerufene Skript befindet, und nachdem bekannt
ist, dass sich der Modul-Code ein Verzeichnis darüber liegt, wechselt
Path::Tiny schnell dorthin und gibt mit canonical
den absoluten Pfad
dahin zurück. Als Kommando im Container verwendet Listing 2 bash
und
übergibt ihr mit der Option -c
einen String mit dem Perl-typischen
Dreisprung perl Makefile.PL; make test; make tardist
, der den Modul-Code
unter Reinraumbedingungen zu einem Distributions-Tarball schnürt.
Weitere Build-Stufen kopieren den Tarball in neue Reinräume und testen,
ob er sich auch installieren und verwenden lässt, was nicht automatisch
der Fall ist, besonders wenn er zur Laufzeit weiter Module vom CPAN benötigt.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Sysadm::Install qw(:all); 04 use FindBin qw( $Bin ); 05 use Path::Tiny; 06 07 my $tag = "build"; 08 my $dir = path( "$Bin/.." )->realpath; 09 10 sysrun "docker", "build", "-t", $tag, "."; 11 12 sysrun qw( docker run --rm --name buildc -v ), 13 "$dir:/mybuild", $tag, "bash", "-c", 14 "cd /mybuild; perl Makefile.PL; make test; make tardist";
Wichtig ist dabei, dass jede einzelne Stufe dieses Build-Prozesses automatisch abläuft und sofort die Notbremse zieht, falls unerwartete Ereignisse auftreten. Automatisch deshalb, weil menschliche Bediener nicht nur ständig Fehler machen, wenn es darum geht, immer die gleichen Schritte auszuführen, sondern auch schnell ermüden und aus Frustration dann auch noch grauenhaften Code nachliefern. Ein Teufelskreis. Wer einmal etwas Zeit investiert hat, um den Build-Prozess zu automatisieren, weiß es zu schätzen, wenn er nach einer Änderung im Code nur noch ein Knöpfchen drücken muss und dann in die Mittagspause gehen kann, weil alles seinen staatlich abgesegneten und tausendfach erfolgreich erprobten Gang geht.
Noch ein weiteres Detail ist wichtig: Um später feststellen zu können, auf welchem Stand des Source-Trees ein Release basiert, muss der Build-Prozess den Stand in Git markieren, üblicherweise mit einem Tag, das die Release-Nummer enthält:
git tag release_1.01 git push --tags origin
Wenn origin
die Remote des Git-Repositories bezeichnet, sorgt der
nachfolgende push
-Befehl mit dem Argument --tags
dafür, dass das Tag
nicht nur im lokalen Git-Repository liegt, sondern für jedermann zugänglich auf
Github oder dem Hoster der Wahl. Wer später einen Bug in einem zurückliegenden
Release reproduzieren möchte und dazu die Sourcen zum Release-Zeitpunkt
braucht, checkt diese einfach mit
git checkout -b testbug release_1.01
aus und findet dann in einem neuen Branch testbug
den Stand der Dinge zum
Zeitpunkt ihrer Entstehung.
Das CPAN nimmt Tarbälle an, aber wer seinen Nutzern mehr Komfort gönnt, packt
den fertigen Build gleich in ein Paket der Zieldistro wie Debian oder RPM. Wer
sich nicht scheut, die Hälfte aller je geschriebenen CPAN-Module als
Abhängigkeiten herunterzuladen, nutzt hierzu Dist::Zilla, wer's schlanker mag
installiert das in Ruby geschriebene Tool fpm
([7]). Mit einer einigermaßen
frischen Ruby-Version installiert sich das praktische Tool mit gem install
fpm
. Es unterstützt eine Unzahl von Optionen, aber wer nur ein paar Dateien in
ein Paket der unterstützten Formate RPM, Debian oder OSX für Mac-User
verschnüren möchte gibt die Option -s dir
und das lokal während des
Build-Vorgangs erzeugte Unterverzeichnis usr
als Quelle an. Dort lagern, wie
aus dem tree
-Kommando in Abbildung 4 ersichtlich, die Dateien so, wie sie
später auch auf dem zu installierenden System liegen sollen, also das Skript
foo
in usr/bin/foo
und das Perl-Modul Foo.pm
unter
usr/lib/perl5/site_perl/Foo.pm
. Das Beispiel gibt als Paketformat mit -t
deb
Debian vor, also liegt nachher im Verzeichnis eine *.deb-Datei mit der
angegebenen Versionsnummer.
Abbildung 4: Das Schnür-Tool fpm packt die unter usr liegenden Dateien in ein Debian-Paket. |
Das Tool ist traumhaft einfach zu bedienen und schirmt den Entwickler von den teilweise recht tragischen Implementierungen spezifischer Paketbündler wie rpmbuild ab. Es beherrscht auch Abhängigkeiten von anderen Paketen und vieles mehr und sollte in keiner Werkzeugkiste fehlen.
Teilweise zieht so ein neues Projekt eine ganze Latte von CPAN-Modulen herein, die unter Umständen aber noch nicht als Pakete für die Distro des Endanwenders existieren. Der Ausweg "Also, um das Projekt zu nutzen, öffnen wir eine CPAN-Shell, initialisieren sie, falls das der erste Aufuf ist, dann werden ein paar Dutzend Module installiert, die wiederum weitere Module heranziehen und nach 15 Minuten kann's losgehen" ist oft nicht machbar, gerade wenn die Zielgruppe nichts mit Perl am Hut hat, sondern einfach nur die neue Kommandozeilen-Utility des Projekts nutzen möchte.
In diesen Fällen leistet das CPAN-Modul Carton unschätzbare Dienste, denn in
einer Datei cpanfile
abgelegte Abhängigkeiten im Format
requires 'Log::Log4perl', '1.0'; requires 'Pod::Usage', '0.01';
löst es selbständig auf, zieht indirekt abhängige Module mit herein, startet
für alles jeweils einen Build-Prozess und installiert mit carton install
die
ganze Enchilada unter einem neuen Verzeichnis namens "local". Wenn man das dann
in das Verzeichnis der im letzten Abschnitt besprochenen Utility fpm
verschiebt, schnürt das Tool ein Paket, das auf dem Zielsystem unabhängig von
dort eventuell schon installierten Modulen funktioniert, was ein sehr robustes
System ergibt.
Auch dem sorgfältigsten Programmierer unterlaufen Fehler, und manchmal geht es nicht anders, als den Tatortreinigeranzug überzustreifen und mit dem Perl-eigenen Debugger durch den Code zu stapfen, um zu sehen, wo das Problem liegt. Tritt das Problem nicht am Anfang des Skripts, sondern mitten in einem Modul auf, kann man in der zugehörigen Funktion entweder einen Breakpoint setzen, manuell dort hinsteppen oder meinen Lieblings-Perltrick verwenden: Das an die gewünschte Stelle im Code eingepflanzte Statement
$DB::single = 1;
unterbricht den mit perl -d script
und c
gestarteten Debugger genau
nach dieser Stelle.
Manchmal laufen Perl-Zeilen ab, noch bevor die eigentliche Show im
Hauptprogramm beginnt. Zum Beispiel legt der ORM-Wrapper Rose::DB
seine Datenstrukturen an, während ein Datenbankmodul
z.B. mit use My::Data
eingebunden wird. Wie kann man nun ein
Skript, das dieses Modul nutzt, so starten, dass der Debugger nicht
erst in der ersten Zeile des Hauptprogramms anhält, sondern bereits
in My::App::Data? Stellt man die Zeile
BEGIN { $DB::single = 1 }
an den Anfang des Skripts, hält der Debugger bereits in der ersten ausführbaren Zeile an, egal ob diese im Hauptprogramm oder sonst irgendwo ist.
Und noch etwas zum Entwicklungsprozess: Oft stolpert ein Entwickler über unschöne Codekonstrukte wie Duplizierungen oder offensichtliche Bugs, die zur Laufzeit hässliche Warnungen ausgeben. Dann passiert irgendetwas überraschendes, wie zum Beispiel die Entdeckung eines weiteren, noch viel katastrophaleren Fehlers, der unbedingt sofort behoben werden muss, oder ein externes Ereignis wie eine Arbeitsstörung in Form eines Chefbesuchs, und schon ist der kleine Fehler in Vergessenheit geraten. Nichts ist ärgerlicher, als solche schon einmal gesehenen Fehler später, unter Umständen bei einer Live-Demo oder gar in der Produktion wiederzusehen, und zu erkennen, dass man sie wissenden Auges durchschlüpfen hat lassen! Deswegen: Immer alles notieren, entweder in einem Merksystem wie Evernote oder, wer die Disziplin besitzt, immer wieder nach TODO-Merkern im Code zu suchen, auch damit. Aber Vorsicht, die meisten Entwickler schaffen das nicht und man trifft auch in schon jahrelang erfolgreich aktiven Paketen immer noch in Vergessenheit geratene TODO-Konstrukte.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/01/Perl
"Am Anfang war der Test", Michael Schilli, Linux-Magazin 08/2013, http://www.linux-magazin.de/Ausgaben/2013/08/Perl-Snapshot
vimrc-Datei des Autors: https://github.com/mschilli/dotfiles
"Science. Not Fiction", Michael Schilli, Linux-Magazin 10/2015, http://www.linux-magazin.de/Ausgaben/2015/10/Perl-Snapshot
"Erhebendes am Dock", Michael Schilli, Linux-Magazin, 05/2014, http://www.linux-magazin.de/Ausgaben/2014/05/Perl-Snapshot
"Erweiterte Testansicht", Michael Schilli, Linux-Magazin, 06/2012, http://www.linux-magazin.de/Ausgaben/2012/06/Perl-Snapshot
"Effing Package Management: fpm", https://github.com/jordansissel/fpm/wiki