Git im Quadrat (Linux-Magazin, August 2010)

Wie erhält der neugekaufte Laptop schnellstmöglich Kopien aller aktiv bearbeiteten Git-Repositories? Ein Meta-Repo führt eine Projektliste und Perlskripts automatisieren die Aufspür- und Klonvorgänge.

Das Versionskontrollsystem Git walzt mit seiner rasanten Performance und überragenden Branch-Strategie Oldtimer wie CVS, Subversion oder Perforce so platt, dass sich so mancher nach dem Wechsel wundert, wie man vor der Erfindung dezentralisierter Versionskontrollsysteme überhaupt Software entwickelte.

Git für Repo-Sammlungen

Allerdings konzentriert sich ein Git-Repo meist auf die Bearbeitung eines einzigen Projektes, Sub-Projekte unterstützt Git allenfalls rudimentär. Aktive Entwickler erzeugen oder klonen deswegen im Laufe der Zeit Dutzende von Git-Repos, die auch oft auf unterschiedlichen Servern liegen. Dieses Verfahren funktioniert ohne großen Aufwand, solange man nicht den Rechner wechselt und plötzlich alles neu klonen muss. Beim Kauf eines neuen Laptops oder dem Umzug auf einen neuen Entwicklungsdesktop wäre es schon hilfreich, wenn man dort gleich eine Kopie aller aktiv bearbeiteten Projekte vorfände.

Oft lassen sich neu installierte Computer auch Gruppen zuordnen, die unterschiedliche Repositories brauchen: Auf dem Laptop verbietet sich vielleicht aus Platzgründen ein Git-Repo mit speicherfressenden Bildern, während auf dem Rechner des Arbeitgebers Repos mit privaten Inhalten nichts zu suchen haben. Eine Konfigurationsdatei, irgendwo auf dem Internet abgelegt, könnte die Zugangsdaten der gewünschten Repositories speichern. Deren Werte ändern sich naturgemäß laufend, denn neue Projekte kommen hinzu und alte fallen weg -- wer behält den Überblick über die Versionen? Natürlich ein Versionskontrollsystem der Marke Git, also ein Meta-Repository!

Abbildung 1: In der Datei gitmeta.gmf im Repository gitmeta irgendwo auf dem Internet liegen die Meta-Daten aller aktiv bearbeiteten Git-Repositories des Users.

Erfundenes Format

Als Format für die Konfigurationsdatei bietet sich das sowohl für menschliche Wesen als auch Computer leicht lesbare YAML-Format an. Der spezielle Dialekt soll GMF (Git Meta Format) heißen, und die Konfigurationsdateien tragen .gmf als Endung. Abbildung 1 zeigt ein Beispiel. Git-Repositories lassen sich auf verschiedene Art und Weise ansprechen, der erste Eintrag verweist auf ein privat gehostetes Repo, das auf dem fiktiven per SSH-Zugang gesicherten Server private.server.com liegt. Der zweite Eintrag zeigt auf das offizielle Git-Repo des Perl5-Kerns, auf dem sich alle Check-ins finden, seit Larry Wall im Jahre 1987 die erste Version von Perl freigab. Beide Repo-Locators bearbeitet der Befehl git clone direkt und legt auf Kommando eine lokales Directory mit einer Kopie des jeweiligen Repositories an.

Abbildung 2: Die aus der YAML-Datei gitmeta.gmf eingelesenen Daten formen eine Perl-Datenstruktur.

Natürlich könnte die GMF-Datei einfach alle aktiv bearbeiteten Repos so als Liste aufführen, doch richtig aktive Entwicklern empfänden das dauernde manuelle Einfügen von neuen oder Löschen von abgewrackten Projekten wohl als zu mühselig. Führt jemand zum Beispiel ein Dutzend Projekte auf Github.com oder in einem Verzeichnis auf einem per SSH zugänglichen Server, ließen sich diese Eingriffe einsparen, wenn es das Meta-Repo verstünde, diese Reposammlungen automatisch zu interpretieren. "Nimm alle Repos in diesem Verzeichnis" oder "alle auf Github liegenden Repos" sollte das Meta-Repo schon verstehen.

Auf einen Schlag

