Tippvorhersage (Linux-Magazin, April 2010)

Der Complete-Mechanismus der Shell vervollständigt angefangene Eingaben, sobald der User die Tabulatortaste drückt. Ein Perlskript erweitert die Funktion maßgeschneidert.

Eigentlich wäre die Tabulatortaste für Shell-Kommandos denkbar ungeeignet, doch seit die Bash-Shell mit ihr intelligent halb eingegebene Befehle, Verzeichnisse und Dateien vervollständigt, nutzen Programmierer diese praktische Funktion so häufig, dass der Tab-Key oft schon ziemlich ausgeleiert in der Tastatur hängt.

Standardrepertoire

Das Standardrepertoire der Bash-Komplettierung zeigt Abbildung 1. Drückt der Benutzer gleich hinter der auf der Kommandozeile eingegebenen Tastenfolge ``lsm'' die Tab-Taste, vervollständigt die Bash dies sofort zu ``lsmod'', dem Kommando zur Abfrage installierter Kernelmodule. Warum? Das eingegebene Buchstabentrio stand am Anfang einer Kommandozeile und deshalb muss es sich um ein ausführbares Kommando handeln. Im eingestellten Pfad ($PATH) fand sich kein anderer Befehl mit diesen drei Anfangsbuchstaben, also tat Bash das einzig Richtige.

Abbildung 1: Normalerweise vervollständigt Bash Kommandos und zu bearbeitende Dateien, sobald der User die Tab-Taste drückt.

Auf der Kommandozeile mit der Nummer 2 gab der User hingegen nur zwei Buchstaben vor, und tippte nach ``ls'' auf die TAB-Taste. Auch hier kommt nur ein Befehl und keine normale Datei in Frage, aber die Vorgabe ist nicht eindeutig, denn im Pfad befinden sich dutzende von Kommandos, die mit ``ls'' beginnen. Also schweigt die Bash still und wartet, bis der Benutzer noch ein weiteres Mal auf die Tab-Taste drückt, worauf sie dann eine kompakte Auflistung aller möglichen Ergebnisse präsentiert und den User weitere Buchstaben eingeben lässt, um die Anzahl der Treffer einzuschränken.

1731 Möglichkeiten

Erfolgt der TAB-Tastendruck im zweiten Wort einer Zeile oder danach, wie in der fünften Kommandozeile in Abbildung 1, folgert der Ergänzungsmechanismus, dass der User dem Kommando ``ls'' eine Datei zugesellen möchte. Allerdings passen darauf sehr viele, so dass die Bash auf einen einmaligen Tab-Druck hin still schweigt und auf einen Doppeldruck hin, wie in Zeile 5 erst einmal nachfragt, ob sie wirklich alle 1731 Möglichkeiten anzeigen soll. Gibt der User mehr Buchstaben vor, wie in den Zeilen 6 und 7, und schränkt damit die Lösungswege ein, folgt auf einen Doppel-Tab wiederum eine Auflistung sinnvoller Ergänzungen. Erst wenn die Wahl eindeutig ist, komplettiert Bash die Zeile. Auch Teilergänzung kommt vor: Gibt der User, wie in Kommandozeile 8, /etc/up vor und drückt einmal auf Tab, vervollständigt die Shell sofort auf /etc/update-, obwohl, wie sich hinterher herausstellt, es mit update-manager und update-notifier zwei Möglichkeiten gibt. Die Teilkomplettierung war jedoch hilfreich, um den Pfad bis zum Scheideweg abzukürzen.

Das auf der Bash-Manualseite (man bash) im Abschnitt ``Programmable Completion'' dokumentierte complete-Kommando erlaubt es dem programmierwütigen Shell-Anwender, das Standardrepertoire gehörig aufzupeppen. Das Projekt ``Bash Completion'' ([2]) bietet eine ganze Sammlung von Ergänzungsregeln zum Download an, die der Nutzer dann in der lokalen Datei .bashrc einbindet. Dieses Gehirnimplantat für die Bash bringt ihr kommandospezifische Regeln bei, und vervollständigt zum Kommandooptionen. Gibt der User zum Beispiel die Zeichenfolge ``git com'' ein und drückt die Tab-Taste, vervollständigt der Mechanismus auf ``git commit'', da dies das einzig verfügbare Subkommando des Versionskontrollwerkzeugs ist, das mit ``com'' anfängt. Weitere Erläuterungen der complete-Funktion finden sich in kurzen Abschnitten in Büchern zum Thema Bash-Shell, zum Beispiel [4] und [5].

