Perl statt Shell (Linux-Magazin, Februar 2005)

Für kurze administrative Aufgaben basteln Systemadministratoren oft Shell-Skripts. Später rennen sie dann gegen Mauern, weil der Sprachumfang nicht für Erweiterungen ausreicht. Warum nicht gleich richtig anfangen?

Mit ein paar Shell-Kommandos lassen sich schnell Skripte zur Systemadministration zusammenklopfen: Eine Installationsroutine mit cp, mv und chmod hier, eine mit grep, awk und sed gezimmerte Abfrage dort. Kommen dann weitere Anforderungen, werden die Skripts komplizierter, oft unübersichtlich, und manchmal endet der Skripter in einer Sackgasse: Leider gehen manche Sachen in der Shell halt nicht oder nur sehr umständlich.

Kreative Programmierer finden zwar immer obskure Methoden, Mauern zu überwinden und Shells wie bash, ksh und tcsh bieten manche Vorzüge einer ``echten'' Programmiersprache, doch warum zu Fuß gehen, wenn man fahren kann?

Freilich ist Standard-Perl für viele einfache Aufgaben nichts für faule Tipper: Wer will schon

    open FILE, "<filename" or
        die "Cannot open filename ($!)";

schreiben wenn's in der Shell ein einfaches

    cat filename

tut? Sysadm::Install, ein neues Modul vom CPAN schafft hier Abhilfe. Es exportiert Funktionen wie cp, mv, untar, mkd, rmf (rm -f), cd, damit Shell-Skripter sich in Perl zuhause fühlen.

Weiter bietet es Funktionen zu interaktiven Benutzerabfragen, zur Dateimanipulation, zum URL-Download und natürlich vereinfachte Schnittstellen zum Aufruf externer Programme.

Transformer

topng zeigt ein Skript, das eine Reihe angegebener JPG-Bilder mit Hilfe der Utility convert ins PNG-Format umwandelt. Es holt mit use Sysadm::Install qw(:all) alle verfügbaren Funktionen in den aktuellen Namensraum. Zwei davon nutzt es: sysrun(), um ein externes Programm ablaufen zu lassen und rmf(), die Sysadm::Install-Version von rm -f. Der Aufruf von

    topng *.jpg

in einem Verzeichnis voller JPGs zeigt lange nichts, und dann sind die PNGs fertig. Mehr Informationen gefällig? Das ist leicht: Da Sysadm::Install das Log::Log4perl-System unterstützt, reicht ein

    use Log::Log4perl qw(:easy);
    Log::Log4perl->easy_init($DEBUG);

am Anfang des Skripts und schon wird es gesprächiger:

    2004/12/03 23:25:20 sysrun: convert 1.jpg 1.png
    2004/12/03 23:25:32 rmf 1.jpg
    2004/12/03 23:25:32 sysrun: convert 2.jpg 2.png
    2004/12/03 23:25:44 rmf 2.jpg

Aber das ist nicht der einzige Grund, warum das Skript sysrun() und rmf() aus dem Sysadm::Install-Fundus verwendet und nicht etwa Perls Standard-Funktionen system() und unlink(). sysrun() und rmf() laufen, wie alle Funktionen aus Sysadm::Install in einem run-or-die-Modus: Jedes Ergebnis wird hinter den Kulissen minutiös geprüft und das Skript bei einem Fehler sofort mit die() abgebrochen. Shell-Programmierer, die bislang immer

    cp a b || exit 1
    mv c d || exit 1

schreiben mussten, atmen auf. Auch verwendet topng bewusst nicht den strict-Modus, der sonst in der Perl-Welt mit fast schon religiöser Strenge eingesetzt wird. Es ist ein quick-and-dirty Skript, und es steht dazu.

Elegant entblättert

tar ist für viele, die es nicht täglich verwenden, ein Buch mit sieben Siegeln. War nun xf die Option zum Entpacken oder cf? Und dann gibt es immer noch Unbelehrbare, die kein einzelnes Top-Verzeichnis einpacken, sondern auf oberster Ebene alle möglichen Dateien hineinpfeffern, auf dass der Entpackende sofort sein aktuelles Verzeichnis vollkleistert. untar() macht Schluss damit. Es nimmt den Namen einer tar-Datei entgegen, findet heraus, ob eine Dekompression notwendig ist und entpackt den Inhalt. Ist kein einzelnes Top-Verzeichnis enthalten sondern ein wildes Tohuwabohu, erzeugt es ein Top-Verzeichnis und legt den Inhalt darin sauber ab.

