readline - La Libreria GNU readline

Scopo: Fornisce una interfaccia alla libreria GNU readline per l'interazione con l'utente da riga di comando.

Il modulo readline può essere usato per migliorare i programmi interattivi di riga di comando per facilitarne l'utilizzo. E' principalmente usato per fornire 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.
Le libreria GNU necessarie per readline non sono disponibili in modalità predefinita su tutte le piattaforme. Se il proprio sistema non le comprende, si potrebbe aver bisogno di ricompilare l'interprete Python per abilitare il modulo, dopo avere installato le dipendenze. Un versione a se stante della libreria viene distribuita dall'Indirizzario dei Pacchetti Python sotto il nome di gnureadline.

Un ringraziamento speciale a Jim Baker per la segnalazione di questo pacchetto.

Configurazione

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 l'identificazione del carattere da tastiera per l'attivazione 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 è con 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 prema ESC, quindi si usino i normali tasti di spostamento impostati in vi come j, k, l ed h.

# readline_parse_and_bind.py


try:
    import gnureadline as readline
except ImportError:
    import readline

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

while True:
    line = input('Prompt ("stop" per abbandonare): ')
    if line == 'stop':
        break
    print('DIGITATO: {!r}'.format(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:

# myreadline.rc

# Attiva il completamento
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():

# readline_read_init_file.py


try:
    import gnureadline as readline
except ImportError:
    import readline

readline.read_init_file('myreadline.rc')

while True:
    line = input('Prompt ("stop" per abbandonare): ')
    if line == 'stop':
        break
    print('DIGITATO: {!r}'.format(line))

Completamento del Testo

Questo programma ha un insieme di possibili comandi definiti al suo interno e usa il completamento via tab quando l'utente digita le istruzioni.

# readline_completer.py


try:
    import gnureadline as readline
except ImportError:
    import readline
import logging

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


class SimpleCompleter:

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

    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 corrispondenze: %s',
                              repr(text), self.matches)
            else:
                self.matches = self.options[:]
                logging.debug('(input vuoto) corrispondenze: %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 = input('Prompt ("stop" per abbandonare): ')
        print('Invia {}'.format(line))


# Registra la funzione di completamento
OPTIONS = ['start', 'stop', 'elenco', 'stampa']
readline.set_completer(SimpleCompleter(OPTIONS).complete)

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

# Richiede testo all'utente
input_loop()

La funzione input_loop() legge una riga dopo l'altra fino a che il valore immesso è "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 e 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, alle chiamate successive.

Quando eseguito lo script, il risultato iniziale è:

$ python3 readline_completer.py

Prompt ("stop" per abbandonare):

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

$ python3 readline_completer.py

Prompt ("stop" per abbandonare):
elenco  stampa  start   stop
Prompt ("stop" per abbandonare):

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

$ tail -f /tmp/completer.log

(input vuoto) corrispondenze: ['elenco', 'stampa', 'start', 'stop']
completato('', 0) => 'elenco'
completato('', 1) => 'stampa'
completato('', 2) => 'start'
completato('', 3) => 'stop'
completato('', 4) => None
(input vuoto) corrispondenze: ['elenco', 'stampa', 'start', 'stop']
completato('', 0) => 'elenco'
completato('', 1) => 'stampa'
completato('', 2) => 'start'
completato('', 3) => 'stop'
completato('', 4) => None

La prima sequenza proviene dalla prima pressione del tasto TAB. L'algoritmo di completamento richiama 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 per l'utente.

Se l'input successivo è "e" seguito da un altro TAB, la videata mostra:

Prompt ("stop" per abbandonare): elenco

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

$ tail -f /tmp/completer.log

'e' corrispondenze: ['elenco']
completato('e', 0) => 'elenco'
completato('e', 1) => None

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

Invia elenco
Prompt ("stop" per abbandonare):

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

Il file di log mostra:

's' corrispondenze: ['stampa', 'start', 'stop']
completato('s', 0) => 'stampa'
completato('s', 1) => 'start'
completato('s', 2) => 'stop'
completato('s', 3) => None

e lo schermo:

Prompt ("stop" per abbandonare): st
Se la funzione di completamento 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 in SimpleCompleter cerca solo l'argomento text passato alla funzione, ma non usa null'altro dello stato interno di readline. E' anche possibile usare le funzioni di readline per manipolare il testo nel buffer di input.

# readline_buffer.py

try:
    import gnureadline as readline
except ImportError:
    import readline
import logging


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

class BufferAwareCompleter:

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

    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) as 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 = input('Prompt ("stop" per uscire): ')
        print('Inviato {}'.format(line))


# Registrazione della propria funzione di completamento
readline.set_completer(BufferAwareCompleter(
    {'elenca':['file', 'directory'],
     'stampa':['pernome', 'perdimensione'],
     '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 successiva. Se l'obiettivo è la prima parola, le chiavi dei dizionario di opzioni vengono usate come candidati. Se non si tratta della prima parola, allora 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:

$ python3 readline_buffer.py

Prompt ("stop" per uscire):
elenca  stampa  stop
Prompt ("stop" per uscire):

Il registro riporta:

riga originale=''
inizio=0
fine=0
in completamento=
parole=[]
completato('', 0) => elenca
completato('', 1) => stampa
completato('', 2) => stop
completato('', 3) => None
riga originale=''
inizio=0
fine=0
in completamento=
parole=[]
completato('', 0) => elenca
completato('', 1) => stampa
completato('', 2) => stop
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 dopo elenca.

riga originale='elenca '
inizio=7
fine=7
in completamento=
parole=['elenca']
candidati=['file', 'directory']
completato('', 0) => file
completato('', 1) => directory
completato('', 2) => None
riga originale='elenca '
inizio=7
fine=7
in completamento=
parole=['elenca']
candidati=['file', 'directory']
completato('', 0) => file
completato('', 1) => directory
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ò essere 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.

# readline_history.py

try:
    import gnureadline as readline
except ImportError:
    import readline
import logging
import os

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

logging.basicConfig(
    format='%(message)s',
    filename=LOG_FILENAME,
    level=logging.DEBUG,
)


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


class HistoryCompleter:

    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 = input('Prompt ("stop" per uscire): ')
            if line == 'stop':
                break
            if line:
                print('Aggiunta di {!r} allo storico'.format(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 e usa quei valori per il completamento dell'input successivo.

$ python3 readline_history.py
Lunghezza max file storico: -1
Storico di partenza: []
Prompt ("stop" per uscire): foo
Aggiunta di 'foo' allo storico
Prompt ("stop" per uscire): bar
Aggiunta di 'bar' allo storico
Prompt ("stop" per uscire): blah
Aggiunta di 'blah' allo 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.

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

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

$ python3 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.

# readline_hooks.py

try:
    import gnureadline as readline
except ImportError:
    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 = input('Prompt ("stop" per uscire): ')
    if line == 'stop':
        break
    print('DIGITATO: {!r}'.format(line))

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

$ python3 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.
gnureadline
Una versione collegata staticamente disponibile per molte piattaforme e installabile via pip.
pyreadline
pyreadline, sviluppato come rimpiazzo basato su Python per readline da usare con Windows.
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.