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.
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.
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].
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. |
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.
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]).
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.
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.
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.
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: |
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!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/04/Perl
``Bash Completion Homepage'', http://bash-completion.alioth.debian.org/
CPAN-Modul Getopt::Complete http://search.cpan.org/dist/Getopt-Complete/
JP Vossen, Cameron Newham, ``Bash Cookbook'', O'Reilly 2007, http://www.amazon.com/dp/0596526784
Oliver Kiddle, Jerry Peek, Peter Stephenson, ``From Bash to Z Shell: Conquering the Command Line'', Apress 2004, http://www.amazon.com/dp/1590593766
Github-API: http://github.com/guides/the-github-api
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. |