Das Beispielskript in Listing untar wird einfach mit dem Tarballnamen aufgerufen:

    untar pari-2.1.4.tgz

Wer statt stoischer Ruhe lieber Informationen über den Ablauf wünscht, fügt die Option -v hinzu

    untar -v pari-2.1.4.tgz

und erhält Details über die Transaktionen:

    2004/12/04 00:24:43 untar pari-2.1.4.tgz
    2004/12/04 00:24:43 Sniffing archive 'pari-2.1.4.tgz'
    2004/12/04 00:24:43 dir=pari-2.1.4
    2004/12/04 00:24:43 archdir=pari-2.1.4
    2004/12/04 00:24:43 Return pari-2.1.4 pari-2.1.4
    2004/12/04 00:24:43 Nice archive, extracting to subdir pari-2.1.4
    2004/12/04 00:24:43 rmf pari-2.1.4

Listing 1: untar

    01 #!/usr/bin/perl
    02 ###########################################
    03 # untar -- Untar tarballs
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Log::Log4perl qw(:easy);
    10 use Getopt::Std;
    11 use Sysadm::Install qw(untar);
    12 
    13 getopts('v', \my %opts);
    14 
    15 Log::Log4perl->easy_init(
    16     $opts{v} ? $DEBUG : $ERROR);
    17 
    18 for my $tar (@ARGV) {
    19    untar($tar);
    20 }

Der Hammer: Perl in der Kaffeepause

Manches Installationsskript fordert den Benutzer zu interaktiven Eingaben auf. Meist reicht es, auf die Enter-Taste zu hämmern, und das ist ganau, was die Funktion hammer() macht. Ein typisches Beispiel ist der perl-Build: Man lädt den Tarball von perl.com, entpackt ihn, springt ins oberste Verzeichnis, startet

    ./Configure

und schon werden tausend Fragen gestellt. Wer sicher ist, dass auf alle die voreingestellte Antwort passt, gibt entweder die Option -d an oder beginnt, auf die Eingabetaste zu hämmern.

Listing mkperl lädt den aktuellen stabilen Perl-Tarball mit download() von perl.com, entpackt ihn mit untar(), konfiguriert den Release und startet den Build. Es nutzt die -d-Option von Configure, da man bei der Perl-Konfiguration nicht immer alles durchgehämmern kann, aber setzt hammer() ein, damit der letzte Prompt, der den Benutzer fragt, ob er die angegebene Konfigurationsdatei manuell ändern will, automatisch abgehakt wird.

Listing 2: mkperl

    01 #!/usr/bin/perl
    02 ###########################################
    03 # mkperl - Download the latest stable perl,
    04 #          configure and install it.
    05 # Mike Schilli, 2004 (m@perlmeister.com)
    06 ###########################################
    07 use strict;
    08 use warnings;
    09 
    10 use Log::Log4perl qw(:easy);
    11 Log::Log4perl->easy_init($DEBUG);
    12 use Sysadm::Install qw(download 
    13          hammer untar cd sysrun);
    14 
    15 download "http://www.perl.com/" .
    16          "CPAN/src/stable.tar.gz";
    17 untar "stable.tar.gz";
    18 cd "stable";
    19 hammer("./Configure", "-d", "-D", 
    20        "prefix=/home/mschilli/PERL-test");
    21 sysrun("make install");

Benutzereingaben

Manchmal brauchen Skripts eine Bestätigung vom Benutzer: Kann dieser Default übernommen werden, oder welche der fünf angebotenen Dateien ist die Richtige? Dazu stellt Sysadm::Install die Funktionen ask und pick bereit.

ask fragt den Benutzer einfach, ob ein vorgegebener Text übernommen oder statt dessen ein neu Eingegebener. pick stellt eine Reihe von Optionen zur Auswahl, numeriert sie durch, und lässt den Benutzer die gewählte Nummer eingeben.

