readline - Interfaccia alla libreria GNU readline

Scopo Fornisce una interfaccia alla libreria GNU readline per l'interazione con l'utente al prompt di comando.
Versione Python 1.4 e superiore

Il modulo readline può essere usato per migliorare i programmi interattivi di riga di comando per facilitarne l'utilizzo. E' principalmente usato per fornire un completamento del testo della riga di comando, altrimenti noto come "tab completion" (in quanto premendo il tasto di tabulazione si ha la possibilità di completare una porzione di testo digitata tramite un elenco di opzioni - n.d.t.).

Visto che readline interagisce con il contenuto della console, la stampa di messaggi di debug rende difficoltoso vedere cosa sta succedendo nel codice di esempio rispetto a quello che readline fa di sua iniziativa. Gli esempi sottostanti usano il modulo logging per scrivere le informazioni di debug in un file separato. Le informazioni contenute in detto file vengono mostrate con ciascun esempio.

Ci sono due modi per configurare la libreria readline sottostante, usando un file di configurazione oppure la funzione parse_and_bind(). Le opzioni di configurazione comprendono il keybinding per la chiamata del completamento, le modalità di modifica (vi oppure emacs), e molti altri valori. Si faccia riferimento alla documentazione della libreria readline GNU per i dettagli.

Il modo più facile per abilitare il completamento tramite il tasto di tabulazione è tramite la chiamata a parse_and_bind(). Altre opzioni possono essere impostate allo stesso tempo. Questo esempio modifica i controlli per la modifica per usare la modalità "vi" invece che la predefinita "emacs". Per modificare la riga in input corrente, si preme ESC, quindi si usano i normali tasti di spostamento impostati in "vi" come j, k, l ed h.

import readline

readline.parse_and_bind('tab: complete')
readline.parse_and_bind('set editing-mode vi')

while True:
    line = raw_input('Prompt ("stop" per uscire): ')
    if line == 'stop':
        break
    print 'DIGITATO: "%s"' % line

La stessa configurazione può essere conservata sotto forma di istruzioni in un file letto dalla libreria con una singola chiamata. Se myreadline.rc contiene:

# Attiva il completamento con il tasto tab
tab: complete

# Usa la modalità di modifica vi invece che emacs
set editing-mode vi

il file può poi essere letto con read_init_file():

import readline

readline.read_init_file('myreadline.rc')

while True:
    line = raw_input('Prompt ("stop" per uscire): ')
    if line == 'stop':
        break
    print 'DIGITATO: "%s"' % line

Completamento del Testo

Come esempio di come si costruisca il completamento di una riga di comando, si può vedere un programma che ha al suo interno un insieme di possibili comandi ed usa il completamento con il tasto tab quando l'utente digita le istruzioni.

import readline
import logging

LOG_FILENAME = '/tmp/completer.log'
logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

class SimpleCompleter(object):

    def __init__(self, options):
        self.options = sorted(options)
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            # Questa è la prima volta per questo testo, quindi si costruisce un elenco di corrispondenze
            if text:
                self.matches = [s
                                for s in self.options
                                if s and s.startswith(text)]
                logging.debug('%s corrisponde a: %s', repr(text), self.matches)
            else:
                self.matches = self.options[:]
                logging.debug('(input vuoto) corrisponde a: %s', self.matches)

        # Restituisce l'elemento che corrisponde a state dalla lista di completamento
        # se ce ne sono a sufficienza
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('completato(%s, %s) => %s',
                      repr(text), state, repr(response))
        return response

def input_loop():
    line = ''
    while line != 'stop':
        line = raw_input('Prompt ("stop" per uscire): ')
        print 'Inviato %s' % line

# Registra la funzione di completamento
readline.set_completer(SimpleCompleter(['start', 'stop', 'list', 'print']).complete)

# Usa il tasto tab per il completamento
readline.parse_and_bind('tab: complete')