Die den unteren zwei Bindestrichen zugeordneten YAML-Blöcke in Abbildung 1 formen jeweils zwei Hash-Strukturen (siehe Perl-Format in Abbildung 2), die im Meta-Format Repo-Sammlungen mit bestimmten Eigenschaften bezeichnen. Der erste Hash führt im Feld "type" den Wert "Github", und der Eintrag "user" weist mit "mschilli" darauf hin, dass alle Repositories, die auf Github.com dem User "mschilli" gehören, auf den lokalen Rechner zu kopieren oder zu aktualisieren sind.

Statt dutzender Einzeleinträge also nur zwei Zeilen, und falls der User auf Github.com neue Repositories anlegt, werden diese automatisch Bestandteil der Konfiguration, ohne dass der User die Konfigurationsdatei anpassen muss. Löscht der Benutzer ein Projekt auf Github, wird der Updater das lokale Projekt nicht explizit löschen, putzt der Benutzer allerdings auch noch die lokale Kopie weg, findet später auch kein Klonvorgang mehr statt.

Der YAML-Eintrag neben dem letzten Bindestrich in Abbildung 1 (oder die letzte Datenstruktur in Abbildung 2) bezeichnet hingegen eine Sammlung von Git-Repositories, die in einem Verzeichnis auf dem angegebenen Server mit SSH-Zugang liegen. Auch hier schnappt sich der Updater automatisch Neueingänge, ohne dass der User eingreifen muss, indem das verarbeitende Skript die Unterverzeichnisse des angegebenen Directories auflistet und die so gefundenen Einzelrepos eins nach dem anderen klont.

Spieglein, Spieglein

Das Skript gitmeta-update übernimmt die Installation und späteres Auffrischen lokaler Repositories anhand der im Meta-Repo festgelegten Daten.

Das Meta-Repo liegt typischerweise auf einem Server mit SSH-Zugang, denn falls es sowohl auf öffentliche als auch private Repos verweist, sollte diese Meta-Information nur für ausgewiesene Nutzer zugänglich sein.

Das Skript erwartet drei Kommandozeilenparameter, die Lage des Meta-Repos, den dortigen Pfad zur .gmf-Datei und das lokale Verzeichnis, in der die gespiegelten Repos zu liegen kommen. Der Aufruf

    gitmeta-update -v user@secret.server.com:git/gitmeta \
        gitmeta.gmf /path/to/local/repo/dir

kontaktiert den Server secret.server.com, loggt sich per SSH als user ein, wechselt dort ins Verzeichnis "git/gitmeta" unter dem Home-Verzeichnis des Users user und spiegelt das dort liegende Git-Repository in einem temporären Verzeichnis auf der lokalen Platte. Es liest dann die aktuelle Version von gitmeta.gmf ein, jagt sie durch den YAML-Parser und arbeitet den die Einträge des Arrays nacheinander ab. Nebenbei gibt der Aufruf oben die Option -v vor, die für eine ausführliche Ausgabe der gerade bearbeiteten Befehle über die Log4perl-API auf STDERR sorgt.

Listing 1: gitmeta-update

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use GitMeta::GMF;
    04 use Sysadm::Install qw(:all);
    05 use File::Basename;
    06 use Getopt::Std;
    07 use Log::Log4perl qw(:easy);
    08 
    09 getopts("vn", \my %opts);
    10 
    11 if($opts{v}) {
    12   Log::Log4perl->easy_init($DEBUG);
    13 } else {
    14   Log::Log4perl->easy_init({
    15       level    => $INFO,
    16       category => "main",
    17   });
    18 }
    19 
    20 my($gmf_repo, $gmf_path, 
    21    $local_dir) = @ARGV;
    22 
    23 die "usage: $0 gmf-repo gmf-path local-dir" 
    24   unless defined $local_dir;
    25 
    26 main();
    27 
    28 ###########################################
    29 sub main {
    30 ###########################################
    31   my $gm = GitMeta::GMF->new(
    32     repo => $gmf_repo,
    33     gmf_path => $gmf_path );
    34   
    35   my @urls = $gm->expand();
    36   
    37   if($opts{n}) {
    38     for my $url ( @urls ) {
    39       print "$url\n";
    40     }
    41     return 1;
    42   }
    43 
    44   cd $local_dir;
    45 
    46   for my $url ( @urls ) {
    47     INFO "Updating $url";
    48     my $repo_dir = basename $url;
    49     $repo_dir =~ s/\.git$//g;
    50     if(-d $repo_dir) {
    51       cd $repo_dir;
    52       my($stdout, $stderr, $rc) =
    53         tap "git", "fetch", "origin";
    54       INFO "$stdout$stderr";
    55       cdback;
    56     } else {
    57       my($stdout, $stderr, $rc) =
    58         tap "git", "clone", $url;
    59       INFO "$stdout$stderr";
    60     }
    61   }
    62   return 1;
    63 }