Das Skript in Listing input zeigt an einem Beispiel, wie erst ein Textstring eingeholt und dann eine von drei Optionen gewählt wird:

    Name [No-Name-Entered]> Bill Gates
      Name: Bill Gates
    [1] 0-100K
    [2] 100K-200K
    [3] 300K-
    Salary [1]> 3
      Salary: 300K-

Die Rückgabewerte von ask und pick entsprechen dem eingegebenen bzw. ausgewählten Wert, "Bill Gates" und ``300K-''.

Listing 3: input

    01 #!/usr/bin/perl
    02 ###########################################
    03 # input -- Test ask() and pick()
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Sysadm::Install qw(:all);
    10 
    11 my $name = ask "Name", "No-Name-Entered";
    12 print "  Name: $name\n";
    13 
    14 my $salary = pick "Salary", 
    15    ["0-100K", "100K-200K", "300K-"], 1;
    16 print "  Salary: $salary\n";

Backslashitis

Wer Perls Einzeiler einsetzt, kennt das Problem, den Perl Code auf der Kommandozeile vor der gefräßigen Shell zu schützen:

    perl -e "print "Hi!\n""

wird nicht funktionieren, da die inneren Anführungszeichen und der Backslash von der Shell gefressen werden und das Ausrufezeichen vorher ausgeführte Kommandos zurückholt.

Maskiert man die empfindlichen Zeichen mit einem Backslash (und den Backslash mit einem weiteren Backslash), klappt's:

    perl -e "print \"Hi\!\\n\""

Eine weiter Möglichkeit sind einfache Anführungszeichen, aber dann interpoliert die Shell keine Variablen mehr und einfache Anführungszeichen im Code müssen maskiert werden. Und was passiert, wenn man obiges Kommando nicht auf dem lokalen Rechner ausführen, sondern mit

    ssh -t localhost command

auf einer anderen Maschine irgendwo im Netz? Dann muss jedes Sonderzeichen (und auch die vorher maskierten Sonderzeichen und ihre Maskierer) wiederum maskiert werden. Und schon stellt sich das heimelige Gefühl der Backslashitis ein:

    ssh -t somehost "perl -e \"print \\\"Hi\\\!\\\\n\\\"\""

Wer derlei Gehirnakrobatik scheut, zieht einfach die von Sysadm::Install auf Verlangen exportierte Funktion qquote() heran. Sie klatscht doppelte Anführungszeichen um einen ihr übergebenen String und maskiert im String enthaltene Quotes und Backslashes. Ist der zweite ihr übergebene Parameter :shell, genießen auch der gefährdete Dollar, das bedrohte Ausrufezeichen und die hinterhältige Backquote diesen Schutz.

Listing ips definiert ab Zeile 11 ein Skript, das das Kommando ifconfig ausführt und die IP-Adressen aller angezeigten Netzwerk-Interfaces extrahiert. Zeile 18 entfernt Zeilenumbrüche und überflüssige Leerzeichen aus dem Skripttext, und qquote() in Zeile 21 formt daraus einen kompakten, in doppelte Quotes eingeschlossenen String, der hinter perl -e gehängt wird.

Zeile 23 hängt noch eine quote()-Runde für das ssh-Kommando dran und schließlich führt system() folgendes Kommando aus, das alle IP-Adressen von somehost einsammelt, ohne dort permanent ein Skript zu platzieren:

    ssh -t somehost "perl -e \" \\\$data = \\\`ifconfig\\\`; while(\\\$data =~ /inet addr:(\\\\S+)/g) { print \\\"\\\$1\\\\n\\\"; } \""

Man könnte natürlich genauso gut gleich das Ergebnis von ifconfig auf den lokalen Host holen und dort verarbeiten, aber bei größeren Datenmengen zeigen sich die Vorteile eines solchen 'mobilen' Skripts.

Listing 4: ips

    01 #!/usr/bin/perl
    02 ###########################################
    03 # ips -- run a script on a remote machine
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Sysadm::Install qw(qquote);
    10 
    11 my $script = q{
    12     $data = `ifconfig`;
    13     while($data =~ /inet addr:(\S+)/g) {
    14        print "$1\n";
    15     }
    16 };
    17 
    18 $script =~ s/\s+/ /g;
    19 
    20 my $cmd = "perl -e " .
    21     qquote($script, ":shell");
    22 
    23 $cmd = "ssh -t somehost " . 
    24        qquote($cmd, ":shell");
    25 
    26 system($cmd);

