Aus Linux-Magazin 04/2010

Was der User tippen will - die Bash ahnt es dank Perl im Vorhinein

© Thomas Max Müller, Pixelio.de

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

Die [Tab]-Taste hängt oft schon ziemlich ausgeleiert in der Tastatur, so beliebt ist bei vielen das Angebot der Bash-Shell, halb eingegebene Bezeichnungen für Befehle, Verzeichnisse und Dateien auf intelligente Weise zu vervollständigen.

Standardrepertoire

Das Standardrepertoire der Bash-Komplettierung zeigt Abbildung 1. Drückt der Benutzer gleich nach der auf der Kom mandozeile eingegebenen Tastenfolge [L]+[S]+[M] die [Tab]-Taste, vervollständigt die Bash das sofort zu »lsmod«, dem Kommando für die Abfrage installierter Kernelmodule. Warum? Das eingegebene Buchstabentrio stand am Anfang einer Kommandozeile, weshalb es sich um einen ausführbaren Befehl handeln muss. Im eingestellten Pfad (»$PATH«) fand sich kein anderer Befehl mit diesen drei Anfangsbuchstaben, also tat die Bash das einzig Richtige.

Auf der Kommandozeile Nummer 2 in Abbildung 1 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 [Tab] drückt, woraufhin 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 vierten Kommandozeile in Abbildung 1, folgert der Ergänzungsmechanismus, dass der User dem Kommando »ls« eine Datei zugesellen will. Allerdings passen darauf sehr viele, sodass die Bash nach einem einmaligen [Tab] stillschweigt und nach einem Doppeldruck 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ösungen ein, folgt auf einen Doppel-[Tab] wieder eine Auflistung sinnvoller Ergänzungen. Erst wenn die Wahl eindeutig ist, komplettiert die Bash die Zeile.

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

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

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«) unter »Programmable Com­ letion« dokumentierte »complete«-Kommando erlaubt es dem programmierwütigen Shellanwender, 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 Kommandooptionen. Gibt der User zum Beispiel die Zeichenfolge »git com« ein und drückt die [Tab]-Taste, vervollständigt der Mechanismus zu »git commit«, da dies das einzige 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, etwa in [4] und [5].

Doch während die Skriptsammlung auf [2] Bash-Funktionen baut, wissen erfahrene Perl-Programmierer, dass Shellskripte zwar schnell von der Hand gehen, sich mit steigender Komplexität jedoch oft als Sackgasse erweisen. Schuld sind die begrenzten Kapselungsmöglichkeiten der Shell. Manch ein Shellskript hätte sein Programmierer besser gleich in einer vollständigen Skriptsprache implementiert, denn früher oder später wird es eh umgeschrieben, sobald es über einen Prototyp hinauswächst.

Marke Eigenbau

Die Vorschläge, mit denen die Bash auf ein nur halb eingegebenes Kommando antwortet, lassen sich ebenso gut mit einem Perl-Skript generieren. Steht zum Beispiel die Direktive

complete -C helper command

in ».bashrc«, sodass die Shell sie beim Start bemerkt, zieht sie 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 Perl-Skript »helper« auf und übergibt ihm in den Environment-Variablen »COMP_LINE« und »COMP_POINT« die bisher eingegebene Kommandozeile und die Position des Cursors zum Zeitpunkt, an dem der User auf [Tab] 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 jetzt als Ausgabe eine Reihe von Ergänzungsvorschlägen, getrennt durch Zeilenumbrüche.

Die Shellsession in Abbildung 2 definiert als Helferlein das Skript »complete-dump« zum Kommando »ls«. Listing 1 zeigt, dass das Helferskript zu Forschungszwecken lediglich den Inhalt der Environment-Variablen »COMP_LINE« und »COMP_POINT« und das 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: Der Shellbefehl »complete« weist mit der Option »-C« dem Kommando »ls« ein Helferskript »complete-dump« zu, das die Shell dann zur Komplettierung aufruft.

Abbildung 2: Der Shellbefehl »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«

#!/usr/local/bin/perl -w
###########################################
# complete-dump - Debug 'complete' function
# Mike Schilli, 2010 (m@perlmeister.com)
###########################################
use strict;
use Data::Dump qw(dump);

my %matches = ();

for my $env_var (keys %ENV) {
  next if $env_var !~ /^COMP_/;
  $matches{ $env_var } = $ENV{ $env_var };
}

$matches{ ARGV } = @ARGV;