Das Einholen und Bearbeiten der YAML-Datei kapselt der Perl-Code in der Klasse GitMeta::GMF, die später erläutert wird. Zeile 26 ruft den Konstruktor new() auf und übergibt ihm den Repo-Locator $gmf_repo und den Pfad zur .gmf-Datei, $gmf_path. Die Methodenaufruf expand() in Zeile 30 löst die direkten und indirekten Verweise in der YAML-Datei auf und gibt eine Liste von Repo-Locators zurück, die auf alle zu spiegelnden Repos verweisen.

Ist die Option -n gesetzt, läuft das Skript im Trockendurchgang und Zeile 32 verzweigt zu einer For-Schleife, die lediglich die gefundenen Locators zu Testzwecken ausgibt und anschließend die Bearbeitung ohne eigentliche Spiegelung abbricht. Im Ernstfall wechselt Zeile 39 mit dem Befehl cd aus dem Module Sysadm::Install in das angegebene lokale Verzeichnis. Die for-Schleife ab Zeile 41 iteriert über alle gefundenen Repo-Locators, entfernt eine etwaige Endung .git vom Repo-Namen und prüft, ob das entsprechende Verzeichnis schon existiert, das Repo also schon einmal gespiegelt wurde. Ist dies der Fall, frischt der Aufruf von git fetch das Repo auf, in dem es Änderungen des Originals hereinholt, sie aber nicht (wie git pull das täte) in den gerade bearbeiteten Zweig hineinmischt. Letzteres könnte Konflikte auslösen, die der User erst langwierig lösen müsste, und das Ziel von gitmeta-update is nur die schnelle Spiegelung, solange ein Internetanschluss vorhanden ist. Mergen kann man mit git selbstverständlich dann Offline.

Frisch geklont

Existiert für das zu spiegelnde Repo noch kein lokales Verzeichnis, legt der Befehl git clone in Zeile 49 eines an und zieht die Daten des Remote-Repos herein, damit ein vollständiger Klon entsteht.

Soweit liegt die ganze Magie des Skripts in der in Zeile 30 aufgerufenen Methode expand() der Klasse GitMeta::GMF, die nicht nur eine .gmf-Datei einholt, sondern auch deren Einträge rekursiv interpretiert. Listing GMF.pm implementiert die von der Basisklasse GitMeta.pm abgeleitete Klasse GitMeta::GMF. Ihre expand()-Methode erwartet zwei benannte Parameter, den Repo-Locator repo und den relativen Pfad gmf_path zur .gmf-Datei im Repo. Die beinahe virtuelle Basisklasse in Listing GitMeta.pm stellt den Standard-Konstruktor new() zur Verfügung, den abgeleitete Klassen erben und damit nicht selbst definieren müssen. Das spart Platz.

Faule Subklassen

Weiter definiert die Basisklasse GitMeta.pm die Methode param_check(), die in den Subklassen prüft, ob deren Konstruktor auch die erwarteten Parameter überreicht bekam und bricht das Programm ab, falls dies nicht der Fall ist. Die Klassenhierarchie, also die Tatsache, dass GitMeta::GMF von GitMeta abgeleitet ist, bringt der Befehl use base qw(GitMeta) in Zeile 8 von GMF.pm zum Ausdruck.

Die in der Basisklasse definierte Version der expand()-Methode in Zeile 12 von GitMeta.pm enthält lediglich eine Anweisung, das Programm zu unterbrechen und wird niemals ausgeführt, falls die Subklasse ihre eigene expand()-Methode definiert. Die die-Anweisung dient nur als Erinnerung an Programmierer von Subklassen, diese virtuelle Methode der Basisklasse tatsächlich in der abgeleiteten Klasse zu implementieren.