Marke Eigenbau

Doch während die Skriptsammlung auf [2] bash-Funktionen baut, wissen erfahrene Perlprogrammierer, dass Shellskripts zwar schnell von der Hand gehen, sich mit steigender Komplexität der Anforderungen wegen begrenzter Kapselungsmöglichkeit der Shell jedoch oft als Sackgasse erweisen. Manch ein Shellskript wäre besser von Grund auf in einer vollständigen Skriptsprache implementiert worden, denn früher oder später wird jedes Bash-Skript eh umgeschrieben, sobald es über einen Prototyp hinauswächst.

Die Vorschläge, die Bash auf ein halb eingegebenes Kommando gibt, lassen sich ebensogut mit einem Perl-Skript generieren. Steht die Direktive

    complete -C helper command

in .bashrc, sodass sie beim Shellstart ausgeführt wird, zieht die Bash das Programm helper für Vorschläge zum Kommando ``command'' zu Rate. Gibt der User ``command '' ein (mit abschließendem Leerzeichen) und drückt die Tab-Taste, ruft die Bash das Perlskript helper auf, und übergibt ihm in den Environment-Variablen COMP_LINE und COMP_POINT die bisher eingegebene Kommandozeile und die Position des Cursors, während der User die Tab-Taste gedrückt hat. Als Argumente (verfügbar in @ARGV in Perl) erhält der Helfer das erste Wort der Zeile (normalerweise das Kommando), das zu komplettierende Wort, und als drittes Argument das Wort davor. Vom Helfer erwartet die Shell anschließend als Rückgabe eine Reihe von mit Zeilenumbrüchen getrennte Ergänzungsvorschläge.

Die Shell-Session in Abbildung 2 definiert als Helferlein das Skript complete-dump zum Kommando ls. Listing complete-dump zeigt, dass das Helferskript zu Forschungszwecken lediglich den Inhalt der Environment-Variablen COMP_LINE und COMP_POINT und den Array @ARGV über die Stderr-Ausgabe zurückliefert. Auf Stdout kommt nichts zurück, was den Komplettiermechanismus dazu veranlasst, dem User keinerlei Vorschläge zu unterbreiten.

Abbildung 2: Das Shell-Befehl "complete" weist mit der Option -C dem Kommando "ls" ein Helferskript "complete-dump" zu, das die Shell dann zur Komplettierung aufruft.

Listing 1: complete-dump

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Data::Dump qw(dump);
    04 
    05 my %matches = ();
    06 
    07 for my $env_var (keys %ENV) {
    08   next if $env_var !~ /^COMP_/;
    09   $matches{ $env_var } = $ENV{ $env_var };
    10 }
    11 
    12 $matches{ ARGV } = \@ARGV;
    13 
    14 print STDERR "\n", 
    15       dump( \%matches );

Die Ausgabe zeigt, dass Bash dem Helferskript in COMP_LINE die bislang eingegebene Kommandozeile überreicht, einschließlich aller Leerzeichen. Warum neben COMP_LINE auch noch die Cursorposition in COMP_POINT angezeigt wird, hängt wohl damit zusammen, dass der User die Kommandozeile mit den Cursortasten editieren und plötzlich in der Mitte des Kommandos TAB drücken könnte, obwohl dies praktisch selten von Nutzen ist. Im Normalfall entspricht COMP_POINT genau der Länge des Strings in COMP_LINE.

