Bewährte Helferlein (Linux-Magazin, September 2011)

Wer viel in der Shell arbeitet und navigiert, nach Textstücken sucht oder CPAN-Module installiert, weiß die hier vorgestellten Helferscripts sicher als Fingerschoner zu schätzen.

Neulich zog ich auf einen neuen Entwicklungs-Desktop um und packte die Gelegenheit beim Schopf, mein überquellendes Home-Verzeichnis nicht nur aufzuräumen, sondern neu aufzubauen. Dort hatten sich über die Jahre hunderte von teilweise schon wieder obsoleten Helferskripts angesammelt. Um Ordnungs in das Chaos zu bringen, beschloss ich, wirklich bei Null anzufangen und jedes bei der täglichen Tipparbeit unsäglich vermisste Skript nachzuinstallieren -- in reproduzierbarer Art und Weise, versteht sich, auf dass der nächste Umzug ohne Gefluche vonstatten gehe.

Versioniert mit Link

Alle Skripts liegen nun in Unterverzeichnissen verschiedener git-Repositories. Damit der Benutzer die Helferlein ohne Pfadangabe aufrufen kann, zeigen Symlinks zeigt vom bin-Pfad des Home-Verzeichnisses zu den eigentlichen Skripts. Ein weiteres Skript, binlinks, ordnet in seinem DATA-Bereich den ins git-Repository eingecheckten Skripts Links im lokalen bin-Verzeichnis des Users zu. So verbleibt zum Beispiel das Skript logtemp zum Auslesen des in [2] vorgestellten Temperaturfühlers im Git-Repo "articles", während das handgeschriebene Konvertierwerkzeug cvs2git im Schilli-Labs-Repo sandbox aufgehoben ist.

Listing 1: binlinks

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Log::Log4perl qw(:easy);
    04 use File::Basename;
    05 use Sysadm::Install qw(mkd);
    06 
    07 Log::Log4perl->easy_init($DEBUG);
    08 
    09 my ($home)   = glob "~";
    10 my $home_bin = "$home/bin";
    11 
    12 while (<DATA>) {
    13  chomp;
    14 
    15  my ($linkbase, $src) = split ' ', $_;
    16 
    17  $src        = "$home/$src";
    18  my $binpath = "$home_bin/$linkbase";
    19 
    20  if (-l $binpath) {
    21   DEBUG "$binpath already exists";
    22   next;
    23  } elsif (-e $binpath) {
    24   ERROR "$binpath already exists, ",
    25     "but not a link!";
    26   next;
    27  }
    28 
    29  INFO "Linking $binpath -> $src";
    30 
    31  symlink $src, $binpath
    32    or LOGDIE
    33    "Cannot link $binpath->$src ($!)";
    34 }
    35 
    36 __DATA__
    37 logtemp git/articles/temper/eg/logtemp
    38 cvs2git git/sandbox/cvs2git/cvs2git

Kommt ein neues Skript in den bin-Bereich hinzu, trägt der Entwickler es ans Ende des DATA-Bereichs von binlinks ein und ruft letzteres auf. Es klappert dann alle Einträge ab, verifiziert, ob der gewünschte Link in ~/bin schon existiert und legt ihn an, falls dies noch nicht der Fall ist. Dass binlinks selbst wiederum in einem Git-Repo liegt, sollte auf der Hand liegen. Es nutzt das Modul Sysadm::Install vom CPAN, einzig um dessen Funktion mkd willen, die neue Verzeichnisse ohne Murren anlegt und mit Log4perl-Ausgaben zum Ablauf Auskunft gibt.

Folge dem Link

So handelt es sich bei einem aufgerufenen Skript oft um einen Symlink. Wenn ein Symlink zu einer Datei in einem anderen Verzeichnis zeigt, möchten Entwickler oft mit cd dorthin wechseln. Dies erledigt das Kommando lcd mit dem Link als Parameter (Abbildung 1).

Abbildung 1: Die Funktion lcd wechselt in das Verzeichnis, das das Skript enthält, auf das ein Symlink zeigt.

Alte Unix-Hasen wissen natürlich, dass ein Skript nicht das aktuelle Verzeichnis des Aufrufers wechseln kann. Skripts laufen in einer Subshell ab, und bei deren Beendigung sind für den Aufrufer keine nachhaltigen Nebenwirkungen bemerkbar. Aus diesem Grund ist lcd im Startskript .bashrc der Bash-Shell als Bash-Funktion definiert:

    function lcd () { cd `symlinkdir $1`; \
                      pwd; ls; }

