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?
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.
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?
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. |
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 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.
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!
Michael Schilliarbeitet 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. |