Aus Linux-Magazin 08/2010

Skript managt Git-Repositories mit Metaverzeichnis

© Claudia Hautumm, Pixelio.de

Wie erhält der neu gekaufte Laptop schnellstmöglich Kopien aller aktiv genutzten Git-Repositories? Ein Meta-Repository führt eine Projektliste und Perl-Skripte automatisieren alle Aufspür- und Klonvorgänge .

Das Versionskontrollsystem Git walzt mit seiner Performance und überragenden Branch-Strategie Oldtimer wie CVS, Subversion oder Perforce platt und mancher fragt sich, wie er vor der Erfindung dezentralisierter Versionskontrollsysteme überhaupt Software entwickeln konnte.

Repository-Sammlungen

Allerdings konzentriert sich Git meist auf ein einzelnes Projekt, Subprojekte unterstützt es allenfalls rudimentär. Aktive Entwickler erzeugen oder klonen deswegen im Lauf der Zeit Dutzende von Git-Repositories, die oft auf unterschiedlichen Servern liegen. Dieses Verfahren funktioniert ohne großen Aufwand – solange der Entwickler nicht den Rechner wechselt und plötzlich alles neu klonen muss. Nach dem Kauf eines neuen Laptops oder dem Umzug auf einen neuen Entwicklungsdesktop wäre es hilfreich, würde er dort gleich eine Kopie aller aktiv bearbeiteten Projekte vorfinden.

Oft lassen sich Computer zudem Gruppen zuordnen, die unterschiedliche Repositories brauchen: Auf dem Laptop verbietet sich vielleicht aus Platzgründen ein Git-Repository mit großen Bildern, während auf dem Rechner des Arbeitgebers private Inhalte nichts zu suchen haben. Eine Konfigurationsdatei, irgendwo im Internet abgelegt, könnte die Zugangsdaten der gewünschten Repositories speichern. Deren Werte ändern sich häufig, denn neue Projekte kommen hinzu und alte fallen weg. Den Überblick behält natürlich ein Versionskontrollsystem wie Git, also ein Meta-Repository!

Erfundenes Format

Als Format für die Konfigurationsdatei bietet sich das sowohl für menschliche Wesen als auch für 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. Der erste Eintrag verweist auf ein privat gehostetes Repository, das auf dem fiktiven, per SSH-Zugang gesicherten Server »private.server.com« liegt. Der zweite Eintrag zeigt auf das offizielle Git-Repository des Perl-5-Kerns, auf dem sich alle Check-ins finden, seit Larry Wall im Jahre 1987 die erste Version von Perl freigab. Beide Repository-Locators bearbeitet der Befehl »git clone« direkt und legt auf Kommando ein lokales Directory mit einer Kopie des jeweiligen Repository an.

Abbildung 1: In der Datei »gitmeta.gmf« im Repository »gitmeta« irgend-wo im Internet liegen die Metadaten aller aktiv bearbeiteten Git-Repositories des Users.

Abbildung 1: In der Datei »gitmeta.gmf« im Repository »gitmeta« irgend-wo im Internet liegen die Metadaten aller aktiv bearbeiteten Git-Repositories des Users.

Natürlich könnte die GMF-Datei einfach alle aktiv bearbeiteten Repositories in einer solchen Liste aufführen, doch sehr aktive Entwickler empfänden das dauernde manuelle Einfügen von neuen oder das 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, könnte er diese Eingriffe einsparen, wenn es das Meta-Repository verstünde, diese Sammlungen automatisch zu interpretieren. Anweisungen wie beispielsweise “Nimm alle Repositories in diesem Verzeichnis” oder “Alle auf Github liegenden Repositories” sollte das Meta-Repository 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 Metaformat Repository-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 er 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 aber auch noch die lokale Kopie weg, findet später auch kein Klonen 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 auflistet und die so gefundenen Einzelrepositories klont.

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

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

Spieglein, Spieglein

