Dateinamenvervollstaendigung in der Kommandozeile (Unix)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Hallo allerseits!

Ich schreibe gerade ein Python-Skript, das an verschiedenen Stellen Eingaben vom User verlangt, unter anderem Dateinamen.

Ich benutze raw_input und habe das readline-Modul eingebunden. Das ist schonmal nicht schlecht, man kann seine Eingabe bequem editieren. Nun haette ich gerne, dass Dateinamen beim Druecken der TAB-Taste vervollstaendigt werden, wie in einer Unix-Shell.

Ich habe

readline.parse_and_bind("tab: complete")

gesetzt, was aber standargmaessig versucht, Python-Kommandos zu vervollstaendigen.

Jetzt muss ich mir eine eigene completer-Funktion schreiben, oder? Wie mache ich das? Oder gibt's noch eine ganz andere Moeglichkeit?

Gruss,
Rebecca
Mad-Marty
User
Beiträge: 317
Registriert: Mittwoch 18. Januar 2006, 19:46

Ich habe da von noch nichts fertigen gehört, wirst du also wohl selbst schreiben müssen.
Verwenden kannst du dazu os.listdir() und prüfst ob der teilweise dateiname mit den ergebnissen matcht ...

Aber vielleicht hat ja jemand eine bessere idee ...
BlackJack

In den Quelltext von IPython schauen hilft vielleicht. Dort gibt's bei TAB Vervollständigung für Python-Schlüsselworte, Objekte und deren Attribute und Dateinamen im Arbeitsverszeichnis.
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

@BlackJack: Das werde ich tun, danke.
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Ich bin jetzt ein wenig schlauer...

Eine completer-Funktion sieht wie folgt aus:

completer(text, state)

Sie soll die Vervollstaendigung Nummer 'state' von String 'text' zurueckliefern oder 'None', wenn es keine (weitere) Vervolltsaendigung (mehr) gibt.

Eine Vervollstaendigung innerhalb des aktuellen Verzeichnisses ./ hinzubekommen ist nicht so schwierig. Dummerweise scheint 'text' nicht exakt das zu sein, was der Benutzer eingibt, sondern die Eingabe wurde schon bereinigt. Das fuehrt zu einem Problem mit slashes:
Die Eingabe "/home/rbreu" fuehrt zu text = "rbreu",
"/home/rbreu/" fuehrt zu text = ""
und so weiter. :-(

Kann man das irgendwie beeinflussen? Das Modul readline hat ja noch einiges an Methoden zu bieten, nur ist die Python-Dokumentation ziemlich knapp. *nichvielversteht*

Hier meine bisherige completer-Funktion:

Code: Alles auswählen

def complete(text, state):
    """Return the next possible completion for 'text'.

    This is called successively with state == 0, 1, 2, ... until it
    returns None.  The completion should begin with 'text'.
    """

    completions = [];  #A list of all possible completions

    #The directory we are looking in:
    the_dir = os.path.join(".", os.path.dirname(text));

    #print ">" + text + "===>" + the_dir;  #for test purposes

    #A list of all files in the_dir:
    files = dircache.listdir(the_dir);

    #Find the entries of 'files' which mach 'text':
    for entry in files:
        os.path.join(the_dir, entry);
        if entry.startswith(text):
            if os.path.isdir(entry):
                entry = os.path.join(entry, "");

            completions.append(entry);


    return completions[state];
Benutzeravatar
Masaru
User
Beiträge: 425
Registriert: Mittwoch 4. August 2004, 22:17

Hmm .. also ich bin gerade unter Windows (mit WConio) dabei ein Consolen Overlay zu implementieren ... deswegen kann ich nicht genau sagen ob und wie es mit 'readline' direkt zu realisieren ist.

Wenn du aber die Eingabe hast, und den Cursor bewegst, und in der Lage bist den CursorIndex auf dem Inputstring herrauszufinden, dann kannst du da genau ansetzen:

Da ich gleich Feierabend habe, hacke ich schnell mal meine Ansätze hier hinnein. Sorry für die grausame Struktur und Rechtschreibfehler ;).

