Aus Linux-Magazin 11/2007

Vim-Makros in Perl schreiben

Der Editor Vim (Vi improved) unterstützt auch Perl-Plugins, die den gerade editierten Text auf Tastendruck manipulieren. Dabei lassen sich mit dem mächtigen Perl komplexe Funktionen deutlich schneller entwickeln als mit Vims eingebauter Skriptsprache.

Wenn sich jemand bei Yahoo für eine Einsteigerposition im Perl-Bereich bewirbt und das Vergnügen hat, bei mir im Interview zu landen, kann es sein, dass ich die folgende Frage stelle: Wie versieht man in Perl ein Listing mit Zeilennummern, wie sie beispielsweise in einer Zeitschrift üblich sind?

Das ist eine recht simple Aufgabe und jeder Kandidat löst sie. Frage ich aber, was zu tun wäre, damit die Zeilennummern bündig sind, kommen manche Prüflinge ins Schleudern.

Im Stress

Hat das Listing nämlich neun Zeilen, sind alle Zeilennummern einstellig. Bei Listings zwischen zehn und 99 Zeilen Länge sind sie zweistellig, wobei die einstelligen Nummern von 1 bis 9 linksseitig mit Nullen aufzufüllen sind (01 bis 09). Längere Listings, die aus mehr als 100 und weniger als 1000 Zeilen bestehen, verlangen dreistellige Zeilennummern, der Programmierer muss sie ab 001 durchnummerieren.

Nun formatiert Perls eingebaute »printf«-Funktion zwar Ziffern mit führenden Nullen. Mit einem Formatstring »%03d« aufgerufen, macht sie aus dem Integer »3« den String »003«, »99« verwandelt sich in »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.

Liegt die Breite der höchsten Zeilennummer in der Variablen »$numlen« vor, hilft ein dynamisch zusammengebauter Formatstring (»”%0″ . $numlen . “d”«). Das so in einem String abzulegen funktioniert aber nicht auf Anhieb, denn in »”%0$numlend”« würde Perl erfolglos nach der Variablen »$numlend« suchen. Alte Perl-Hasen wissen aber, dass sich ein Skalar statt als »$numlen« auch als »${numlen}« schreiben lässt. Das löst das Problem: »”%0${numlen}d”«.

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

Zurück zur Schulbank

Doch woher die Länge »$numlen« der letzten Zeilennummer »$num« nehmen? Perl verwandelt 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 so den Wert für »$numlen«.

Es gibt jedoch noch eine weitere Möglichkeit. Im Dezimalsystem sind die Ziffern einer Zahl von rechts nach links mit 100, 101, 102 und so weiter gewichtet. Die Zahl 15 zerlegt sich so zu 5*100 +1*101. Die Zahl 100, die drei Ziffern lang ist, lässt sich als 1*102 schreiben. Die Zahl 1000, die vier Ziffern lang ist, entspricht 1*103. Aus wie vielen Ziffern besteht also eine Zahl N?

Wer in der Schule aufgepasst hat, erinnert sich, dass das Ergebnis von “10 hoch wie viel 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, weiß noch, dass sich der Logarithmus von N zur Basis x, also logx N, bestimmen lässt, indem man logy N durch logy 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.

Viele Wege

Das Skript »linenum« in Listing 1 zeigt eine Möglichkeit, das Problem anzupacken. Es liest zunächst alle Zeilen eines durch den Dateinamen angegeben oder über STDIN hereinsprudelnden Skripts in den Array »@lines« ein. Das ist natürlich nur bei kleinen Dateien sinnvoll, aber wer Perl-Skripte mit mehr als 100000 Zeilen schreibt, sieht eh einer düsteren beruflichen Zukunft entgegen. Den Formatstring baut Perls Punktoperator (».«) zusammen, sodass etwas wie »”%02d %s”« entsteht.

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 }

Das Schöne an der beschriebenen Prüfungsaufgabe ist freilich, dass es nicht eine dieser unnützen Puzzle-Aufgaben ist, deren Lösung der Kandidat entweder schon gehört hat oder trotz Stress-Situation zufällig löst. Falls es nicht gleich klingelt, kann der Prüfer – ich – weiterhelfen und sehen, ob der Kandidat zuhören kann und auf Vorschläge eingeht.

Es gibt viele Lösungsmöglichkeiten, deren Vor- und Nachteile sich diskutieren lassen. Was passiert, wenn die Datei plötzlich 10 GByte groß ist? Welcher Ansatz ist der schnellste? Was ist zu beachten, falls die Datei in Unicode kodierte Zeichen beinhaltet?