Kommandozeile Nummer 3 versucht, das erste Argument zum ls-Kommando zu vervollständigen und erhält als Argumente ``ls'' (erstes Wort), ``/etc/p'' (zu vervollständigendes Wort) und noch einmal ``ls'' (das Wort davor). Im vierten Fall, der das zweite Argument zu ls vervollständigen möchte, kommt als drittes Helfer-Argument ``/etc'' zurück -- wieder das Wort vor dem zu vervollständigen Wort.

Eingebautes Helferlein

Selbstgeschriebene Skripts können die Helferfunktionen auch gleich selbst mitbringen. Das Kommando

    complete -C myscript myscript

definiert, dass Bash myscript selbst um Rat fragt, falls ein User nach Eingabe von "myscript " auf die Tab-Taste hämmert. Das Skript fragt dann ab, ob "COMP_LINE" gesetzt ist, und stellt in diesem Fall Vorschläge bereit, während es sonst seine normale Funktionen ausführt. Das ist natürlich ein Balanceakt, denn ein Programmierfehler im Skript könnte eine zerstörerische Funkion auslösen, während der User das Kommando noch gar nicht eingegeben hat, sondern noch auf Ergänzungsvorschläge wartet. Das CPAN-Modul Getopt::Complete, das Skripts elegant ihre eigenen Optionen komplettieren lässt, schlägt deshalb als konservative Lösung vor, das Skript im Helfermodus mit ``perl -c myscript 2>/dev/null'' nur in Perls Compile-Phase eintreten zu lassen, und gar nicht erst auszuführen ([3]).

Listing 2: getopt-complete

    1 #!/usr/local/bin/perl -w
    2 use strict;
    3 
    4 use Getopt::Complete(
    5    'bgcolor' => ['red', 'blue', 'green'],
    6 );

Listing getopt-complete zeigt ein kurzes Beispiel, das die Option --bgcolor zum Setzen der Hintergrundfarbe anbietet und drei Farbwerte akzeptiert. Wurde vorher complete -C getopt-complete getopt-complete aufgerufen, komplettiert die Shell nicht nur Farbwerte, sondern auch Optionsnamen:

    $ getopt-complete [TAB]
      -> getopt-complete --bgcolor=
    $ getopt-complete --bgcolor=r[TAB]
      -> getopt-complete --bgcolor=red

Fertig kompilierte Programme, die man nicht umschreiben möchte, benötigen allerdings externe Helferlein. Listing github-helper zeigt ein Beispiel, das einem User, der offensichtlich ein Git-Repository mit ``git clone'' klonen möchte eine Liste aller seiner auf github.com liegenden Repositories als Vorschläge unterbreitet.

Auf Bewährtes zurückgreifen

Findet das Helferlein allerdings nichts passendes, springt dummerweise auch der Default-Mechanismus nicht mehr an. Das ist unschön, falls der User ``git add '' tippt und darauf hofft, dass die Shell naheliegende Dateien vorschlägt, was sie aber nicht tut, da github-helper für diesen Fall nichts parat hat. Dies behebt die Option -o default, die auf den Ergänzungsmechanismus der Shell zurückgreift, falls der Helfer nichts daherbringt. Das Kommando

    complete -C github-helper -o default git

in der Datei .bashrc behebt das Problem. Wer auch noch möchte, dass eventuell per [3] definierte Bash-Ergänzungen vorher berücksichtigt werden, fügt noch -o bashdefault hinzu. Ruft der User das Programm mit dem vollen Pfad, also /usr/bin/git auf, sucht die Shell zunächst nach einem complete-Eintrag für den vollen Pfad, und fällt, falls keiner definiert wurde, auf den Programmnamen, also git zurück. Der oben generierte Eintrag funktioniert also in beiden Fällen.