1. String bis zur aktuellen CursorPosition nehmen
(Bsp: /data/test/ <- Cursor auf 'e' von Test = Index von 7
neuer String = derString[:7]
neuer String ist somit: /data/te

2. wenns man genau wie auf den meisten Plattformen haben möchte, müsste man noch vom Ende aus nur den Teil nehmen, der kein Leerzeichen enthält
(Bsp: "bla /data/test" ... zu "/data/te")

3. den String nun mit os.path.basename und .dirname aufsplitten
(Bsp: dir_ = /data
base = te)

4. schauen ob "dir_" ein vollständiger Root-Pfad ist, oder ob er von der aktuellen Position aus laufen soll (falls er von der aktuellen Position aus laufen soll, mit os.path.join(os.getcwd(), dir_) vervollständigen)

5. tjoa .. nun kann man halt aus dem "dir_" Pfad mit os.listdir(dir_) die Elemente holen, und mittels Regulären Ausdruck (ansonsten tut's ein "if elemt.find(base) == 0" auch) am besten prüfen, ob "base" der Anfang des Elementes halt ist
etwas gemein ist es, wenn "base" natürlich ein Leerstring ist ... das ist ein Soderfall, der eine Iteration über "alle" Elemente erlauben sollte

6. entsprechend dann den Eingabestring (den nun gefunden Path) in der Commandline erweitern

Noch nicht gelöst ist hiermit eine "Iteration" über die einzelnen Treffer.
Z.B:
/data/test
/data/test_2
/data/test_3
/data/technik
... ein solche tabbare Verzeichnisiteration erfordert ein paar Merkschritte, sowie die Sortierung der Verzeichnisinhaltsliste.

Ich versuche die Tage die WConio Lösung hier zu posten.

Gruß
>>Masaru<<
BlackJack

Rebecca hat geschrieben:Dummerweise scheint 'text' nicht exakt das zu sein, was der Benutzer eingibt, sondern die Eingabe wurde schon bereinigt. Das fuehrt zu einem Problem mit slashes:
Die Eingabe "/home/rbreu" fuehrt zu text = "rbreu",
"/home/rbreu/" fuehrt zu text = ""
und so weiter. :-(

Kann man das irgendwie beeinflussen? Das Modul readline hat ja noch einiges an Methoden zu bieten, nur ist die Python-Dokumentation ziemlich knapp. *nichvielversteht*
Man kann angeben welche Zeichen als "Wortgrenzen" für die Vervollständigung genommen werden. Da scheint der '/' dabei zu sein. Wenn man es zum Beispiel nur auf Leerzeichen beschränkt, dann klappt's:

Code: Alles auswählen

import os
import readline


def completer(text, state, _names=list()):
    path, incomplete_part = os.path.split(text)
    if state == 0:
        _names[:] = filter(lambda name: name.startswith(incomplete_part),
                           os.listdir(os.path.join(os.path.curdir, path)))
    return os.path.join(path, _names[state])


def main():
    readline.set_completer_delims(' ')
    readline.set_completer(completer)
    readline.parse_and_bind('tab: complete')
    while True:
        line = raw_input('> ')
        print ':', line
Es scheint das ich mir meinen eigenen `dircache` gebaut habe, da ich das Modul nicht im Kopf hatte. Wobei ich die Dateien wirklich nur einmal lese und `dircache` jedesmal zumindest testet ob sich die Einträge geändert haben. Ich vermute meine Lösung ist schneller und macht bei Verzeichnissen mit vielen Einträgen einen merkbaren Unterschied. Aber das sollte man besser ausprobieren. Der Cache ist dazugekommen, nachdem ich mal versucht habe `/usr/lib/` + TAB einzugeben. :-)
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

BlackJack hat geschrieben: Man kann angeben welche Zeichen als "Wortgrenzen" für die Vervollständigung genommen werden. Da scheint der '/' dabei zu sein. Wenn man es zum Beispiel nur auf Leerzeichen beschränkt, dann klappt's:

Code: Alles auswählen

readline.set_completer_delims(' ')
Aha! Genau das war's, was mir gefehlt hat, super! Ich hatte schon vermutet, dass es damit zu tun hat, aber irgendwas hatte ich wohl mit set_completer_delims noch falsch gemacht.

Mit den richtigen Delimitern konnte ich dann auch feststellen, dass meine oben gepostete Funktion noch nicht so ganz richtig ist. Bei mir sieht's jetzt so aus:

Code: Alles auswählen

def complete(text, state):
    """Return the next possible completion for 'text'.

    This is called successively with state == 0, 1, 2, ... until it
    returns None.  The completion should begin with 'text'.
    """

    completions = [];  #A list of all possible completions

    (the_dir, incomplete) = os.path.split(text);

    #A list of all files in the_dir:
    files = dircache.listdir(os.path.join(".", the_dir));

    #Find the entries of 'files' which mach 'text':
    for entry in files:
        if entry.startswith(incomplete):
            if os.path.isdir(os.path.join(the_dir, entry)):
                entry = os.path.join(entry, "");
            completions.append(os.path.join(the_dir, entry));
        
    return completions[state];
Etwas laenger als BlackJack's, dafuer aber meins *g*
BlackJack hat geschrieben: Es scheint das ich mir meinen eigenen `dircache` gebaut habe, da ich das Modul nicht im Kopf hatte. Wobei ich die Dateien wirklich nur einmal lese und `dircache` jedesmal zumindest testet ob sich die Einträge geändert haben. Ich vermute meine Lösung ist schneller und macht bei Verzeichnissen mit vielen Einträgen einen merkbaren Unterschied.
Ich denke, damit kann ich leben. :-D Danke nochmals.

Rebecca,
die sich jetzt endlich beim Testen ihres Skripts nicht mehr die Finger wundtippen muss 8)
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Argh, wie bescheuert ist das denn? Das readline-Module ignoriert alle Exceptions einer completer-Funktion! :evil:

Ich habe gerade einen halbe Ewigkeit damit verbracht, harauszufinden, warum die Vervollstaendigung in meinem neuen Programm nicht mehr funktioniert, wo sie doch in einem anderen Programm so gut funktioniert hat. Hab mich dann gewundert, warum die obige complete-Funktion anscheinend nicht ueber Zeile 10 nicht hinauskam -- hatte vergessen, os zu importieren. :evil:
CrackPod
User
Beiträge: 205
Registriert: Freitag 30. Juni 2006, 12:56

Hallo,

tut mir leid, dass ich das Thema wieder ausgrabe, aber ich habe durch die Suche eure Skripte gefunden, weil ich auch eine Textvervollständigung brauche. Allerdings würde ich sie auch gerne Verstehen ;) Ich nehme jetzt einfach mal BlackJacks Code:

