| Scopo | Fornisce una interfaccia alla libreria GNU readline per l'interazione con l'utente al prompt di comando. |
| Versione Python | 1.4 e superiore |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo readline
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.).
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
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
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:
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
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.
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: