Vom Fachmann für Könner (Linux-Magazin, Januar 2016)

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)

Solides Fundament

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.

Stehenbleiben, TDD-Polizei!

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.

IDE für alte Hasen

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.

Infrastruktur als Code

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.

Listing 1: Dockerfile

    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.

Listing 2: build

    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";

Automatisch fehlerfrei

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.

Release-Status markieren

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.

Pakete schnüren

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.

Rein in den Karton

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.

Bugs? Welche Bugs?

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.

Alles Aufschreiben

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.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/01/Perl

[2]

"Am Anfang war der Test", Michael Schilli, Linux-Magazin 08/2013, http://www.linux-magazin.de/Ausgaben/2013/08/Perl-Snapshot

[3]

vimrc-Datei des Autors: https://github.com/mschilli/dotfiles

[4]

"Science. Not Fiction", Michael Schilli, Linux-Magazin 10/2015, http://www.linux-magazin.de/Ausgaben/2015/10/Perl-Snapshot

[5]

"Erhebendes am Dock", Michael Schilli, Linux-Magazin, 05/2014, http://www.linux-magazin.de/Ausgaben/2014/05/Perl-Snapshot

[6]

"Erweiterte Testansicht", Michael Schilli, Linux-Magazin, 06/2012, http://www.linux-magazin.de/Ausgaben/2012/06/Perl-Snapshot

[7]

"Effing Package Management: fpm", https://github.com/jordansissel/fpm/wiki

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.