Tipp-Ex für den Bildschirm (Linux-Magazin, Juni 2010)

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.

Längliches SQL

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 schottet ab

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.

Listing 1: readline-test

    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 }

Zeichensalat im Debugger

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.

Religionskrieg Emacs-Vi

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.

Wir schreiben Geschichte

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.

Zwangsweise eingewickelt

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.

Listing 2: wrapper-test

    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.

Du komplettierst mich

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.

Listing 3: readline-complete

    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!

Infos

[1]

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

[2]

``Pro Perl'', Peter Wainwright, Apress, 2005, Seite 551, ``Advanced Line Input with Term::ReadLine''.

[3]

``rlwrap'', der aufoktroierende readline-Wrapper, http://utopia.knoware.nl/~hlub/rlwrap/man.html

[4]

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 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.