Listing 3: github-helper

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Pod::Usage;
    04 use LWP::UserAgent;
    05 use XML::Simple;
    06 
    07 my $netloc = 'git@github.com';
    08 my $user   = 'mschilli';
    09 
    10 if(!defined $ENV{COMP_LINE}) {
    11   pod2usage("COMP_LINE missing");
    12 }
    13 
    14 my($git, $clone, $args) = 
    15     split /\s+/, $ENV{COMP_LINE}, 3;
    16 
    17 $args = "" unless defined $args;
    18 
    19 if(!defined $clone or
    20     $clone ne "clone") {
    21     # Only 'clone' suggestions
    22   exit(0);
    23 }
    24 
    25 if($ARGV[2] ne "clone") {
    26     # Do nothing if user doesn't want
    27     # to expand the argument after 'clone'
    28   exit 0;
    29 }
    30 
    31   # Two pseudo choices to get their
    32   # common path expanded right away
    33 if(!length $args) {
    34   for (1..2) {
    35     print "$netloc/$user/$_\n";
    36   }
    37   exit 0;
    38 }
    39 
    40 my @repos = remote_repos( $user );
    41 
    42 for my $repo (remote_repos( $user )) {
    43   my $remote = "$netloc/$user/$repo";
    44 
    45   if($args eq
    46      substr($remote, 0, length $args)) {
    47     print "$remote\n";
    48   }
    49 }
    50 
    51 ###########################################
    52 sub remote_repos {
    53 ###########################################
    54   my($user) = @_;
    55 
    56   my @repos = ();
    57 
    58   my $ua = LWP::UserAgent->new();
    59   my $resp = $ua->get(
    60    "http://github.com/api/v1/xml/$user");
    61 
    62   if($resp->is_error) {
    63     die "API fetch failed: ", 
    64         $resp->message();
    65   }
    66 
    67   my $xml = XMLin($resp->decoded_content());
    68 
    69   for my $repo (keys 
    70     %{$xml->{repositories}->{repository}}
    71   ) {
    72     push @repos, $repo;
    73   }
    74 
    75   return @repos;
    76 }
    77 
    78 __END__
    79 
    80 =head1 NAME
    81 
    82     github-helper - Complete github repos
    83 
    84 =head1 SYNOPSIS
    85 
    86     COMP_LINE=... github-helper

Ruft ein unwissender User das Skript ohne die gesetzte Environment-Variable COMP_LINE auf, bricht es ab, indem es mit dem CPAN-Modul Pod::Usage und dessen Funktion pod2usage() seine unten anhängende POD-Dokumentation ausgibt. Zeile 14 bricht den bisher eingegebenen Kommandozeilenstring in maximal drei Teile, die durch Leerzeichen voneinander getrennt sind. Falls das Git-Subkommando nicht clone ist (also zum Beispiel git add aufgerufen wurde), beendet sich das Skript ohne jegliche Ausgabe, in Zeile 22, um dem Complete-Mechanismus mitzuteilen, dass es nichts beizutragen hat und statt dessen etwaige andersweitig definierten Complete-Funktionen zum Zuge kommen sollen. Zeile 25 prüft ab, ob der User sich tatsächlich beim Tippen des Arguments nach ``clone'' befindet, und der Cursor nicht etwa direkt hinter ``clone'' (ohne Leerzeichen) steht.

Damit die Shell nach ``git clone '' (mit abschließendem Leerzeichen) ohne mit dem Github-Server Kontakt aufzunehmen, auf einen Tab-Druck hin sofort git@github.com/mschilli hinschreibt, greift das Skript in Zeile 33 zu einem Trick: Es gibt zwei Pseudo-Repositories

    git@github.com/mschilli/1
    git@github.com/mschilli/2

aus und die Shell führt sofort eine Teilergänzung bis zum größten gemeinsamen Nenner durch, wie in der zweiten Zeile von Abbildung 3 sichtbar. Folgen weitere zwei Tabs, ist der User tatsächlich an den Remote-Repos auf dem Server interessiert, und Zeile 40 ruft remote_repos() auf.

WebAPI auf Github