Listing 2: GitMeta.pm

    01 ###########################################
    02 package GitMeta;
    03 ###########################################
    04 # 2010, Mike Schilli <m@perlmeister.com>
    05 ###########################################
    06 
    07 ###########################################
    08 sub new { 
    09 ###########################################
    10   my($class, %options) = @_;
    11 
    12   my $self = { %options };
    13   bless $self, $class;
    14 }
    15 
    16 ###########################################
    17 sub expand { 
    18 ###########################################
    19   die "You need to implement 'expand'";
    20 }
    21 
    22 ###########################################
    23 sub param_check { 
    24 ###########################################
    25   my($self, @params) = @_;
    26 
    27   for my $param (@param) {
    28     if(! exists $self->{ $param }) {
    29       die "Parameter $param missing";
    30     }
    31   }
    32 }
    33 
    34 1;

Polymorphes Expandieren

Die ab Zeile 48 in GMF.pm definierte Methode _fetch klont das angegebene Git-Repo in einem temporären Verzeichnis und schlürft die YAML-Daten der .gmf-Datei in eine Perl-Struktur, die sie als Ergebnis zurück gibt. Der Unterstrich im Methodennamen weist darauf hin, dass es sich um eine interne, private Methode handelt, die nicht zum exportierten API der Klasse gehört.

Die exportierte Methode expand() ruft zunächst _fetch auf und iteriert dann in der for-Schleife ab Zeile 28 über alle in der .gmf-Datei gefundenen Elemente des YAML-Arrays. Stehen dort normale Repo-Locators ohne type-Eintrag, fügt sie Zeile 33 unmodifiziert ans Ende des @locs-Arrays an. Steht im gerade bearbeitenen YAML-Element hingegen eine Struktur mit einem Eintrag im type-Feld, delegiert GMF.pm die Bearbeitung an eine Subklasse dieses Typs.

Listing 3: GMF.pm

    01 ###########################################
    02 package GitMeta::GMF;
    03 ###########################################
    04 # 2010, Mike Schilli <m@perlmeister.com>
    05 ###########################################
    06 use strict;
    07 use warnings;
    08 use base qw(GitMeta);
    09 use File::Temp qw(tempdir);
    10 use Log::Log4perl qw(:easy);
    11 use YAML qw(Load);
    12 use Sysadm::Install qw(:all);
    13 use File::Basename;
    14 
    15 ###########################################
    16 sub expand {
    17 ###########################################
    18   my($self) = @_;
    19 
    20   $self->param_check("repo", "gmf_path");
    21 
    22   my $yml = $self->_fetch( 
    23       $self->{repo}, 
    24       $self->{gmf_path} );
    25 
    26   my @locs  = ();
    27 
    28   for my $entry ( @$yml ) {
    29     my $type = ref($entry);
    30 
    31     if($type eq "") {
    32       # plain git url
    33       push @locs, $entry;
    34     } else {
    35       my $class = "GitMeta::" .
    36                ucfirst( $entry->{type} );
    37       eval "require $class;" or
    38          LOGDIE "Class $class missing";
    39       my $expander = $class->new(%$entry);
    40       push @locs, $expander->expand();
    41     }
    42   }
    43 
    44   return @locs;
    45 }
    46 
    47 ###########################################
    48 sub _fetch {
    49 ###########################################
    50     my($self, $git_repo, $gmf_path) = @_;
    51 
    52     my($tempdir) = tempdir( CLEANUP => 1 );
    53 
    54     cd $tempdir;
    55     tap "git", "clone", $git_repo;
    56     my $data = slurp(basename($git_repo) . 
    57                      "/$gmf_path");
    58     cdback;
    59     my $yml = Load( $data );
    60     return $yml;
    61 }
    62 
    63 1;

Gültige Werte für type sind "github" und "sshdir", die die Bearbeitung des Eintrags jeweils an die abgeleiteten Klassen GitMeta::Github und GitMeta::SshDir weiterleiten. Hierzu bindet das eval-Kommando in 37 die gesuchte Klasse in das laufende Programm ein und Zeile 39 ruft deren Konstruktor mit den im YAML-Eintrag gefundenen Parametern auf.

In bester Polymorphie-Tradition verfügen auch die abgeleiteten Klassen über eine expand()-Methode, die ebenfalls Listen von Repo-Locators zurückliefern. Was zurückkommt, egal woher, wandert ans Ende des @locs-Arrays und trägt zum Ergebnis bei.