Das Skript »gitmeta-update« in Listing 1 übernimmt die Installation und das Auffrischen lokaler Repositories anhand der im Meta-Repository festgelegten Daten. Das Meta-Repository liegt typischerweise auf einem Server mit SSH-Zugang, damit diese möglicherweise vertrauliche Meta-Information nur für ausgewiesene Nutzer zugänglich ist.

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 }
14
15 my($gmf_repo, $gmf_path,
16 $local_dir) = @ARGV;
17
18 die "usage: $0 gmf-repo gmf-path local-dir"
19 unless defined $local_dir;
20
21 main();
22
23 ###########################################
24 sub main {
25 ###########################################
26  my $gm = GitMeta::GMF->new(
27                         repo => $gmf_repo,
28                     gmf_path => $gmf_path );
29
30  my @urls = $gm->expand();
31
32  if ($opts{n}) {
33      for my $url ( @urls ) {
34          print "$urln";
35      }
36      return 1;
37  }
38
39  cd $local_dir;
40
41  for my $url ( @urls ) {
42      my $repo_dir = basename $url;
43      $repo_dir =~ s/.git$//g;
44      if (-d $repo_dir) {
45          cd $repo_dir;
46          tap "git", "fetch", "origin";
47      cdback;
48      } else {
49          tap "git", "clone", $url;
50      }
51  }
52  return 1;
53 }

Das Skript erwartet drei Kommandozeilen-Parameter: die Lage des Meta-Repository, den dortigen Pfad zur GMF-Datei und das lokale Verzeichnis, in dem die gespiegelten Repositories zu liegen kommen. Der Aufruf

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

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

Das Einholen und Bearbeiten der YAML-Datei kapselt der Perl-Code in der Klasse GitMeta::GMF (dazu später). Die Zeilen 26 bis 28 rufen den Konstruktor »new()« auf und übergeben ihm den Repository-Locator »$gmf_repo« und den Pfad zur GMF-Datei »$gmf_path«. Der Methodenaufruf »expand()« in Zeile 30 löst direkte und indirekte Verweise in der YAML-Datei auf und gibt eine Liste von Repository-Locators zurück, die auf zu spiegelnde Repositories verweisen.

Ist die Option »-n« gesetzt, läuft das Skript im Trockengang und Zeile 32 verzweigt zu einer For-Schleife, die nur die gefundenen Locators zu Testzwecken ausgibt und dann die Bearbeitung ohne eigentliche Spiegelung abbricht. Im Ernstfall wechselt Zeile 39 mit dem Befehl »cd« aus dem Modul Sysadm::Install in das angegebene lokale Verzeichnis.

Die For-Schleife ab Zeile 41 iteriert über alle gefundenen Repository-Locators, entfernt eine etwaige Endung ».git« aus dem Namen und prüft, ob das entsprechende Verzeichnis schon existiert, das Repository also schon einmal gespiegelt wurde. Wenn ja, frischt es der Aufruf von »git fetch« auf, in dem es Änderungen des Originals hereinholt, sie aber nicht (wie »git pull« es täte) in den gerade bearbeiteten Zweig hineinmischt. Dies könnte Konflikte auslösen, die der User erst langwierig lösen müsste, aber das Ziel von »gitmeta-update« ist 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 Repository kein lokales Verzeichnis, legt »git clone« in Zeile 49 von Listing 1 eines an und zieht die Daten des Remote-Repository 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 reinholt, sondern auch deren Einträge rekursiv interpretiert.

Listing 3 implementiert die von der Basisklasse »GitMeta.pm« abgeleitete Klasse GitMeta::GMF. Ihre »expand()«-Methode erwartet zwei Parameter, den Repository-Locator »repo« und den relativen Pfad »gmf_path« zur dortigen GMF-Datei. Die beinahe virtuelle Basisklasse in Listing 2 stellt den Standard-Konstruktor »new()« zur Verfügung, den abgeleitete Klassen erben. Das spart Platz.

Listing 2:
»GitMeta.pm«

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

Faule Subklassen

Außerdem 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 7 von Listing 3 zum Ausdruck.

Listing 3:
»GMF.pm«

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

Die in der Basisklasse definierte Version der »expand()«-Methode ab Zeile 15 von Listing 2 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.

Polymorphes Expandieren