print STDERR "n", 
      dump( %matches );

Die Ausgabe zeigt, dass Bash dem Helferskript in »COMP_LINE« die bislang eingegebene Kommandozeile überreicht, einschließlich aller Leerzeichen. Warum es neben »COMP_LINE« auch noch die Cursorposition in »COMP_POINT« mitliefert, 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 3 versucht das erste Argument zum »ls«-Kommando zu vervollständigen und erhält in »@ARGV« 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« er- gänzen möchte, kommt als drittes Helfer- Argument »/etc« zurück – wieder das Wort vor dem zu vervollständigen Wort.

Eingebautes Helferlein

Selbst geschriebene Skripte können die Helferfunktionen auch gleich selbst mit- bringen. Das Kommando

complete -C myscript myscript

definiert, dass die Bash »myscript« selbst um Rat fragt, falls ein User nach der Eingabe von »myscript « auf die [Tab]-Taste einhämmert. Das Skript fragt dann ab, ob »COMP_LINE« gesetzt ist, und stellt in diesem Fall Vorschläge bereit, während es sonst seine normalen Funktionen ausführt.

Das ist natürlich eine Gratwanderung, 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 Skripte elegant ihre eigenen Optionen komplettieren lässt, schlägt daher 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 zeigt ein kurzes Beispiel, das die Option »–bgcolor« zum Setzen der Hintergrundfarbe anbietet und drei Farbwerte akzeptiert. Ruft der Anwender vorher »complete -C getopt-complete getopt-complete« auf, 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 3 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.

Listing 2:
»getopt-complete«

#!/usr/local/bin/perl -w
###########################################
# getopt-complete - Test Getopt::Complete
# Mike Schilli, 2010 (m@perlmeister.com)
###########################################
use strict;

use Getopt::Complete(
   'bgcolor' => ['red', 'blue', 'green'],
);

Listing 3:
»github-helper«

#!/usr/local/bin/perl -w
###########################################
# github-helper - Complete github repos
# 2010, Mike Schilli <m@perlmeister.com>
###########################################
use strict;
use Pod::Usage;
use LWP::UserAgent;
use XML::Simple;

my $netloc = 'git@github.com';
my $user   = 'mschilli';

if(!defined $ENV{COMP_LINE}) {
  pod2usage("COMP_LINE missing");
}

my($git, $clone, $args) = 
    split /s+/, $ENV{COMP_LINE}, 3;

$args = "" unless defined $args;

if(!defined $clone or
    $clone ne "clone") {
    # Only 'clone' suggestions
  exit(0);
}

if($ARGV[2] ne "clone") {
    # Do nothing if user doesn't want
    # to expand the argument after 'clone'
  exit 0;
}

  # Two pseudo choices to get their
  # common path expanded right away
if(!length $args) {
  for (1..2) {
    print "$netloc/$user/$_n";
  }
  exit 0;
}

my @repos = remote_repos( $user );

for my $repo (remote_repos( $user )) {
  my $remote = "$netloc/$user/$repo";

  if($args eq
     substr($remote, 0, length $args)) {
    print "$remoten";
  }
}

###########################################
sub remote_repos {
###########################################
  my($user) = @_;

  my @repos = ();

  my $ua = LWP::UserAgent->new();
  my $resp = $ua->get(
   "http://github.com/api/v1/xml/$user");

  if($resp->is_error) {
    die "API fetch failed: ", 
        $resp->message();
  }

  my $xml = XMLin($resp->decoded_content());

  for my $repo (keys 
    %{$xml->{repositories}->{repository}}
  ) {
    push @repos, $repo;
  }

  return @repos;
}

__END__

=head1 NAME

    github-helper - Complete github repos

=head1 SYNOPSIS

    COMP_LINE=... github-helper

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« (Listing 3) 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 anbietet. Das Kommando

complete -C github-helper -o default git

in der Datei ».bashrc« behebt das Problem. Wer zudem möchte, dass die Bash eventuell per [3] definierte Ergänzungen vorher berücksichtigt, fügt noch »-o bash­default« hinzu. Ruft der User das Programm mit vollem Pfad, also mit »/usr/bin/git« auf, sucht die Shell zunächst nach einem Complete-Eintrag für den vollen Pfad und fällt, falls keiner zu finden ist, auf den Programmnamen zurück, also auf »git«. Der oben generierte Eintrag funktioniert also in beiden Fällen.

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 19 bricht den bisher eingegebenen Kommandozeilenstring in maximal drei Teile, die durch Leerzeichen voneinander getrennt sind.