# Prompt all'utente per il testo
input_loop()

La funzione input_loop() legge semplicemente una riga dopo l'altra fino a che il valore imputato è "stop". Un programma più sofisticato potrebbe elaborare realmente la riga di input ed eseguire il comando.

La classe SimpleCompleter mantiene un elenco di "opzioni" che sono suscettibili di auto-completamento. Il metodo complete() per una istanza è concepito per essere registrato con readline come sorgente per i completamenti. I parametri sono una stringa "text" da completare ed un valore "state", che indica quante volte la funzione è stata chiamata con lo stesso testo. Questa funzione viene chiamata ripetutamente con state incrementato ogni volta. Dovrebbe restituire una stringa se c'è un candidato per quel valore di state, oppure None se non ci sono ulteriori candidati. Qui l'implementazione di complete() cerca un insieme di corrispondenze quando state è 0, quindi ritorna tutte le corrispondenze, una alla volta, su chiamate successive.

Quando eseguito lo script, l'output iniziale assomiglia a questo:

$ python readline_completer.py
Prompt ("stop" per uscire):

Se si preme due volte il tasto TAB, viene stampato un elenco di opzioni.

$ python readline_completer.py
Prompt ("stop" per uscire):
list   print  start  stop
Prompt ("stop" per uscire):

Il file di registro mostra che complete() è stato chiamato con due sequenze separate di valori di state.

$ tail -f /tmp/completer.log
DEBUG:root:(input vuoto) corrisponde a: ['list', 'print', 'start', 'stop']
DEBUG:root:completato('', 1) => 'print'
DEBUG:root:completato('', 2) => 'start'
DEBUG:root:completato('', 3) => 'stop'
DEBUG:root:completato('', 4) => None
DEBUG:root:(input vuoto) corrisponde a: ['list', 'print', 'start', 'stop']
DEBUG:root:completato('', 0) => 'list'
DEBUG:root:completato('', 1) => 'print'
DEBUG:root:completato('', 2) => 'start'
DEBUG:root:completato('', 3) => 'stop'
DEBUG:root:completato('', 4) => None

La prima sequenza proviene dalla prima pressione del tasto TAB. L'algoritmo di completamento richiede tutti i candidati ma non espande la riga di input vuota. Poi, al secondo TAB, l'elenco di candidati viene ricalcolato in modo da potere essere stampato all'utente.

Se successivamente si digita "l" quindi ancora TAB, la videata mostra:

Prompt ("stop" per uscire): list

Ed il registro rispecchia i parametri diversi per complete():

DEBUG:root:'l' corrisponde a: ['list']
DEBUG:root:completato('l', 0) => 'list'
DEBUG:root:completato('l', 1) => None

La pressione di INVIO ora fa sì che raw_input() restituisca il valore, ed il ciclo in while riprende.

Inviato list
Prompt ("stop" per uscire):

Ci sono due possibili completamenti per il comando che inizia per "s". Digitando "s", quindi premendo TAB si trovano "start" e "stop " come candidati, ma il testo viene completato solo parzialmente sullo schermo aggiungendo una "t".

Il file di log mostra:

DEBUG:root:'s' corrisponde a: ['start', 'stop']
DEBUG:root:completato('s', 0) => 'start'
DEBUG:root:completato('s', 1) => 'stop'
DEBUG:root:completato('s', 2) => None

La prima sequenza proviene dalla prima pressione di TAB. L'algoritmo di completamento richiede tutti i coandidati ma non espande la riga vuota. Quindi, alla seconda pressione di TAB, l'elenco dei candidati viene ricalcolato in modo da potere essere stampato per l'utente.

Se successivamente si digita "l", quindi si preme TAB nuovamente, lo schermo mostra:

Prompt ("stop" per uscire): list

Ed il registro riflette i diversi parametri per complete():