Ruft jemand lcd bin/cvs2git auf, übergibt die Bash-Funktion das Argument bin/cvs2git an das Skript symlinkdir und ruft das Shell-Kommando cd mit dessen Ausgabe auf. Die Implementierung von symlinkdir zeigt Listing 2.

Listing 2: symlinkdir

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use File::Basename;
    04 
    05 my($link) = @ARGV;
    06 
    07 die "No link specified" unless $link;
    08 die "$link not a symbolic link" 
    09   unless -l $link;
    10 
    11 while(-l $link) {
    12   $link = readlink($link);
    13 }
    14 
    15 $link = dirname($link) unless -d $link;
    16 print "$link\n";

Das Skript folgt mit der Systemfunktion readlink() dem als Parameter überreichten Link und wiederholt dies solange, bis das Ergebnis kein Link mehr ist. Die Funktion dirname() aus dem Modul File::Basename extrahiert aus dem resultierenden Pfad das Verzeichnis und Zeile 16 gibt es auf der Standard-Ausgabe aus, wo die Bash-Funktion lcd() es aufschnappt, dorthin wechselt, es mit pwd ausgibt und mit ls die dort liegenden Einträge auflistet.

Sparsamer CPAN-Installierer

Kaum ein Perl-Snapshot kommt ohne zusätzlich zu installierende CPAN-Module aus, und üblicherweise klappt dies kurz und schmerzlos mit einer CPAN-Shell, die entweder als perl -MCPAN -eshell aufgerufen wird, oder mittels dem neueren Perl-Distributionen beiliegenden Skript cpan.

Allerdings führt der Resourcenhunger der CPAN-Shell auf Billighost-Accounts schnell zum Rauswurf. Abbildung 2 zeigt, was nach einigen Sekunden auf dem Shared-Hosting-Provider Dreamhost passiert, ohne dass die CPAN-Shell auch nur das gewünschte Modul vom CPAN geladen hätte. Angeblich verbraucht es zuviel RAM-Speicher und damit die anderen Shared-Accounts nicht leiden, zieht Dreamhost - wohl etwas übereilt - die Notbremse.

Abbildung 2: Zuviel für den Billighoster: Eine CPAN-Shell zum Installieren eines Perl-Modules führt zum Ziehen der Notbremse.

Abbildung 3: Das resourcenschonende CPAN-Modul App::cpanminus installiert mit cpanm problemlos das gewünschte Modul.

Rettung naht in Form des CPAN-Moduls App::cpanminus. Abbildung 3 zeigt die kurz gehaltene Ausgabe des unscheinbaren Tausendsassas, der so wenig Resourcen verbraucht, dass es selbst dem Billighoster mit seinen strengen Richtlinien nicht auffällt.

Wie sein großer Bruder CPAN.pm weiß auch cpanminus mit lokalen Modulpfaden umzugehen. Damit der User CPAN-Module mit einem nicht-priviligierten Account installieren kann und auch nicht die heilige Ordnung des Paketmanagers durcheinander bringt, raten Experten dringend zur Verwendung von local::lib, falls die verwendete Linux-Distribution benötigte Perlmodule nicht im Repository führt.

Das Modul local::lib installiert der Admin (ein letztes Mal als root) am geschicktesten mittels des Paketmanagers, unter Ubuntu zum Beispiel mit

    sudo apt-get install liblocal-lib-perl

Erlaubt ein Hoster keinen Root-Zugriff und lässt sich auch nicht erbarmen, das nützliche local::lib per Admin-Eingriff nachzuinstallieren, lädt der Benutzer den Tarball selbst vom CPAN, entpackt ihn und ruft

    perl Makefile.PL --bootstrap
    make install

auf. Künftig mit einer CPAN-Shell installierte Module landen dann im Verzeichnis "perl5" unter dem Heimverzeichnis des unpriviligierten Users. Wenn dieser anschließend noch das Kommando

    eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)

in die Startup-Datei seiner Bash-Shell anhängt normalerweise (.bashrc), setzt es die Variablen PERL_MM_OPT und PERL5LIB, sodass auf der einen Seite sowohl die CPAN-Shell als auch cpanminus bei 'make install' neu installierte Module lokal im Home-Verzeichnis installieren und andererseits aufgerufene Skripts, die die neu installierten Module mit "use XXX" einbinden, diese auch finden. Falls diese (zum Beispiel als Cronjob) nicht über die Environment-Variablen aus .bashrc verfügen, tut's auch ein explizit eingetragenes use local::lib im Programmcode vor dem Laden der benötigten lokal installierten Module.

Suchen nach Text

Oft weiß der Benutzer, dass sich unterhalb des aktuellen Pfades irgendwo eine Datei befindet, in der sich ein gesuchter Textstring "blabla" befindet. In der Shell könnte man nun einen find-Befehl wie etwa

    find . -type file -exec grep blabla {} /dev/null \;