Falls das Git-Subkommando nicht »clone« ist (sondern zum Beispiel »git add«), beendet sich das Skript ohne jegliche Ausgabe in Zeile 26, um dem Complete-Mechanismus mitzuteilen, dass es nichts beizutragen hat und statt seiner etwaige anderweitig definierte Complete-Funktionen zum Zuge kommen sollen. Zeile 29 prüft, ob der User sich tatsächlich beim Tippen des Arguments an einer Position nach dem Wort »clone « (mit Leerzeichen) befindet oder ob 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« schreibt, greift das Skript in Zeile 38 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 zu sehen. Folgen weitere zwei Tabs, ist der User tatsächlich an den Remote-Repos auf dem Server interessiert und Zeile 44 ruft »remote_repos()« auf.

Abbildung 3: Hier ergänzt das Helferskript die Namen von Git-Repositories.

Abbildung 3: Hier ergänzt das Helferskript die Namen von Git-Repositories.

Nachfrage beim Web-API von Github

Um die einem bestimmten Github-User gehörenden Repositories aufzuspüren, setzt das Skript in der Funktion »remote_repos()« ab Zeile 56 einen Request an das 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 73 stopft sie in ein Array »@repos«, das 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 letzten Zeile sichtbar, zwei weitere Buchstaben ein, die die Selektion eindeutig machen, komplettiert die Shell auf einen einfachen [Tab]-Druck hin das Ergebnis, der User braucht nur noch [Enter] zu drücken.

Das If-Statement ab Zeile 49 in Listing 3 prüft für jedes gefundene Repository, ob es bis zur Länge des bislang eingegebenen Strings mit der User-Eingabe übereinstimmt. Trifft dies zu, so druckt das »print()«-Kommando in Zeile 51 den vollen Repository-String gefolgt von einem Newline-Zeichen auf der Standardausgabe des Skripts aus, wo ihn an- schließend der Komplettiermechanismus aufschnappt.

Glücklicherweise enthalten Repositorynamen keine speziellen Zeichen, die die Shell irritieren könnten. Andernfalls müsste eine Escapefunktion alle Ergebnisse vor der Ausgabe Shell-sicher machen. Enthält ein Treffer zum Beispiel ein Leer- oder ein Dollarzeichen, sollte das Helferskript » « oder »$« zurückliefern, damit die Shell das Zeichen nicht interpretiert und den Ergänzungsmechanismus durcheinanderbringt.

Installation

Die Zeilen 11 und 12 in Listing 3 sind von jedem Anwender an seine lokalen Bedürfnisse anzupassen und der User »mschilli« ist durch das auf Github.com verwendete Kürzel jenes Users zu ersetzen, der das Skript benutzt. Natürlich steht es jedem Leser frei, meine Repositories zu klonen, dazu ist Git-Repositories. Github ja schließlich da. Das Skript muss, damit die Shell es findet, ausführbar in einem Verzeichnis im »$PATH« installiert sein. Anschließend wandert der oben gezeigte Aufruf des »complete«-Kommandos in die ».bashrc«-Datei, die die laufende Shell per »source .bashrc« einliest und die jede neu aufgerufene Shell benutzt. Die verwendeten CPAN-Module XML::Simple, LWP::UserAgent und Pod::Usage installieren sich wie immer einfach mit einer CPAN-Shell.

Anwender, denen ein Lookup über das Netzwerk zu lange dauert (er braucht ungefähr zwischen 1 und 2 Sekunden), können außerdem noch einen Cache einfügen, der die Ergebnisse zwischenspeichert. Allerdings ist dabei zu beachten, dass die Shell das Skript »github-helper« jedes Mal neu aufruft – also muss Letzteres seine Daten persistent auf der Fest- patte ablegen.

Das beschriebene Beispiel soll lediglich illustrieren, was mit einer Bash-Completion alles erreichbar ist, die Anwendungsmöglichkeiten sind vielfältig und Unix-Programmierer sind ja bekannt dafür, an allen Ecken und Enden Tastendrücke zu sparen. Also mit der Bash-Completion schneller zum Ziel! (jcb)

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.

[5] Oliver Kiddle, Jerry Peek, Peter Stephenson,”From Bash to Z Shell: Conquering the Command Line”: Apress 2004.

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

Der Autor


Der Autor Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (engl­isch) 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