Objektorientiert spezialisiert

Trifft das Skript beim Interpretieren einer .gmf-Datei auf einen Eintrag des Typs "github", aktiviert es die Klasse GitMeta::Github in Listing Github.pm. Auch sie erbt von der Basisklasse GitMeta und überschreibt lediglich die Methode expand(), in der sie die Namen aller auf Github liegenden Repos eines vorgegebenen Users holt. Hierzu nutzt sie Githubs simple XML-API, die ohne Token unter dem Pfad /api/v1/xml/username auf github.com frei verfügbar ist. Die Methode decoded_content() stellt sicher, dass auch UTF8-kodierte Projektbeschreibungen gültiges XML liefern.

Das von der Web-Anfrage zurückkommende XML schnappt sich die Funktion XMLin() aus dem CPAN-Modul XML::Simple und wandelt es in eine tief verschachtelte Hash-Datenstruktur um, in die Zeile 35 unter dem Schlüssel {repositories}->{repository} hineinlangt und einen Hash bekommt, dessen Keys die Repo-Namen repräsentieren.

Listing 4: Github.pm

    01 ###########################################
    02 package GitMeta::Github;
    03 ###########################################
    04 # 2010, Mike Schilli <m@perlmeister.com>
    05 ###########################################
    06 use strict;
    07 use warnings;
    08 use base qw(GitMeta);
    09 use LWP::UserAgent;
    10 use XML::Simple;
    11 
    12 ###########################################
    13 sub expand {
    14 ###########################################
    15   my($self) = @_;
    16 
    17   $self->param_check("user");
    18 
    19   my $user  = $self->{user};
    20   my @repos = ();
    21 
    22   my $ua = LWP::UserAgent->new();
    23   my $resp = $ua->get(
    24    "http://github.com/api/v1/xml/$user");
    25 
    26   if($resp->is_error) {
    27     die "API fetch failed: ",
    28         $resp->message();
    29   }
    30 
    31   my $xml = XMLin(
    32       $resp->decoded_content());
    33 
    34   my $by_repo = 
    35     $xml->{repositories}->{repository};
    36 
    37   for my $repo (keys %$by_repo) {
    38       push @repos, 
    39         "git\@github.com:$user/$repo.git";
    40   }
    41 
    42   return @repos;
    43 }
    44 
    45 1;

Zeile 39 formt aus dem Namen einen Github-typischen Repo-Locator, der dem lokalen User sowohl Lese- als auch Schreibberechtigung einräumt, vorrausgesetzt natürlich der User identifiziert sich mit einem gültigen SSH-Key.

SSH versteckt Privates

Eine weitere spezialisierte Klasse findet sich in Listing SshDir.pm. Das dort definierte, ebenfalls von Gitmeta erbende Paket Gitmeta::SshDir zeichnet für Repos verantwortlich, die als Unterverzeichnisse in einem Directory auf einem per SSH-Zugang geschützten Server liegen. Diese eignen sich hervorragend für private Repos, da weder ihr Inhalt noch ihre Namen irgendwo öffentlich erscheinen.

Listing 5: SshDir.pm

    01 ###########################################
    02 package GitMeta::SshDir;
    03 ###########################################
    04 # 2010, Mike Schilli <m@perlmeister.com>
    05 ###########################################
    06 use strict;
    07 use warnings;
    08 use base qw(GitMeta);
    09 use Sysadm::Install qw(:all);
    10 use Log::Log4perl qw(:easy);
    11 
    12 ###########################################
    13 sub expand {
    14 ###########################################
    15   my($self) = @_;
    16 
    17   $self->param_check("host", "dir");
    18 
    19   INFO "Retrieving repos ",
    20        "from $self->{host}";
    21 
    22   my($stdout) = tap "ssh", $self->{host}, 
    23      "ls", $self->{dir};
    24 
    25   my @repos = ();
    26 
    27   while( $stdout =~ /(.*)\n/g ) {
    28       push @repos, 
    29         "$self->{host}:$self->{dir}/$1";
    30   }
    31 
    32   return @repos;
    33 }
    34 
    35 1;