absetzen, aber das ist extrem viel Tipparbeit und erfordert einiges Nachdenken, speziell wegen des Tricks mit /dev/null, das bei Einzeltreffern auch den Dateinamen anzeigt und dem seltsamerweise erforderlichen maskierten Semicolon, der der Option -exec anzeigt, dass das ihr übergebene Kommando damit endet. Früher hatte ich ein Skript findgrep, das mit Perls File::Find-Modul eine rekursive Textsuche startete, aber seit es ack [3] gibt, lädt man das mächtige Kommando einfach vom CPAN und tippt

    $ ack blabla

ein -- fertig ist der Lack. Allerdings ist das Skript sehr penibel mit Dateitypen: Nur was es aufgrund der Namensendung als textähnliche Datei ansieht, untersucht es auch. Möchte man alle Dateien durchforsten, muss man ack -a blabla eingeben.

Wer gesteigerten Wert auf Performance legt, ist in einem Git-Repository mit der oft übersehenen Funktion

    $ git grep blabla

übrigens weit besser bedient. Da git die von ihm verwalteten Dateien in einem Index abspeichert, braucht es für die rekursive Suche keine Dateibäume zu durchforsten und schlägt den ungeschlachten Ansatz gerade bei Dateien um Längen, die noch nicht im Buffer Cache des Operationssystems liegen und in tief verschachtelten Ordnern ausharren.

Automatisch formatiert

Damit selbstgeschriebene Perlskripts vorgegebenen Normen genügen, schickt der ordnungsliebende Programmierer sie am Ende durch den Beautifyer perltidy. Dieses Skript ist als CPAN-Modul erhältlich und versteht eine Fülle von Konfigurationen, die jedem Stil gerecht werden. Wo stehen die geschweiften Klammern, in der if-Zeile oder in der nächsten? Kommt das else direkt nach der schließenden geschweiften Klammer oder erst nach einem Zeilenumbruch? Leerzeichen zwischen runden Klammern bei Funktionsaufrufen und deren Argumenten? Und, ganz wichtig, wie groß ist die maximale Zeilenlänge, ab wann muss der Formatierer lange Codezeilen umbrechen?

Die Manualseite von perltidy listet alle Optionen auf und beschreibt deren Wirkung. Listing 3 zeigt die Konfiguration für Perl-Listings im Linux-Magazin. Die Zeilenbreite beträgt 43 Zeichen, und Zeilen in Blöcken rückt der Formatierer um zwei Zeichen ein (-i=2). Bricht er eine Zeile um, rückt er den Zeilenrest auf der nächsten Zeile ebenfalls um zwei Zeichen ein (-ci=2). Else-Anweisungen folgen direkt, ohne Zeilenumbruch, nach der schließenden geschweiften Klammer des if-Blocks ("cuddled else", -ce). Und da Platz im Magazin stets Mangelware ist und die Redakteure gern damit geizen, steht die "vertical tightness", also die vertikale Textdichte, auf dem höchsten Wert -vt=2. Mit dieser Option geizt der Formatierer mit Zeilenumbrüchen wie's nur geht. Ebenfalls um Platz zu sparen bestimmt -nbbc schließlich, dass vor ganzzeiligen Kommentaren keine Leerzeilen stehen.

Listing 3: perltidyrc

    1 # perltidy-Optionen für Perlskripts im Linux-Magazin
    2 
    3 -l=43  # line width
    4 -i=2   # 2 cols indent
    5 -ci=2  # 2 cols continuation indent
    6 -ce    # cuddled else
    7 -vt=2  # vertical tightness
    8 -nbbc  # no blank lines before whole-line comments

Damit der Formatierer ein Perlskript mit den definierten Optionen ummodelt, ruft der Entwickler ihn mit

    perltidy -pro=pfad/perltidyrc scriptname

auf, worauf er, falls das Skript syntaktisch korrekt ist, die Datei scriptname.tdy erzeugt, die entsprechend umformatiert wurde. Wer möchte, definiert mit

    :nnoremap <buffer> <silent> X :w<Enter>1GdG\
    :.!perltidy -pro=pfad/perltidyrc <%<Enter>

noch ein vim-Kommando, das die Formatierung im Editor vornimmt, falls der Benutzer "X" drückt. Bequemer geht's nun wirklich nicht mehr.

Infos

[1]

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

[2]

"Klimaforschung", Michael Schilli, Linux Magazin 10/2010, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2010/10/Klimaforschung

[3]

"ack", http://betterthangrep.com

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.