Viele Wege zum Ziel (Linux-Magazin, November 2007)

Der Editor vim (Vi IMproved) unterstützt auch Perl-Plugins, die den gerade editierten Text auf Tastendruck manipulieren. In der mächtigen Skriptsprache Perl geht die Entwicklung komplexerer Funktionen deutlich schneller als mit Vims eingebauter Skriptsprache.

Wenn sich jemand bei Yahoo für eine Einsteiger-Position im Perlbereich bewirbt und das Vergnügen hat, bei mir im Interview zu landen, kann es sein, dass er die folgende Frage gestellt bekommt: Wie bitte versieht man in Perl ein Listing mit Zeilennummern, um es in einer Zeitschrift abzudrucken?

Prüfungsstress

Das ist eine recht simple Aufgabe, und jeder Kandidat löst sie. Frage ich aber, wie man es anstellt, dass die Zeilennummern bündig sind, kommen manche Prüflinge schon ins Schleudern. Hat man nämlich ein Listing mit 9 Zeilen, sind alle Zeilennummern einstellig. Bei Listings zwischen 10 und 99 sind sie zweistellig, wobei die einstelligen Nummern von 1 bis 9 linksseitig mit Nullen aufgefüllt werden (01 - 09). Längere Listings, die aus mehr als 100 und weniger als 1000 Zeilen bestehen, verlangen dreistellige Zeilennummern, sie werden ab 001 durchnumeriert.

Perls eingebaute printf-Funktion formatiert Ziffern mit führenden Nullen. Mit einem Formatstring %03d aufgerufen, macht sie aus dem Integer 3 den String ``003'', 99 wird ``099'', und 100 bleibt ``100''.

Wie aber pumpt printf den String auf eine variable Länge auf? Falls mir jemand ein if/elsif-Konstrukt vorschlägt, das eine limitierte Anzahl von Ziffernlängen abprüft, gehen die Alarmglocken los und die Falltür zum Haifischbecken öffnet sich automatisch. Kleiner Scherz! Liegt die Spaltenbreite der höchsten Zeilennummer in der Variablen $numlen vor, hilft ein dynamisch zusammengebauter Formatstring (``%0'' . $numlen . ``d''). Alles in einen String hineinzuschreiben funktioniert nicht auf Anhieb, denn in "%0$lend" würde Perl erfolglos nach der Variablen $lend suchen. Alte Perlhasen wissen aber, dass man einen Skalar statt als $numlen auch als ${numlen} schreiben kann, und das klärt die Situation: "%0${numlen}d".

Mit der Programmiersprache C aufgewachsene Haudegen wissen vielleicht auch noch, dass printf variable Formatfelder mit dem Platzhalter .* und einem zusätzlichen Parameter erlaubt. Der Aufruf printf("%0.*d", 3, 1) pumpt die Zahl 1 mit zwei führenden Nullen auf die Gesamtbreite 3 auf. Ersetzt man 3 durch eine Variable, hat man eine weitere Möglichkeit, die Zeilennummer auf eine dynamisch vorgegebene Breite aufzufüllen.

Zurück zur Schulbank

Doch wie bestimmt man die Länge $numlen der letzten Zeilennummer $num nun programmatisch? In Perl verwandelt sich eine Zahl bekanntlich leicht in einen String, dessen Länge sich einfach durch die eingebaute Funktion length() ermitteln lässt: Das Ergebnis des Aufrufs length($num) liefert den gesuchten Wert für $numlen.

Und noch eine weitere Möglichkeit gibt es. Man bedenke folgendes: Im Dezimalsystem sind die Ziffern einer Zahl von rechts nach links mit 10^0, 10^1, 10^2 und so weiter gewichtet. Die Zahl 15 zerlegt sich so zu 5 * 10^0 + 1 * 10^1. Die Zahl 100, die drei Ziffern lang ist, lässt sich als 1 * 10^2 schreiben. Die Zahl 1000, die vier Ziffern lang ist, entspricht 1 * 10^3. Aus wievielen Ziffern besteht also eine Zahl N?