Um eine Liste dort verfügbarer Verzeichnisse einzulesen und später an den Updater durchzureichen, setzt Zeile 22 in SshDir.pm ein ls-Kommando über das ssh-Protokoll auf dem Server ab und erfragt damit alle unter dem angegebenen Verzeichnis stehenden Directories. Die Ausgabe ist Unix-Shell-typisch durch Zeilenumbrüche getrennt. Die while-Schleife ab Zeile 27 trennt die Zeilen, formt aus jedem Eintrag einen Repo-Locator für das Git-über-SSH-Protokoll und hängt ihn an den Ergebnis-Array @repos an, den die Methode dann an den Aufrufer zurückreicht.

Meta im Quadrat

Meta-Repos dürfen auch andere Meta-Repos referenzieren, wie Abbildung 3 zeigt. Der gezeigte Eintrag definiert im type-Feld "GMF" und der bearbeitende Code zieht darum die Klasse GitMeta::GMF zur Bearbeitung heran, die das Remote-Repo einholt und wiederum dessen .gmf-Datei analysiert.

Das Skript löst dann die Einträge rekursiv auf und erzeugt eine lange Liste mit Repos, die es aufzufrischen gilt. So lassen sich Repo-Gruppen kombinieren und jedes Zielsystem erhält eine maßgeschneiderte Repo-Sammlung, ohne dass Repos doppelt und dreifach in mehreren Konfigurationen stehen müssen.

Abbildung 3: Aus einem Git-Meta-Repo lassen sich weitere Git-Meta-Repos referenzieren.

Die in Abbildung 3 stehende Konfiguration entspricht genau dem Aufruf

    gitmeta-update user@devhost.com:git/gitmeta privdev.gmf ...

nur dass dem Kommandozeilenaufruf noch ein Verzeichnis lokal gespiegelter Git-Repos folgt. Die .gmf-Dateien im Meta-Repo dürfen zur besseren Strukturierung übrigens ruhig in Unterverzeichnissen liegen, es wäre durchaus denkbar, ein Meta-Repo mit zwei GMF-Dateien priv/free.gmf und priv/commerce.gmf zu bestücken, um freie von kommerzieller Software zu trennen. Hierzu ist lediglich der gmf_path in der GMF-Konfiguration bzw der zweite Parameter von gitmeta-update auf der Kommandozeile anzupassen.

Schlüssel statt Passwort

Damit die SSH-Zugriffe den User nicht dazu zwingen, dauernd sein Passwort anzugeben, ist es notwendig alle beteiligten SSH-Server mit Public Keys auszustatten. Andernfalls fragen die Server nach einem Passwort, doch diese Rückfragen bekommt der User durch die ausgabeschluckenden tap()-Befehle nicht zu Gesicht und wundert sich, warum der Zugriff hängt. Github lässt von vornherein keine Passworteingabe bei Git-Zugriffen zu und verlangt deswegen, dass der User seinen Public Key auf der Webseite hinterlegt.

Installation

Damit das gitmeta-Skript auf einer neu eingerichteten Maschine läuft, müssen dort neben perl auch die vom Skript und seinen Modulen verwendeten CPAN-Module installiert sein. Die vier vorgestellten Klassen müssen exakt in der folgenden Verzeichnishierarchie im Filesystem unter einem Pfad finden, den das Skript findet:

    GitMeta.pm
    GitMeta/GMF.pm
    GitMeta/Github.pm
    GitMeta/Sshdir.pm

Zum Anlegen neuer GMF-Dateien erzeugt man ein neues Gitmeta-Repo auf einem Server mit SSH-Zugang, editiert die .gmf-Datei und führt, wie in Abbildung 4 gezeigt, einen Commit durch.

Abbildung 4: Zum Anlegen neuer GMF-Dateien erzeugt man ein neues Gitmeta-Repo auf einem Server mit SSH-Zugang, editiert die .gmf-Datei und führt einen Commit durch.

Nach dem Anlegen des Meta-Repos auf dem Server ist das Meta-Repo über den Locator

    user@some.host.com/repos/gitmeta

erreichbar und einem Aufruf von gitmeta-update mit diesem Parameter beginnt mit dem Klonvorgang. Wer noch keinen neuen Laptop hat, um es auszuprobieren, hat nun den perfekten Vorwand, um einen zu erwerben.

Infos

[1]

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

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.