Code: Alles auswählen

import os
import readline


def completer(text, state, _names=list()):
    path, incomplete_part = os.path.split(text)
    print path #1
    print incomplete_part #1
    print state #2
    if state == 0:
        _names[:] = filter(lambda name: name.startswith(incomplete_part),
                           os.listdir(os.path.join(os.path.curdir, path))) #3
        print _names #4
    return os.path.join(path, _names[state]) #5


def main():
    readline.set_completer_delims(' ') #6
    readline.set_completer(completer)
    readline.parse_and_bind('tab: complete') #7
    while True:
        line = raw_input('> ')
        print ':', line
  1. Wieso wird hier kein Pfad/Name ausgegeben?
  2. Für was steht state?(eventuell die Anzahl der enthaltenend Dateien im Ordner?)
  3. Wieso schreibst du _name[:]? Das liefert doch eine flache Kopie der Liste, oder? Für was ist das notwending?
  4. Wie wird denn state weitergeben?
  5. Wie kann man denn keinen Delimiter angeben bzw einen, der keinen "Einfluss" hat, denn ein Leerzeichen ist - wie ich finde - unpraktisch, da es ja auch Order/Dateinamen mit Leerzeichen gibt.
  6. Wie kommt man auf tab: complete? Gibts auch andere Einstellungsmöglichkeiten?
Ich finde, in der Doku ist readline ein bisschen mager beschrieben.

LG
Tobsl
Hi! I'm a .signature virus! copy me into your .signature file to help me spread!
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

re 1: wo ist "hier"?

re 2: erwähnte Rebecca doch vor zwei Jahren: Der n-te Vorschlag.

re 3: Die Kopie liefert es nur, wenn es auf der rechten Seite vom "=" steht. Links führt es dazu, das bei der Liste alle Elemente durch die Elemente rechts vom "=" ersetzt werden. BlackJacks Code nutzt einen fiesen Trick, um eine private globale Variable zu bekommen, indem er eine Liste manipuliert, die der Standardwert eines Parameters ist, wissend, dass die Standardwerte von allen aufgerufenen Funktionen gemeinsam benutzt werden.

re 4: state wird von libreadline übergeben.

re 5: keine Ahnung, vielleicht mit ""? Die Python-Funktion ruft rl_completer_word_break_characters() auf, dafür wird es doch wohl eine Dokumentation geben...

re 6: Die Funktion ruft rl_parse_and_bind() auf. Auch dafür gibt es Dokumentation... Es ist das init-file-Format. Man könnte z.B. readline.parse_and_bind('A: "Hallo"') benutzen und bei jedem großen A wird dann Hallo geschrieben. Es funktioniert auch mit mehreren Zeichen, etwa readline.parse_and_bind('"mfg": "Mit freundlichen Grüßen"') (aber nicht mit dem Python 2.5.1, das OS X 10.5 beiliegt)

Stefan
Benutzeravatar
Rebecca
User
Beiträge: 1662
Registriert: Freitag 3. Februar 2006, 12:28
Wohnort: DN, Heimat: HB
Kontaktdaten:

Hey, das war mein erster Post hier, da werd ich ja ganz nostalgisch! :wink:
sma hat geschrieben:re 1: wo ist "hier"?
Wenn die print-Ausgaben gemeint sind: Die werden vlt. einfach genau so ignoriert wie meine Exceptions in meinem letzten Post... Ist vlt. ein Schutz davor, die Kommandozeile nicht voellig unbrauchbar zu machen...
BlackJacks Code nutzt einen fiesen Trick, um eine private globale Variable zu bekommen
Stimmt, damit kann man quasi statische Variablen aus C nachbauen. Schade, dass es so ein Hack ist...
Offizielles Python-Tutorial (Deutsche Version)

Urheberrecht, Datenschutz, Informationsfreiheit: Piratenpartei
Antworten