Wer in der Schule aufgepasst hat, erinnert sich, dass das Ergebnis von ``10 hoch wieviel ist X?'' der Zehner-Logarithmus von X ist. Perl hat zwar keinen Zehner-Logarithmus, aber die Funktion log(), die den Logarithmus einer Zahl zur Basis e (der Eulerzahl) bestimmt. Und wer über ein Elefantengedächtnis verfügt, weiss noch, dass man den Logarithmus von N zur Basis x, also log_x N, bestimmt, indem man log_y N durch log_y y teilt. Im vorliegenden Fall ermittelt sich der Zehnerlogarithmus von N in Perl also durch log(N)/log(10). Die Ziffernlänge von N ergibt sich aus dem auf die nächste Ganzzahl abgerundeten und dann um 1 erhöhten Ergebnis der Logarithmusoperation.

Das Skript linenum in Listing 1 zeigt eine Möglichkeit, das Problem anzupacken. Es liest zunächst alle Zeilen eines per Dateiname angegeben oder über STDIN hereinsprudelnden Skripts in den Array @lines ein. Dies ist natürlich nur bei kleinen Dateien sinnvoll, aber wer Perlskripts mit mehr als 100.000 Zeilen schreibt, sieht eh einer düsteren beruflichen Zukunft entgegen. Der Formatstring wird mit Perls Punktoperator (".") zusammengebaut, sodass etwas wie ``%02d %s'' entsteht.

Das schöne an der beschriebenen Prüfungsaufgabe ist freilich, dass es nicht einer dieser unnützen Puzzle-Aufgaben ist, deren Lösung der Kandidat entweder schon gehört hat oder trotz Stresssituation zufällig löst. Falls es nicht gleich klingelt, kann man weiterhelfen und sehen, ob der Kandidat zuhören kann und auf Vorschläge eingeht. Es gibt viele Lösungsmöglichkeiten, deren Vor- und Nachteile man diskutieren und je nach Anforderungen auswählen kann. Was passiert, wenn die Datei plötzlich 10 Gigabyte groß ist? Welcher Ansatz ist der schnellste? Was ist zu beachten, falls die Datei in Unicode kodierte Zeichen beinhaltet?

Listing 1: linenum

    01 #!/usr/bin/perl -w
    02 use strict;
    03 
    04 my @lines = <>;
    05 
    06 my $numlen = length scalar @lines;
    07 
    08 my $num = 1;
    09 
    10 for my $line (@lines) {
    11     printf "%0" . $numlen . "d %s", 
    12            $num++, $line;
    13 }

Abbildung 1: Das Listing ohne ...

Abbildung 2: ... und auf Tastendruck mit bündigen Zeilennummern.

Externes Perl

Wie lässt sich das Ganze nun in Vim programmieren, so dass man nur eine Taste drücken muss, um das gesamte Listing durchzunumerieren? Als einfachste Lösung bietet es sich an, Zeilen in einem Bereich einfach durch das Skript als Filter laufen zu lassen. Der Befehl :1,$!lineum schnappt sich alle Zeilen der gerade editierten Datei (von 1 bis zur letzten Zeile $) und reicht sie per STDIN an das wegen des Aufrufezeichens extern aufgerufenen Skripts linenum weiter. Dessen Ausgabe schnappt sich Vim anschließend und ersetzt die bearbeiteten Dateizeilen damit. Der folgende map-Befehl in .vimrc legt das Kommando auf die Taste ``L'' im Normalmodus:

    :map L :silent :1,$!linenum<Return>

falls sich linenum ausführbar im Pfad der gerade laufenden Shell befindet. Die Option :silent würgt jedwege Ausgaben ab, sodass das Kommando sauber durchläuft, ohne dass vim irgendwelche Statusmeldungen auf die Konsole schreibt und dafür auch noch nervige Bestätigungen einfordert. Das Kommando <Return> simuliert eine gedrückte Return/Enter-Taste, fehlt es, schreibt vim nur die Kommandozeile voll und wartet darauf, dass der Benutzer sie mit Enter abschickt. Soll statt des gesamten Dokuments nur ein Bereich von Marker a bis Marker b numeriert werden, ist statt 1,$ der Bereich 'a,'b zu wählen.

Vim-Perl

Vim verfügt auch noch über einen einkompilierten Perl-Interpreter. Allerdings muss der Installateur diesen beim Compilieren extra konfigurieren. Automatisch wird perl nicht mitkompiliert. Ein Skript kann zur Laufzeit feststellen, ob perl vorliegt oder nicht und im Fehlerfall mit einer erklärenden Meldung abbrechen, bevor eine Funktion wegen eines ihr unverständlichen Kommandos unvermittelt abkracht. Die Abfrage has('perl') liefert einen wahren Wert, falls perl vorhanden ist.