Externes Perl

Wie lässt sich das Ganze nun in Vim programmieren, sodass nur eine Taste zu drücken ist, um das gesamte Listing durchzunummerieren? 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 Skript »linenum« weiter, das wegen des Ausrufezeichens extern startet. Dessen Ausgabe schnappt sich Vim anschließend wieder und ersetzt die unbearbeiteten Zeilen damit (Abbildungen 1 und 2).

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 jedwede Fehlermeldungen 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 [Enter]-Taste. Fehlt es, schreibt Vim nur die Kommandozeile voll und wartet darauf, dass der Benutzer sie mit [Enter] abschickt. Wer statt des gesamten Dokuments nur einen Bereich von Marker »a« bis Marker »b« nummerieren möchte, muss statt »1,$« den Bereich »\’a,\’b« wählen.

Abbildung 1: Das Listing ohne ...

Abbildung 1: Das Listing ohne …

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

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

Vims Perl

Vim verfügt über einen einkompilierten Perl-Interpreter, der Installateur muss ihn nur extra konfigurieren, damit der Compiler ihn mit übersetzt. Ein Skript stellt zur Laufzeit fest, ob der Interpreter vorhanden ist oder nicht, und bricht im Fehlerfall mit einer erklärenden Meldung ab, bevor eine Funktion wegen eines ihr unverständlichen Kommandos unvermittelt abstürzt. Das erledigt die Abfrage »has(\’perl\’)«, die einen wahren Wert liefert, falls Perl vorhanden ist.

Das Listing »vimperl« (Listing 2) definiert in der Vim-Skriptsprache eine Vim-Funktion »Linenum()«, die bündige 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 »:echo« eine Meldung auf der Statuszeile aus.

Listing 2:
»vimperl«

01 :function! Linenum()
02
03  :if !has('perl')
04   :echo "Sorry, no Perl!"
05   return
06  :endif
07
08  perl <<EOT 
09    $numlen = length($curbuf->Count());
10    for $num (1..$curbuf->Count()) {
11      $newline = sprintf "%0.*d %s", 
        $numlen, $num, $curbuf->Get($num);
13    $curbuf->Set($num, $newline);
14    }
15  EOT
16  :endfunction
17
18 :command! Linenum :call Linenum()

Wie unter [3] nachzulesen ist, zeigt »$curbuf« in Vims Perl-Interpreter automatisch auf den aktuellen Puffer, in dem die Zeilen der gerade bearbeiteten Datei liegen. Die Methode »Count()« liefert deren Anzahl. Die folgende »for«-Schleife (in Zeile 10) iteriert über alle Zeilen des aktuellen Puffers und holt sie mit »$curbuf->Get($num)« herein, wobei »$num« die Nummer der gerade bearbeiteten Pufferzeile ist.

Die um die Zeilennummer angereicherte Zeile schreibt »$curbuf->Set()« später wieder zurück. Als Argumente nimmt es dabei die Nummer der Zeile und deren neuen Inhalt entgegen.

At your fingertips

Der Aufruf »:source vimperl« lädt Listing »vimperl« in den Editor, aber für den Produktionsgebrauch sollte der Anwender es entweder in Vims Initialisierungsdatei ».vimrc« integrieren oder in einem der Plugin-Verzeichnisse von Vim ablegen. Für eine 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 Perl-Skript steht in einem Here-Dokument, das mit »<<EOT« anfängt und mit »EOT« aufhört. Allerdings ist dabei noch zu beachten, dass das abschließende »EOT« (End of Text) unbedingt am Anfang einer Zeile stehen muss, denn sonst kann Perl das Ende des Skripts nicht erkennen.

Nachdem »:endfunction« das Ende der Funktionsdefinition anzeigt, definiert »vimperl« (Listing 2) mit »:command« auch noch ein Kommando »Linenum«, das der Benutzer mit »:Linenum« von Vims Kommandozeile aus aufrufen oder auf eine Taste mappen kann. Auch der Name dieses benutzerdefinierten Kommandos soll nach Vims Willen mit einem Großbuchstaben beginnen. Das 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 in Zukunft umstellen. Hoffentlich finde ich dabei genauso schwere und vielleicht noch interessantere! (jcb)

Infos

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

[2] 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/05: [https://www.linux-magazin.de/heft_abo/ausgaben/2005/07/tipps_fuer_tippfaule__1]

Der Autor


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.

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