readline — La biblioteca readline de GNU

Propósito:Proporciona una interfaz a la biblioteca readline de GNU para interactuar con el usuario en un símbolo del sistema.

El módulo readline se puede usar para mejorar programas de línea de comando interactivo para que sean más fáciles de usar. Se utiliza principalmente para proporcionar la complementación del texto de la línea de comandos o «complementación por tabulador».

Nota

Debido a que readline interactúa con el contenido de la consola, la impresión de mensajes de depuración dificulta ver lo que está sucediendo en el código de muestra en comparación con lo que readline está haciendo de forma gratuita. Los siguientes ejemplos usan el módulo logging para escribir información de depuración en un archivo separado. La salida del registro se muestra con cada ejemplo.

Nota

Las bibliotecas GNU necesarias para readline no están disponibles en todas las plataformas de forma predeterminada. Si tu sistema no los incluye, es posible que debas volver a compilar el intérprete de Python para habilitar el módulo, después de instalar las dependencias. También se distribuye una versión independiente de la biblioteca desde el índice del paquete Python en el nombre gnureadline. Los ejemplos en esta sección primero intentan importar gnureadline, y luego recurren a readline.

Un agradecimiento especial a Jim Baker por señalar este paquete.

Configuración

Hay dos formas de configurar la biblioteca readline subyacente, utilizando un archivo de configuración o la función parse_and_bind(). Las opciones de configuración incluyen la combinación de teclas para invocar la complementación, modos de edición (vi o emacs) y muchos otros valores. Consulta la documentación de la biblioteca de línea de lectura de GNU para detalles.

La forma más fácil de habilitar la complementación por tabulador es mediante una llamada a parse_and_bind(). Se pueden configurar otras opciones al mismo tiempo. Este ejemplo cambia los controles de edición para usar el modo «vi» en lugar de el valor predeterminado de «emacs». Para editar la línea de entrada actual, presione ESC luego use las teclas de navegación vi normales como j, k, l y 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" to quit): ')
    if line == 'stop':
        break
    print('ENTERED: {!r}'.format(line))

La misma configuración se puede almacenar como instrucciones en un archivo leído por la biblioteca con una sola llamada. Si myreadline.rc contiene

myreadline.rc
# Turn on tab completion
tab: complete

# Use vi editing mode instead of emacs
set editing-mode vi

el archivo se puede leer 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" to quit): ')
    if line == 'stop':
        break
    print('ENTERED: {!r}'.format(line))

Completar texto

Este programa tiene un conjunto incorporado de posibles comandos y usa la complementación por tabulador cuando el usuario está ingresando instrucciones.

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:
            # This is the first time for this text,
            # so build a match list.
            if text:
                self.matches = [
                    s
                    for s in self.options
                    if s and s.startswith(text)
                ]
                logging.debug('%s matches: %s',
                              repr(text), self.matches)
            else:
                self.matches = self.options[:]
                logging.debug('(empty input) matches: %s',
                              self.matches)

        # Return the state'th item from the match list,
        # if we have that many.
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('complete(%s, %s) => %s',
                      repr(text), state, repr(response))
        return response


def input_loop():
    line = ''
    while line != 'stop':
        line = input('Prompt ("stop" to quit): ')
        print('Dispatch {}'.format(line))


# Register the completer function
OPTIONS = ['start', 'stop', 'list', 'print']
readline.set_completer(SimpleCompleter(OPTIONS).complete)

# Use the tab key for completion
readline.parse_and_bind('tab: complete')

# Prompt the user for text
input_loop()

La función input_loop() lee una línea tras otra hasta que el valor de entrada sea "stop". Un programa más sofisticado en realidad podría analizar la línea de entrada y ejecutar el comando.

La clase SimpleCompleter mantiene una lista de «opciones» que son candidatos para la complementación. El método complete() para un instancia está diseñada para registrarse con readline como fuente de complementación. Los argumentos son una cadena de text para completar y un valor de state, que indica cuántas veces ha sido llamada la función con el mismo texto. La función se llama repetidamente con el estado incrementado cada vez. Debería devolver una cadena si hay un candidato para ese valor de estado o None si no hay más candidatos. La implementación de complete() aquí busca un conjunto de coincidencias cuando el estado es 0, y luego devuelve todos los candidato que coinciden, uno a la vez, en las llamadas posteriores.

Cuando se ejecuta, la salida inicial es:

$ python3 readline_completer.py

Prompt ("stop" to quit):

Al presionar TAB dos veces, se imprime una lista de opciones.

$ python3 readline_completer.py

Prompt ("stop" to quit):
list   print  start  stop
Prompt ("stop" to quit):

El archivo de registro muestra que complete() se llamó con dos secuencias de valores de estado.

$ tail -f /tmp/completer.log

(empty input) matches: ['list', 'print', 'start', 'stop']
complete('', 0) => 'list'
complete('', 1) => 'print'
complete('', 2) => 'start'
complete('', 3) => 'stop'
complete('', 4) => None
(empty input) matches: ['list', 'print', 'start', 'stop']
complete('', 0) => 'list'
complete('', 1) => 'print'
complete('', 2) => 'start'
complete('', 3) => 'stop'
complete('', 4) => None

La primera secuencia es de la primera pulsación de tecla TAB. El algoritmo de complementación solicita todos los candidatos pero no expande la línea de entrada vacía. Luego, en el segundo TAB, la lista de candidatos se recalcula para que se pueda imprimir para el usuario.

Si la siguiente entrada es «l» seguida de otra TAB, la pantalla muestra:

Prompt ("stop" to quit): list

y el registro refleja los diferentes argumentos para complete():

'l' matches: ['list']
complete('l', 0) => 'list'
complete('l', 1) => None

Presionar RETURN ahora hace que input() devuelva el valor, y los ciclos del bucle while.

Dispatch list
Prompt ("stop" to quit):

Hay dos finalizaciones posibles para un comando que comienza con «s». Escribir «s», luego presionar TAB encuentra que «start» y «stop» son candidatos, pero solo completa parcialmente el texto en la pantalla agregando una «t».

El archivo de registro muestra:

's' matches: ['start', 'stop']
complete('s', 0) => 'start'
complete('s', 1) => 'stop'
complete('s', 2) => None

y la pantalla:

Prompt ("stop" to quit): st

Nota

Si una función de complementación lanza una excepción, se ignora en silencio y readline supone que no hay complementaciones coincidentes.

Acceso al búfer de complementación

El algoritmo de complementación en SimpleCompleter solo se fija en el argumento de texto pasado a la función, pero no utiliza más del estado interno de readline. También es posible usar funciones de readline para manipular el texto del búfer de entrada.

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:
            # This is the first time for this text,
            # so build a match list.

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

            logging.debug('origline=%s', repr(origline))
            logging.debug('begin=%s', begin)
            logging.debug('end=%s', end)
            logging.debug('being_completed=%s', being_completed)
            logging.debug('words=%s', words)

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

                    if being_completed:
                        # match options with portion of input
                        # being completed
                        self.current_candidates = [
                            w for w in candidates
                            if w.startswith(being_completed)
                        ]
                    else:
                        # matching empty string,
                        # use all candidates
                        self.current_candidates = candidates

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

                except (KeyError, IndexError) as err:
                    logging.error('completion error: %s', err)
                    self.current_candidates = []

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


def input_loop():
    line = ''
    while line != 'stop':
        line = input('Prompt ("stop" to quit): ')
        print('Dispatch {}'.format(line))


# Register our completer function
completer = BufferAwareCompleter({
    'list': ['files', 'directories'],
    'print': ['byname', 'bysize'],
    'stop': [],
})
readline.set_completer(completer.complete)

# Use the tab key for completion
readline.parse_and_bind('tab: complete')

# Prompt the user for text
input_loop()

En este ejemplo, se están completando comandos con subopciones. El método complete() necesita fijarse en la posición de la complementación dentro del búfer de entrada para determinar si es parte de la primera palabra o una palabra posterior. Si el objetivo es la primera palabra, las llaves del diccionario de opciones se utilizan como candidatas. Si no es la primera palabra, luego la primera palabra se usa para buscar candidatos del diccionario de opciones.

Hay tres comandos de nivel superior, dos de los cuales tienen subcomandos.

  • list
    • files
    • directories
  • print
    • byname
    • bysize
  • stop

Siguiendo la misma secuencia de acciones que antes, presionar TAB dos veces da los tres comandos de nivel superior:

$ python3 readline_buffer.py

Prompt ("stop" to quit):
list   print  stop
Prompt ("stop" to quit):

y en el registro:

origline=''
begin=0
end=0
being_completed=
words=[]
complete('', 0) => list
complete('', 1) => print
complete('', 2) => stop
complete('', 3) => None
origline=''
begin=0
end=0
being_completed=
words=[]
complete('', 0) => list
complete('', 1) => print
complete('', 2) => stop
complete('', 3) => None

Si la primera palabra es "list" (con un espacio después de la palabra), los candidatos para la finalización son diferentes.

Prompt ("stop" to quit): list
directories  files

El registro muestra que el texto que se está completando no es la línea completa, pero la porción después de list.

origline='list '
begin=5
end=5
being_completed=
words=['list']
candidates=['files', 'directories']
complete('', 0) => files
complete('', 1) => directories
complete('', 2) => None
origline='list '
begin=5
end=5
being_completed=
words=['list']
candidates=['files', 'directories']
complete('', 0) => files
complete('', 1) => directories
complete('', 2) => None

Historial de entrada

readline sigue el historial de entrada automáticamente. Hay dos diferentes conjuntos de funciones para trabajar con la historia. La historia para la sesión actual se puede acceder con get_current_history_length() y get_history_item(). El mismo historial se puede guardar en un archivo para volver a cargarlo más tarde usando write_history_file() y read_history_file(). Por defecto se guarda todo el historial pero la longitud máxima del archivo puede ser establecida con set_history_length(). Una longitud de -1 significa que no hay límite.

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 = []

    def complete(self, text, state):
        response = None
        if state == 0:
            history_values = get_history_items()
            logging.debug('history: %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('matches: %s', self.matches)
        try:
            response = self.matches[state]
        except IndexError:
            response = None
        logging.debug('complete(%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('Max history file length:',
          readline.get_history_length())
    print('Startup history:', get_history_items())
    try:
        while True:
            line = input('Prompt ("stop" to quit): ')
            if line == 'stop':
                break
            if line:
                print('Adding {!r} to the history'.format(line))
    finally:
        print('Final history:', get_history_items())
        readline.write_history_file(HISTORY_FILENAME)


# Register our completer function
readline.set_completer(HistoryCompleter().complete)

# Use the tab key for completion
readline.parse_and_bind('tab: complete')

# Prompt the user for text
input_loop()

El HistoryCompleter recuerda todo lo escrito y usa esos valores al completar entradas posteriores.

$ python3 readline_history.py

Max history file length: -1
Startup history: []
Prompt ("stop" to quit): foo
Adding 'foo' to the history
Prompt ("stop" to quit): bar
Adding 'bar' to the history
Prompt ("stop" to quit): blah
Adding 'blah' to the history
Prompt ("stop" to quit): b
bar   blah
Prompt ("stop" to quit): b
Prompt ("stop" to quit): stop
Final history: ['foo', 'bar', 'blah', 'stop']

El registro muestra esta salida cuando a la «b» le siguen dos TABs.

history: ['foo', 'bar', 'blah']
matches: ['bar', 'blah']
complete('b', 0) => 'bar'
complete('b', 1) => 'blah'
complete('b', 2) => None
history: ['foo', 'bar', 'blah']
matches: ['bar', 'blah']
complete('b', 0) => 'bar'
complete('b', 1) => 'blah'
complete('b', 2) => None

Cuando la secuencia de comandos se ejecuta por segunda vez, se lee todo el historial del archivo.

$ python3 readline_history.py

Max history file length: -1
Startup history: ['foo', 'bar', 'blah', 'stop']
Prompt ("stop" to quit):

Hay funciones para eliminar elementos individuales del historial y también borrar toda la historia.

Ganchos

Hay varios ganchos disponibles para activar acciones como parte de la secuencia de interacción. El gancho startup se invoca inmediatamente antes de imprimir el símbolo del sistema, y el enlace pre-input se ejecuta después del símbolo del sistema pero antes de leer el texto del usuario.

readline_hooks.py
try:
    import gnureadline as readline
except ImportError:
    import readline


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


def pre_input_hook():
    readline.insert_text(' from 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" to quit): ')
    if line == 'stop':
        break
    print('ENTERED: {!r}'.format(line))

Cualquiera de los ganchos es un lugar potencialmente bueno para usar insert_text() para modificar el búfer de entrada.

$ python3 readline_hooks.py

Prompt ("stop" to quit): from startup_hook from pre_input_hook

Si el búfer se modifica dentro del gancho de pre-entrada, redisplay() debe ser llamado para actualizar la pantalla.

Ver también

  • Documentación de la biblioteca estándar para readline
  • GNU readline – Documentación para la biblioteca readline de GNU.
  • formato de archivo init de readline – El formato de archivo de inicialización y configuración.
  • effbot: El módulo readline – guía para el módulo readline de Effbot.
  • gnureadline – Una versión estáticamente vinculada de readline disponible para muchas plataformas e instalable a través de pip.
  • pyreadline – pyreadline, desarrollado como un reemplazo basado en Python de readline para ser usado en Windows.
  • cmd – El módulo cmd usa readline ampliamente para implementar la complementación por tabulador en la interfaz de comando. Algunos de los ejemplos aquí fueron adaptados del código en cmd
  • rlcompleterrlcompleter usa readline para añadir complementación por tabulador al intérprete de Python.