DEBUG:root:'l' corrisponde a: ['list']
DEBUG:root:completato('l', 0) => 'list'
DEBUG:root:completato('l', 1) => None

La pressione di INVIO fa sì che raw_input ritorni il valore, quindi riprendo il ciclo di while.

Inviato list
Prompt ("stop" per uscire):

Ci sono due possibili completamenti per un comando che inizia per "s". Digitando "s" quindi premendo TAB si trovano "start" e "stop" come candidati, ma il completamento del testo è solo parziale aggiungendo una "t".

Il file di registro mostra:

DEBUG:root:'s' corrisponde a: ['start', 'stop']
DEBUG:root:completato('s', 0) => 'start'
DEBUG:root:completato('s', 1) => 'stop'
DEBUG:root:completato('s', 2) => None

e lo schermo:

Prompt ("stop" per uscire): st
Se il proprio computer solleva una eccezione, essa viene silenziosamente ignorata e readline assume che non ci siano completamenti corrispondenti.

Accedere al Buffer di Completamento

L'algoritmo di completamento di cui sopra è semplicistico in quanto viene cercato solo il parametro text passato alla funzione, ma non usa null'altro dello stato interno di readline. E' anche possibilie usare le funzioni di readline per manipolare il testo nel buffer di input.

import readline
import logging

LOG_FILENAME = '/tmp/completer.log'
logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

class BufferAwareCompleter(object):

    def __init__(self, options):
        self.options = options
        self.current_candidates = []
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            # Questa è la prima volta per questo testo, quindi si costruisce un elenco di corrispondenze

            origline = readline.get_line_buffer()
            begin = readline.get_begidx()
            end = readline.get_endidx()
            being_completed = origline[begin:end]
            words = origline.split()

            logging.debug('riga originale=%s', repr(origline))
            logging.debug('inizio=%s', begin)
            logging.debug('fine=%s', end)
            logging.debug('in completamento=%s', being_completed)
            logging.debug('parole=%s', words)

            if not words:
                self.current_candidates = sorted(self.options.keys())
            else:
                try:
                    if begin == 0:
                        # prima parola
                        candidates = self.options.keys()
                    else:
                        # parola ulteriore
                        first = words[0]
                        candidates = self.options[first]

                    if being_completed:
                        # cerca corrispondenza di opzioni con la
                        # porzione di input che si sta completando
                        self.current_candidates = [ w for w in candidates
                                                    if w.startswith(being_completed) ]
                    else:
                        # corrispondenza con una stringa vuota, quindi si usano tutti i candidati
                        self.current_candidates = candidates

                    logging.debug('candidati=%s', self.current_candidates)

                except (KeyError, IndexError), err:
                    logging.error('errore di completamento: %s', err)
                    self.current_candidates = []

        try:
            response = self.current_candidates[state]
        except IndexError:
            response = None
        logging.debug('completato(%s, %s) => %s', repr(text), state, response)
        return response


def input_loop():
    line = ''
    while line != 'stop':
        line = raw_input('Prompt ("stop" per uscire): ')
        print 'Inviato %s' % line

# Registrazione della propria funzione di completamento
readline.set_completer(BufferAwareCompleter(
    {'list':['files', 'directories'],
     'print':['byname', 'bysize'],
     'stop':[],
    }).complete)

# Uso del tasto tab per il completamento
readline.parse_and_bind('tab: complete')

# Prompt all'utente per il testo
input_loop()

In questo esempio, i comandi con sotto opzioni sono completati. Il metodo complete() deve cercare alla posizione del completamento all'interno del buffer di input per determinare se parte della prima parola o di una parola ulteriore. Se l'obiettivo è la prima parola, le chiavi dei dizionario di opzioni vengono usate come candidati. Se non si tratta della prima parola, allore viene usata la prima parola per cercare candidati nel dizionario delle opzioni.

Ci sono tre comandi di primo livello, due dei quali hanno sotto comandi:

  • elenca
  • file
  • directory
  • stampa
  • pernome
  • perdimensione
  • stop

Seguendo la stessa sequenza di azioni di prima, premendo TAB per due volte si ottengono i tre comandi del livello superiore:

$ python readline_buffer.py
Prompt ("stop" per uscire):
elenca  stampa  stop
Prompt ("stop" per uscire):

Ed il registro riporta:

DEBUG:root:riga originale=''
DEBUG:root:inizio=0
DEBUG:root:fine=0
DEBUG:root:in completamento=
DEBUG:root:parole=[]
DEBUG:root:completato('', 0) => elenca
DEBUG:root:completato('', 1) => stampa
DEBUG:root:completato('', 2) => stop
DEBUG:root:completato('', 3) => None
DEBUG:root:riga originale=''
DEBUG:root:inizio=0
DEBUG:root:fine=0
DEBUG:root:in completamento=
DEBUG:root:parole=[]
DEBUG:root:completato('', 0) => elenca
DEBUG:root:completato('', 1) => stampa
DEBUG:root:completato('', 2) => stop
DEBUG:root:completato('', 3) => None

Se la prima parola è "elenca " (con uno spazio dopo la parola), i candidati per il completamento sono diversi:

Prompt ("stop" per uscire): elenca
directory  file

Il registro mostra che il testo che è stato completato non è l'intera riga, ma la porzione successiva

DEBUG:root:riga originale='elenca '
DEBUG:root:inizio=7
DEBUG:root:fine=7
DEBUG:root:in completamento=
DEBUG:root:parole=['elenca']
DEBUG:root:candidati=['file', 'directory']
DEBUG:root:completato('', 0) => file
DEBUG:root:completato('', 1) => directory
DEBUG:root:completato('', 2) => None
DEBUG:root:riga originale='elenca '
DEBUG:root:inizio=7
DEBUG:root:fine=7
DEBUG:root:in completamento=
DEBUG:root:parole=['elenca']
DEBUG:root:candidati=['file', 'directory']
DEBUG:root:completato('', 0) => file
DEBUG:root:completato('', 1) => directory
DEBUG:root:completato('', 2) => None

Input Storico

readline tiene traccia dello storico di input automaticamente. Si sono due diversi insiemi di funzioni che lavorano con lo storico. Lo storico per la sessione corrente può esseere indirizzato attraverso get_current_history_length() e get_history_item(). Lo stesso storico può essere salvato in un file per un recupero successivo usando write_history_file() e read_history_file. Nella modalità predefinita l'intero storico viene salvato ma la dimensione massima del file può essere impostata con set_history_length(). Una dimensione di -1 significa che non c'è limite.

import readline
import logging
import os

LOG_FILENAME = '/tmp/completer.log'
HISTORY_FILENAME = '/tmp/completer.hist'

logging.basicConfig(filename=LOG_FILENAME,
                    level=logging.DEBUG,
                    )

def get_history_items():
    return [ readline.get_history_item(i)
             for i in xrange(1, readline.get_current_history_length() + 1)
             ]

class HistoryCompleter(object):

    def __init__(self):
        self.matches = []
        return

    def complete(self, text, state):
        response = None
        if state == 0:
            history_values = get_history_items()
            logging.debug('storico: %s', history_values)
            if text:
                self.matches = sorted(h
                                      for h in history_values
                                      if h and h.startswith(text))
            else:
                self.matches = []
            logging.debug('corrispondenze: %s', self.matches)
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('completato(%s, %s) => %s',
                      repr(text), state, repr(response))
        return response

def input_loop():
    if os.path.exists(HISTORY_FILENAME):
        readline.read_history_file(HISTORY_FILENAME)
    print 'Lunghezza max file storico:', readline.get_history_length()
    print 'Storico di partenza:', get_history_items()
    try:
        while True:
            line = raw_input('Prompt ("stop" per uscire): ')
            if line == 'stop':
                break
            if line:
                print 'Aggiunta di "%s" nello storico' % line
    finally:
        print 'Storico finale:', get_history_items()
        readline.write_history_file(HISTORY_FILENAME)