Schlürf & Spuck

Weil Skripts oft mit den gesamten Daten einer Datei arbeiten, soll es in Perl 6 zukünftig eine slurp()-Funktion geben. Sysadm::Install bietet schon heute eine an, zusammen mit dem Gegenstück blurt(), das gespeicherte Daten wieder in einem Rutsch in eine Datei zurückschreibt.

Ein Beispiel: Um zum Linux dazu zu bewegen, nach dem Booten nicht mehr automatisch in den graphischen X-Modus zu wechseln, sondern den Login im Text-Modus durchzuführen, muss in /etc/inittab die Zeile

    id:5:initdefault:

durch

    id:3:initdefault:

ersetzt werden. Mit slurp() und blurt() geht das ganz einfach, wie in Listing fixinittab gezeigt.

Listing 5: fixinittab

    01 #!/usr/bin/perl
    02 ###########################################
    03 # fixinittab
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 
    07 use Sysadm::Install qw(:all);
    08 
    09 $file = "/etc/inittab";
    10 $data = slurp $file;
    11 $data =~ 
    12    s/id:5:initdefault:/id:3:initdefault:/;
    13 blurt $data, $file;

Aber es geht sogar noch kompakter, wie Listing fixinittab-pie zeigt: In Anlehnung an perls Inline-Edit-Modus, der mit

    perl -p -i -e "..."

aktiviert wird, stellt Sysadm::Install die pie()-Funktion bereit, die mindestens zwei Argumente nimmt: Eine Referenz auf einen vom Benutzer definierten Callback und ein oder mehrere Dateinamen. pie geht zeilenweise durch alle angegebenen Dateien, ruft für jede den Callback auf, und ersetzt die Zeile durch den Rückgabewert der Funktion. Sind alle Änderungen durchgeführt, schreibt pie() das Ergebnis wieder in die originale Datei zurück.

Bei Ersetzungen mit dem Substitutionsoperator ist zu beachten, dass s/a/b/ nicht etwa den Ergebnisstring zurückliefert, sondern die Anzahl der ausgeführten Ersetzungen. Besteht der Callback nur aus einer Ersetzung, sorgt s/a/b/; $_; dafür, dass auch wirklich der Ergebniswert der Ersetzung zurück in die Datei wandert.

Listing 6: fixinittab-pie

    01 #!/usr/bin/perl
    02 ###########################################
    03 # fixinittab-pie
    04 # Mike Schilli, 2004 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use Sysadm::Install qw(:all);
    10 
    11 pie(sub { 
    12   s/id:5:initdefault
    13    /id:3:initdefault/gx; $_;
    14   }, "/etc/inittab");

Zum Schluss noch ein Tipp für ganz Faule: Wer es leid ist, ständig #!/usr/bin/perl zu tippen und use Sysadm::Install qw(:all), der definiert sich einfach ein vim-Macro wie in Abbildung 1 gezeigt. Es ordnet der Tastenfolge !P (P für Perl) im Kommandomodus ein Insert-Kommando zu, gefolgt von den ersten sieben Zeilen eines Sysadm::Install-Skripts: Der Perl-Shebang-Zeile, etwas Verzierung, und einem Template für den Skriptnamen, Verwendungszweck und den Namen des Autors. Das .vimrc-Kommando muss in einer Zeile stehen, die später als ^M angezeigten Zeilenumbrüche erreicht man mit CTRL-V, Return in vis Eingabemodus.

Ein neues Skript entsteht so einfach mit dem Aufruf vi test-script. Ein anschließendes !P fügt den Header ein und setzt den vi in den Insert-Modus. Ein neues, aufregendes Perl-Shell-Skript kann beginnen!

Abbildung 1: Ein vim-Macro, das der Tastenfolge !P im Kommando-Modus die Startzeilen eines Perl-Skripts zuordnet und den Editor in den Einfüge-Modus setzt.

Abbildung 2: Nachdem !P eingegeben wurde: Der Skriptautor kann verzögerungsfrei loslegen.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2005/02/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.