Die ab Zeile 47 in Listing 3 definierte Methode »_fetch()« klont das angegebene Git-Repository in ein temporäres Verzeichnis und schlürft die YAML-Daten der GMF-Datei in eine Perl-Struktur, die sie als Ergebnis zurückgibt. 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 27 über alle in der GMF-Datei gefundenen Elemente des YAML-Array. Stehen dort normale Repository-Locators ohne »type«-Eintrag, fügt sie Zeile 32 unmodifiziert ans Ende des »@locs«-Array an. Steht im gerade bearbeiteten YAML-Element hingegen eine Struktur mit einem Eintrag im »type«-Feld, delegiert »GMF.pm« die Bearbeitung an eine Subklasse dieses Typs.

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 Zeile 36 die gesuchte Klasse in das laufende Programm ein, Zeile 38 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 Repository-Locators zurückliefern. Was zurückkommt, egal woher, wandert ans Ende des »@locs«-Array 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 4. Auch sie erbt von der Basisklasse »GitMeta« und überschreibt lediglich die Methode »expand()«, in der sie die Namen aller auf Github liegenden Repositories eines vorgegebenen Users holt.

Listing 4:
»Github.pm«

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

Hierzu nutzt sie Githubs simples XML-API, das ohne Token unter dem Pfad »/api/v1/xml/username« auf »github.com« frei verfügbar ist. Die Methode »decoded_content()« stellt sicher, dass auch die UTF8-kodierten Projektbeschreibungen gültiges XML liefern.

Das von der Webanfrage 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 33 unter dem Schlüssel »{repositories}->{repository}« hineinlangt und daraufhin einen Hash bekommt, dessen Keys die Repository-Namen repräsentieren.

Die Zeilen 36 und 37 formen aus dem Namen einen Github-typischen Repository-Locator, der dem lokalen User sowohl Lese- als auch Schreibberechtigung einräumt, vorausgesetzt natürlich der User hat sich zuvor mit einem gültigen SSH-Key identifiziert.

SSH versteckt Privates

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

Listing 5:
»SshDir.pm«

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

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

Meta-Repositories dürfen auch andere Meta-Repositories referenzieren, wie in Abbildung 3 zu sehen ist. Der gezeigte Eintrag definiert im »type«-Feld »GMF«, der bearbeitende Code zieht darum die Klasse »GitMeta::GMF« zur Bearbeitung heran, die das Remote-Repository einholt und wiederum dessen GMF-Datei analysiert.

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

Die in Abbildung 3 stehende Konfiguration entspricht genau dem Aufruf

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

nur dass dem Kommandozeilen-Aufruf noch ein Verzeichnis lokal gespiegelter Git-Repositories folgt. Die GMF-Dateien im Meta-Repository 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 beziehungsweise der zweite Parameter von »gitmeta-update« auf der Kommandozeile anzupassen.

Abbildung 3: Aus einem Gitmeta-Repository lassen sich ohne Weiteres auch noch andere Gitmeta-Repositories referenzieren, sodass auf diese Weise eine hierarchische Struktur entsteht.

Abbildung 3: Aus einem Gitmeta-Repository lassen sich ohne Weiteres auch noch andere Gitmeta-Repositories referenzieren, sodass auf diese Weise eine hierarchische Struktur entsteht.

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. Sonst fragen die Server nach einem Passwort, doch diese Rückfragen bekommt der User wegen der die Ausgaben schluckenden »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, 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 von ihm und seinen Modulen verwendeten weiteren CPAN-Module installiert sein. Die vier in diesem Artikel vorgestellten Klassen müssen exakt in der folgenden Verzeichnishierarchie im Filesystem unter einem Pfad liegen, den das Skript auch finden kann:

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

Zum Anlegen neuer GMF-Dateien erzeugt der Entwickler ein neues Gitmeta-Repository auf einem Server mit SSH-Zugang, editiert die GMF-Datei und führt anschließend – wie in Abbildung 4 gezeigt – einen Commit durch. Nachdem das Meta-Repository auf dem Server angelegt wurde, ist es über den Locator

user@some.host.com:repos/gitmeta

erreichbar. Nach einem Aufruf von »gitmeta-update« mit diesem Parameter beginnt schließlich der Klonvorgang. Wer noch keinen neuen Laptop hat, um alles auszuprobieren, der hat damit einen perfekten Vorwand, sich jetzt einen zu kaufen. (jcb)

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

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

Infos

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

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben