Wer einem handgeschriebenen interaktiven Tool einen praktischen History-Mechanismus mit Editierfunktion beibringen möchte, braucht nur die GNU Readline und History Libraries einzubinden und jeweils eine ihrer Funktionen zu nutzen.
In unserer Reihe ``Reliquien aus der Steinzeit der Datenverarbeitung
die jeder nutzt aber kaum jemand kennt'' möchte ich heute das Augenmerk der
werten Leserschaft auf die Gnu-Utilities readline
und
history
lenken. Mit ihnen
kann jedes Kommandozeilenprogramm ohne viel Eigeninitiative einen
Mechanismus zum Editieren und Wiederholen von User-Eingaben anbieten.
Wer zum Beispiel im
MySQL-Client mysql
eine längliche SQL-Abfrage eintippt, weiß es sicher
zu schätzen, dass er selbige durch einen Druck auf die Cursor-Up-Taste beim
nächsten Eingabe-Prompt wieder hervorholen kann, entweder,
um sie nochmals abzusetzen oder um sie zu modifizieren.
Diese Funktionen bestehen
übrigens unabhängig vom Kommando ``\e'', mit dem der mysql-Client eine weitere
Möglichkeit der Eingabekorrektur bietet.
Auch wenn sich beim
Eingeben einer langen Zeile das erste Wort als falsch herausstellt,
fährt der User einfach mit dem Cursor zurück zum Anfang und berichtigt
den Fehler noch vor dem Abschicken des Kommandos.
Abbildung 1: Eine Kommandozeilen-Session mit dem MySQL-Client mysql. |
Wie ein Blick auf den MySQL-Sourcecode verrät, implementiert mysql
den
praktischen Mechanismus nicht etwa selbst, sondern nutzt lediglich
die C-Funktionen readline()
und add_history()
der GNU Readline und
History Libraries zum
Einlesen der User-Eingaben und Hinzufügen ausgewählter Kommandos zum
später aufrufbaren History-Pool. Gnu-typisch bringen die Befehle
``info readline'' und ``info history'' die vollständigen Manualseiten
der Utilities im 70er-Jahre-Look ans Licht. Mit der ``n''-Taste springt
info
zum nächsten Kapitel, mit ``p'' zurück zum zuletzt gesichteten,
und die TAB-Taste tastet sich innerhalb einer Seite zum nächsten
verlinkten Aufzählungspunkt vor.
Sogar nach einem Neustart des Mysql-Clients ist die History-Information
der letzten Session noch abrufbar. Das Geheimnis: Wie Abbildung 2 zeigt,
legt der in mysql verwendete Gnu-History-Mechanismus
die Information in der Datei ~/.mysql_history ab.
Das letzte Kommando ``quit'' erscheint nicht in der History-Datei, da
mysql
nur nutzbringende Kommandos speichert und bei ``quit'' noch
vor dem Aufruf von add_history()
die Bearbeitung abbricht.
Abbildung 2: In der Datei ~/.mysql_history liegen die eingegebenen Kommandozeilen für später aufgerufene Sessions abrufbereit vor. |
Perl bietet verwöhnten Skriptprogrammierern eine komfortable Schnittstelle zu den GNU-Libraries an. Das Perl-Modul Term::ReadLine::Gnu vom CPAN kommuniziert hierfür mit dem C-Layer der ebenfalls installierten GNU-Libraries und präsentiert sich dem Perl-Programmierer als objektorientierte Schicht. Das Modul Term::ReadLine liegt Perl-Distributionen von Anfang an bei, bietet allerdings nur eingeschränkte Funktionen. Erst durch die Installation von Term::ReadLine::Gnu vom CPAN ist Term::ReadLine voll funktionsfähig.
Listing readline-test
erzeugt ein Objekt der Klasse
Term::ReadLine und ruft dessen Methode readline()
auf.
Diese fordert
den User dann mit dem ihr überreichten String (``input>'')
zur Eingabe eines Kommandos
auf. Enthält das eingegebene Kommando verwertbare Zeichen
(im Listing: alles außer Leerzeichen), ist
es sinnvoll, es mit add_history()
in den Zeilenspeicher zu übernehmen,
um es später mittels der Cursortasten wieder hervorkramen zu können.
Weitere Informationen zur Terminal-Programmierung unter Perl
sind in den Manualseiten der beiden CPAN-Module sowie spärlich in der
Perl-Literatur, wie zum Beispiel in [2], zu finden.
01 #!/usr/local/bin/perl -w 02 use strict; 03 04 use Term::ReadLine; 05 my $term = Term::ReadLine->new("myapp"); 06 07 while (1) { 08 my $input = $term->readline("input>"); 09 last unless defined $input; 10 print "Input was '$input'\n"; 11 12 if($input =~ /\S/) { 13 $term->addhistory($input); 14 } 15 }
Auch der eingebaute Perl-Debugger verfügt über einen History-Mechanismus, damit der User nicht ewig die gleichen Kommandos eintippen muss. Kommt allerdings auf den Druck einer Cursortaste nur ein Zeichensalat wie
perl -d test.pl DB<1> $ ^[[A
zum Vorschein, ist dies ein sicheres Zeichen dafür, dass vergessen wurde, den Perl-Wrapper der GNU Readline Library mit
cpan> install Term::ReadLine::Gnu
vom CPAN zu installieren. Perls Debugger findet nämlich am Programmstart heraus, ob das Gnu-Modul verfügbar ist und stellt bei dessen Abwesenheit eine zwar funktionale, aber eingeschränkte Tippumgebung ohne History-Funktionen bereit.
Ohne manuellen Eingriff erfolgt die Cursor-Navigation mittels Emacs-Kommandos, was für vi-Liebhaber befremdlich wirkt. Bekanntlich hat sich schon so mancher beim Tippen der komplizierten Tastenkombinationen den Handwurzelknochen gebrochen. Mit der Option
# ~/.inputrc set editing-mode vi
in der Datei ~/.inputrc
im Home-Verzeichnis lässt sich dies aber
ruckzuck beheben, denn damit springt Readline automatisch in den vi-Modus.
Fällt erst während des Tippens auf, dass Readline sich nicht im
gottgewollten Editormodus befindet, schaltet die Tastenkombination
Meta-Control-j um. Dies klingt
zwar Emacs-verdächtig, doch auch der vi-Modus versteht sie und schaltet
daraufhin in den emacs-Modus um. Verfügt das Keyboard über keine
Meta-Taste, kann man hierfür auch erst die ESC-Taste kurz antippen, und dann
Control-j drücken.
Statt CTRL-B, um den Cursor ein Zeichen nach links zu rücken, tippt der vi-Freund dann ESC, um in den Command-Modus zu gelangen und anschließend einfach so oft ``h'', bis der Cursor an der gewünschten Stelle weiter links steht. Mit ``i'' geht's dann wieder zurück in den Insert-Modus.
In einer History, die dutzende von Kommandos umfasst, sucht der User schneller nach bestimmten Einträgen, statt durch Reihen unpassender Kommandos zu blättern. Im vi-Modus schaltet die ESC-Taste zunächst in den Command-Modus, in dem dann ein Querstrich, gefolgt von Teilen des gesuchten Texteintrags und der Return-Taste eine Liste von Treffern zurückgibt, durch die die Taste ``n'' (next) vorwärts und die Taste ``p'' (previous) rückwärts blättert.
Erscheint der gewünschte History-Eintrag, schickt die Return-Taste ihn ab, doch auch weitere Editiervorgänge sind mit gängigen vi-Kommandos möglich. Im emacs-Modus sucht Control-r rückwärts und zeigt in diesem aktiven Suchmodus zu teilweise eingegebenen Zeichenketten jeweils passende Treffer an (Abbildungen 3 und 4).
Abbildung 3: Im Emacs-Suchmodus holt Readline auf den Buchstaben "o" hin das letzte passende Kommando hervor ... |
Abbildung 4: ... und tippt der User weiter ("ow"), findet der Mechanismus ein weiter zurückliegendes Kommando. |
Programmen, die ohne readline-Funktionen programmiert wurden und denen
deswegen die Funktionen zum Merken und Editieren von Eingabezeilen
völlig abgehen, bringt der Wrapper rlwrap
([3]) die notwendigen Tricks bei.
Listing wrapper-test
zeigt ein einfaches Perlskript, das mit dem
Perl-typischen Konstrukt <STDIN>
dreimal von der Standardeingabe
eine Benutzereingabe entgegennimmt und diese anschließend ausgibt.
01 #!/usr/local/bin/perl -w 02 use strict; 03 04 $| = 1; 05 06 for(1..3) { 07 print "Input> "; 08 09 my $in = <STDIN>; 10 chomp $in; 11 12 print "You said '$in'\n"; 13 }
Abbildung 5: Ohne Readline-Unterstützung bringt die Cursortaste keine alten Einträge hervor, sondern Zeichensalat. |
Abbildung 5 zeigt, wie der User auf die
Cursor-Up-Taste drückt, um den letzen Eintrag erneut abzuspulen, aber auf
Unverständnis stößt und einen aus ^[[A
bestehenden Zeichensalat erntet.
In Abbildung 6 hingegen wirft der User das unveränderte Skript mit dem Wrapper
rlwrap
an und, siehe da, ein Tastendruck auf Cursor-UP in der dritten
Eingabezeile zaubert die in der zweiten Eingabezeile getippten Daten wieder
hervor. Die Daten bestehen sogar über den Aufruf des Skripts hinaus weiter,
wer neugierig im Home-Verzeichnis schnüffelt, findet sie in der Datei
.wrapper-test_history
. Zauberei? Nein, rlwrap
überlädt lediglich
mit LD_PRELOAD die Eingabefunktionen des ursprünglichen Programms und ersetzt
sie durch Wrapper, die mit Gnus Readline- und History-Libraries ticken.
Abbildung 6: Der Wrapper rlwrap bringt dem unveränderten Programm Readline-Unterstützung bei und Cursor-UP bringt den letzten Eintrag nochmal hervor. |
Readline bietet nicht nur eine Editierfunktion, sondern komplettiert auf den Druck der TAB-Taste hin auch unvollständige Eingaben. Ähnlich wie der in [4] kürzlich hier vorgestellte Bash-Komplettiermechanismus kann der Anwender auch diese Funktion programmatisch individuell anpassen.
Listing readline-complete
zeigt ein Beispiel eines Kommandointerpreters,
der die Befehle install
, remove
und quit
beherrscht. Die API der
Kommandoergänzung der readline-Library ist etwas kompliziert, denn unter
dem Eintrag completion_entry_function
erwartet sie einen Callback, den
readline mehrmals aufruft, falls der User nur einmal TAB drückt, solange,
bis alle Vorschläge vorliegen. Readline legt dem Aufruf des Callbacks
jeweils zwei Parameter bei, $count und $word. Der Parameter
$word ist dabei das zu
ergänzende Wort (also der String, an dessen Ende der Cursor stand als
der User TAB drückte) und $count
ist beim ersten Aufruf 0 und wird
bei folgenden Aufrufen hochgezählt. Die Callback-Funktion initialisiert
sich also, wenn $count den Wert 0 führt und liefert dann bei jedem weiteren
Aufruf mit $count != 0 einen Wert aus einer Liste von möglichen
Ergänzungen zurück. Ein Rückgabewert von undef signalisiert hingegen,
dass die Liste abgearbeitet ist.
Falls zu einem angefangenen Wort also nur eine Ergänzung in Frage kommt,
liefert der Callback beim Aufruf mit $count gleich 0 das Ergebnis
zurück und im darauffolgenden Aufruf mit $count gleich 1 den Wert undef
.
Zum Glück hat Term::ReadLine::Gnu für solche einfachen Fälle
schon eine Callbackfunktion vorbereitet, die unter
dem Eintrag list_completion_function
unter der Referenz $attribs
verfügbar ist und Ergänzungen
aus einem Wort-Array unter dem Hasheintrag completion_word
abarbeitet.
01 #!/usr/local/bin/perl -w 02 use strict; 03 04 use Term::ReadLine; 05 my $term = Term::ReadLine->new('myapp'); 06 my $attribs = $term->Attribs; 07 $attribs->{completion_entry_function} = 08 $attribs->{list_completion_function}; 09 10 $attribs->{completion_word} = 11 [qw(install remove quit)]; 12 13 while(1) { 14 my $cmd = $term->readline("myapp> "); 15 last if $cmd =~ /^quit/i; 16 }
Statt langwierig eine Callback-Funktion schreiben zu müssen, legt
der Programmierer also lediglich eine Liste komplettierbarer Schlüsselwörter
im Hash-Eintrag completion_word
ab und setzt den
Wert von completion_entry_function
auf die unter dem Schlüssel list_completion_function
liegende
Funktionsreferenz. Fragt das Skript in
Listing readline-complete
also mit myapp>
nach einem Kommando
und der User gibt i [TAB]
ein, vervollständigt Readline dies sofort
zu install
, da dies der einzige passende Eintrag im Array unter
completion_word
ist. Die geschundenen Programmiererfinger werden es
ihrem Besitzer danken!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/06/Perl
``Pro Perl'', Peter Wainwright, Apress, 2005, Seite 551, ``Advanced Line Input with Term::ReadLine''.
``rlwrap'', der aufoktroierende readline-Wrapper, http://utopia.knoware.nl/~hlub/rlwrap/man.html
Michael Schilli, ``Stets zu Diensten'' (Bash-Komplettierung), Linux-Magazin 04/2010, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2010/04/Stets-zu-Diensten =back
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. |