Listing vimperl definiert in der Vim-Skriptsprache eine Vim-Funktion Linenum(), die die bündigen Zeilennummern in die gerade editierte Datei einschleust. Zu beachten ist, dass vim darauf besteht, dass benutzerdefinierte Funktionen mit einem Großbuchstaben anfangen.

Falls has('perl') anzeigt, dass kein Perl-Interpreter vorhanden ist, gibt die Funktion mit dem Vim-Befehl :echo eine Meldung auf der Statuszeile aus, die diesen Missstand anzeigt.

Wie man unter [3] nachlesen kann, zeigt $curbuf in Vims Perl-Interpreter automatisch auf den aktuell editierten Buffer, in dem die Zeilen der gerade bearbeiteten Datei liegen. Die Methode Count() liefert deren Anzahl einfach als Integerwert zurück.

Die nachfolgende for-Schleife iteriert über alle Zeilen des aktuellen Buffers und holt diese mit $curbuf->Get($num) herein, wobei $num die Nummer der gerade bearbeiteten Bufferzeile ist.

Die um die Zeilennummer angereichterte Zeile schreibt $curbuf->Set() wieder zurück in den Buffer. Als Argumente nimmt die Funktion die Nummer der Zeile und deren neuen Inhalt entgegen.

In Vims Skriptsprache fangen Kommentare übrigens mit einem doppelten Anführungszeichen an und gelten bis zum Ende der aktuellen Zeile.

Listing 2: vimperl

    01 "###########################################
    02 "# linenum.vim - Print listing with numbers
    03 "# Mike Schilli, 2007 (m@perlmeister.com)
    04 "###########################################
    05 :function! Linenum()
    06 
    07   :if !has('perl')
    08     :echo "Sorry, no Perl!"
    09     return
    10   :endif
    11 
    12   perl <<EOT
    13         $numlen = length($curbuf->Count());
    14         for $num (1..$curbuf->Count()) {
    15           $newline = sprintf "%0.*d %s", $numlen,
    16                              $num, $curbuf->Get($num);
    17           $curbuf->Set($num, $newline);
    18         }
    19 EOT
    20 :endfunction
    21 
    22 :command! Linenum :call Linenum()

Der Aufruf :source vimperl lädt Listing vimperl in den Editor, aber für den Produktionsgebrauch sollte es entweder in Vims Initialisierungsdatei .vimrc integriert werden oder in eine von Vims Plugin-Verzeichnissen. In der Testphase ist es ganz praktisch, :function! (mit Ausrufezeichen) zu verwenden, denn dann überschreibt Vim kommentarlos die Funktion, auch wenn sie vorher schon definiert war. Andernfalls bricht er mit einer Fehlermeldung ab. Das Perlskript steht in einem Here-Dokument, das mit <<EOT anfängt und mit EOT aufhört, allerdings ist zu beachten, dass das abschließende EOT am Anfang einer Zeile stehen muss, sonst erkennt Perl das Ende des Skripts nicht.

Nach dem :endfunction, das das Ende der Funktionsdefinitinon anzeigt, definiert vimperl mit :command auch noch ein Kommando Linenum, das der Benutzer mit :Linenum von Vims Kommandozeile aus aufrufen oder auf eine Taste mappen kann. Auch den Namen dieses benutzerdefinierten Kommandos will Vim mit einem Großbuchstaben begonnen sehen. Und Kommando :map L :Linenum <Return> packt den Befehl wiederum auf die L-Taste im Normalmodus.

So, nun ist also die Katze aus dem Sack und ich muss meine Interviewfragen umstellen. Ich hoffe, ich finde gleich schwere und vielleicht noch interessantere!

Infos

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

[2]
Die Homepage des vim-Projekts: http://www.vim.org

[3]
Vims spärliche Perl-Dokumentation, http://www.vim.org/htmldoc/if_perl.html

[4]
Michael Schilli, ``Tipps für Tippfaule'', Linux-Magazin 07/2005, http://www.linux-magazin.de/heft_abo/ausgaben/2005/07/tipps_fuer_tippfaule__1

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.