Um die einem Github-User gehörenden Repositories aufzuspüren, setzt das Skript in der Funktion remote_repos() ab Zeile 52 einen Request an die Web-API des Github-Servers ab. Dies geht ganz ohne Anmeldung und mit einer sehr intuitiven Schnittstelle, die wahlweise XML- oder JSON-Daten zurückschickt. Mit dem CPAN-Modul XML::Simple und seiner exportierten Funktion XMLin() nimmt github-helper den zurückkommenden XML-Strom entgegen und wandelt ihn in eine Perl-Datenstruktur um. Unter repositories->repository findet sich darin ein Hash, dessen Keys aus den Namen der Repositories des Users bestehen. Perls keys()-Funktion liefert sie als Liste zurück und die for-Schleife ab Zeile 69 stopft sie in einen Array @repos, den die Funktion am Ende ans aufrufende Programm zurückgibt. Das dritte Kommando in Abbildung 3 zeigt, wie beim Tippen des Clone-Befehls durch einen Doppeldruck auf die Tab-Taste plötzlich alle verfügbaren Repositorypfade zur Auswahl stehen. Gibt der User, wie in der letzen Zeile sichtbar, zwei weitere Buchstaben ein, sodass die Selektion eindeutig wird, komplettiert die Shell auf einen einfachen Tab-Druck hin das Ergebnis und der User braucht nur noch Enter zu drücken.

Das if-Statement ab Zeile 45 in github-helper prüft für jedes gefundene Repo, ob es bis zur Länge des bislang eingegebenen Strings mit der User-Eingabe übereinstimmt. Trifft dies zu, druckt das print()-Kommando in Zeile 47 den vollen Repo-string, gefolgt von einem Newline-Zeichen auf der Standardausgabe des Skripts aus, von wo aus ihn der Komplettiermechanismus aufschnappt. Glücklicherweise enthalten Reponamen keine speziellen Zeichen, die die Shell durcheinander brächten. Andernfalls müssten die Ergebnisse vor der Ausgabe allesamt durch eine Escape-Funktion Shell-sicher gemacht werden. Enthält ein Treffer zum Beispiel ein Leer- oder ein Dollarzeichen, sollte das Helferskript ``\ '' beziehungsweise ``\$'' zurückliefern, damit die Shell das Zeichen nicht interpretiert und den Ergänzungsmechanismus durcheinanderbringt.

Abbildung 3:

Installation

Die Zeilen 7 und 8 des Skripts github-helper sind an die lokalen Bedürfnisse anzupassen und der User ``mschilli'' durch das auf github.com verwendete Kürzel des Users zu ersetzen. Natürlich steht es jedem Leser frei, meine Repositories zu klonen, dazu ist Github ja schließlich da. Das Skript muss, damit die Shell es findet, ausführbar in ein Verzeichnis im $PATH installiert werden. Anschließend wandert der oben gezeigte Aufruf des complete-Kommandos in die .bashrc-Datei, die in der laufenden Shell per ``source .bashrc'' eingelesen und jeder neu aufgerufenen Shell automatisch untergejubelt wird. Die verwendeten CPAN-Module XML::Simple, LWP::UserAgent und Pod::Usage installieren sich wie immer mit einer CPAN-Shell.

Wem der Lookup übers Netz zu langsam geht (er dauert etwa ein, zwei Sekunden kann noch einen Cache einfügen, der Ergebnisse zwischenspeichert. Allerdings ist dabei zu beachten, dass die Shell das Skript github-helper jedesmal neu aufruft, also muss letzteres die Daten persistent auf der Platte halten. Das gezeigte Beispiel illustriert lediglich, was mit Bash-Completion alles möglich ist, die Anwendungsmöglichkeiten sind vielfältig und Unix-Programmierer sind bekannt dafür an allen Ecken und Enden Tastendrücke zu sparen. Mit Bash-Completion schneller zum Ziel!

Infos

[1]

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

[2]

``Bash Completion Homepage'', http://bash-completion.alioth.debian.org/

[3]

CPAN-Modul Getopt::Complete http://search.cpan.org/dist/Getopt-Complete/

[4]

JP Vossen, Cameron Newham, ``Bash Cookbook'', O'Reilly 2007, http://www.amazon.com/dp/0596526784

[5]

Oliver Kiddle, Jerry Peek, Peter Stephenson, ``From Bash to Z Shell: Conquering the Command Line'', Apress 2004, http://www.amazon.com/dp/1590593766

[6]

Github-API: http://github.com/guides/the-github-api

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.