# Registra la funzione di completamento
readline.set_completer(HistoryCompleter().complete)

# Uso del tasto tab per il completamento
readline.parse_and_bind('tab: complete')

# Prompt all'utente per il testo
input_loop()

La classe HistoryCompleter ricorda qualsiasi cosa venga digitata ed usa quei valori per il completamento dell'input successivo.

$ python readline_history.py
Lunghezza max file storico: -1
Storico di partenza: []
Prompt ("stop" per uscire): foo
Aggiunta di "foo" nello storico
Prompt ("stop" per uscire): bar
Aggiunta di "bar" nello storico
Prompt ("stop" per uscire): blah
Aggiunta di "blah" nello storico
Prompt ("stop" per uscire): b
bar   blah
Prompt ("stop" per uscire): stop
Storico finale: ['foo', 'bar', 'blah', 'stop']

Il registro mostra questo output quando "b" viene seguito da due TAB.

DEBUG:root:storico: ['foo', 'bar', 'blah']
DEBUG:root:corrispondenze: ['bar', 'blah']
DEBUG:root:completato('b', 0) => 'bar'
DEBUG:root:completato('b', 1) => 'blah'
DEBUG:root:completato('b', 2) => None
DEBUG:root:storico: ['foo', 'bar', 'blah']
DEBUG:root:corrispondenze: ['bar', 'blah']
DEBUG:root:completato('b', 0) => 'bar'
DEBUG:root:completato('b', 1) => 'blah'
DEBUG:root:completato('b', 2) => None

Quando lo script viene eseguito per la seconda volta, tutto lo storico viene letto dal file.

$ python readline_history.py
Lunghezza max file storico: -1
Storico di partenza: ['foo', 'bar', 'blah', 'stop']
Prompt ("stop" per uscire):

Ci sono funzioni sia per rimuovere elementi singoli nello storico che per eliminare interamente lo storico.

Agganci

Ci sono parecchi agganci disponibili per far scattare dele azioni come parte della sequenza di interazione. L'aggancio startup viene chiamato immediatamente prima della stampa del prompt, e l'aggancio pre-input viene eseguito dopo il prompt, ma prima di leggere il testo dall'utente.

import readline

def startup_hook():
    readline.insert_text('da startup_hook')

def pre_input_hook():
    readline.insert_text(' da pre_input_hook')
    readline.redisplay()

readline.set_startup_hook(startup_hook)
readline.set_pre_input_hook(pre_input_hook)
readline.parse_and_bind('tab: complete')

while True:
    line = raw_input('Prompt ("stop" per uscire): ')
    if line == 'stop':
        break
    print 'DIGITATO: "%s"' % line

Entrambi gli agganci sono potenzialmente un buon punto per usare insert_test() per modificare il buffer di input.

python readline_hooks.py
Prompt ("stop" per uscire): da startup_hook da pre_input_hook

Se il buffer viene modificato all'interno dell'aggancio pre-input, occorre chiamare redisplay() per aggiornare lo schermo.

Vedere anche:

readline
La documentazione della libreria standard per questo modulo.
GNU readline
La documentazione per la libreria GNU readline.
readline init file format
Il formato del file di inizializzazione e configurazione.
effbot: The readline module
La guida di Effbot al modulo readline.
pyreadline
pyreadline, sviluppato come rimpiazzo basato su Python per readline da usare con iPython.
cmd
Il modulo cmd usa readline in modo estensivo per implementare il completamento con tab nell'interfaccia del comando. Alcuni esempi qui sopra sono stati adattati sulla base del codice in cmd.
ricompleter
rlcompleter usa readline per aggiungere il completamento con tab all'interprete Python interattivo.