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.).
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.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
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
- pernome
- perdimensione
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.