cmd — Procesadores de comando orientados a líneas

Propósito:Crea procesadores de comandos orientados a líneas.

El módulo cmd contiene una clase pública, Cmd, diseñada para usarse como clase base para shells interactivos y otros intérpretes de comandos. Por defecto utiliza readline para el manejo interactivo de solicitudes, edición de línea de comandos y complementación de comandos.

Procesamiento de comandos

Un intérprete de comandos creado con cmd usa un bucle para leer todas las líneas de su entrada, analizarlas y luego enviar el comando a un controlador de comandos apropiado. Las líneas de entrada se analizan en dos partes: el comando y cualquier otro texto en la línea. Si el usuario ingresa foo bar, y la clase de intérprete incluye un método llamado do_foo(), se ejecuta con "bar" como único argumento.

El marcador de fin de archivo se envía a do_EOF(). Si un controlador de comandos devuelve un valor verdadero, el programa saldrá limpiamente. Entonces para dar una forma limpia de salir del intérprete, asegúrate de implementar do_EOF() y hacer que devuelva True.

Este sencillo programa de ejemplo admite el comando «greet»:

cmd_simple.py
import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, line):
        print("hello")

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Ejecutarlo interactivamente demuestra cómo se envían los comandos y muestra algunas de las características incluidas en Cmd.

$ python3 cmd_simple.py

(Cmd)

Lo primero que debes notar es el símbolo del sistema, (Cmd). La solicitud se puede configurar a través del atributo prompt. El valor de solicitud es dinámico, y si un controlador de comando cambia el atributo de solicitud, el nuevo valor se utiliza para consultar el siguiente comando.

Documented commands (type help <topic>):
========================================
help

Undocumented commands:
======================
EOF  greet

El comando help está integrado en Cmd. Sin argumentos, help muestra la lista de comandos disponibles. Si la entrada incluye un nombre de comando, la salida es más detallada y está restringida a los detalles de ese comando, cuando esté disponible.

Si el comando es greet, se invoca do_greet() para manejarlo:

(Cmd) greet
hello

Si la clase no incluye un controlador específico para un comando, se ejecuta el método default() con toda la línea de entrada como argumento. La implementación incorporada de default() informa un error.

(Cmd) foo
*** Unknown syntax: foo

Como do_EOF() devuelve True, escribir Ctrl-D hace que el intérprete salga.

(Cmd) ^D$

No se imprime ninguna nueva línea al salir, por lo que los resultados son un poco confusos.

Argumentos de comando

Este ejemplo incluye algunas mejoras para eliminar algunas de las molestias y agregar ayuda para el comando greet.

cmd_arguments.py
import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, person):
        """greet [person]
        Greet the named person"""
        if person:
            print("hi,", person)
        else:
            print('hi')

    def do_EOF(self, line):
        return True

    def postloop(self):
        print()


if __name__ == '__main__':
    HelloWorld().cmdloop()

La cadena de documentación agregada a do_greet() se convierte en el texto de ayuda para el comando:

$ python3 cmd_arguments.py

(Cmd) help

Documented commands (type help <topic>):
========================================
greet  help

Undocumented commands:
======================
EOF

(Cmd) help greet
greet [person]
        Greet the named person

La salida muestra un argumento opcional para greet, person. Aunque el argumento es opcional para el comando, existe una distinción entre el comando y el método de devolución de llamada. El método siempre toma el argumento, pero a veces el valor es una cadena vacía. Le corresponde al controlador de comandos determinar si un argumento vacío es válido, o realizar cualquier análisis y procesamiento adicional del comando. En este ejemplo, si se proporciona el nombre de una persona, entonces el saludo es personalizado.

(Cmd) greet Alice
hi, Alice
(Cmd) greet
hi

Ya sea que el usuario proporcione un argumento o no, el valor pasado al controlador de comando no incluye el comando en sí. Eso simplifica el análisis en el controlador de comandos, especialmente si se necesitan múltiples argumentos.

Ayuda en vivo

En el ejemplo anterior, el formato del texto de ayuda deja algo que desear. Como proviene de la cadena de documentación, retiene la sangría del archivo fuente. La fuente podría cambiarse para eliminar el espacio en blanco adicional, pero eso dejaría el código de la aplicación con un formato deficiente. Una mejor solución es implementar un controlador de ayuda para el comando greet, llamado help_greet(). Se llama al manejador de ayuda para producir texto de ayuda para el comando nombrado.

cmd_do_help.py
# Set up gnureadline as readline if installed.
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class HelloWorld(cmd.Cmd):

    def do_greet(self, person):
        if person:
            print("hi,", person)
        else:
            print('hi')

    def help_greet(self):
        print('\n'.join([
            'greet [person]',
            'Greet the named person',
        ]))

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

En este ejemplo, el texto es estático pero con un formato más agradable. También sería posible utilizar el estado del comando anterior para adaptar el contenido del texto de ayuda al contexto actual.

$ python3 cmd_do_help.py

(Cmd) help greet
greet [person]
Greet the named person

Depende del manejador de ayuda enviar el mensaje de ayuda y no simplemente devolver el texto de ayuda para su manejo en otro lugar.

Auto complementación

Cmd incluye soporte para la complementación de comandos en función de los nombres de los comandos con métodos de controlador. El usuario activa la complementación presionando la tecla de tabulación en una solicitud de entrada. Cuando son posibles varias finalizaciones, presionar la tecla dos veces imprime una lista de opciones.

Nota

Las bibliotecas GNU necesarias para readline no están disponibles en todas las plataformas de forma predeterminada. En esos casos, la complementación puede no funcionar. Consulta readline para obtener consejos sobre cómo instalar las bibliotecas necesarias si tu instalación de Python no las tiene.

$ python3 cmd_do_help.py

(Cmd) <tab><tab>
EOF    greet  help
(Cmd) h<tab>
(Cmd) help

Una vez que se conoce el comando, la complementación del argumento se maneja mediante métodos con el prefijo complete_. Esto permite que los nuevos controladores de complementación reúnan una lista de posibles complementaciones utilizando criterios arbitrarios (es decir, consultar una base de datos o mirar un archivo o directorio en el sistema de archivos). En este caso, el programa tiene un conjunto codificado de «friends» que reciben un saludo menos formal que los desconocidos nombrados o anónimos. Un programa real probablemente guardaría la lista en algún lugar, la leería una vez y luego guardaría en caché los contenidos para escanearlos según sea necesario.

cmd_arg_completion.py
# Set up gnureadline as readline if installed.
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class HelloWorld(cmd.Cmd):

    FRIENDS = ['Alice', 'Adam', 'Barbara', 'Bob']

    def do_greet(self, person):
        "Greet the person"
        if person and person in self.FRIENDS:
            greeting = 'hi, {}!'.format(person)
        elif person:
            greeting = 'hello, {}'.format(person)
        else:
            greeting = 'hello'
        print(greeting)

    def complete_greet(self, text, line, begidx, endidx):
        if not text:
            completions = self.FRIENDS[:]
        else:
            completions = [
                f
                for f in self.FRIENDS
                if f.startswith(text)
            ]
        return completions

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Cuando hay texto de entrada, complete_greet() devuelve una lista de amigos que coinciden. De lo contrario, se devuelve la lista completa de amigos.

$ python3 cmd_arg_completion.py

(Cmd) greet <tab><tab>
Adam     Alice    Barbara  Bob
(Cmd) greet A<tab><tab>
Adam   Alice
(Cmd) greet Ad<tab>
(Cmd) greet Adam
hi, Adam!

Si el nombre dado no está en la lista de amigos, se da el saludo formal.

(Cmd) greet Joe
hello, Joe

Anulación de métodos de clase base

Cmd incluye varios métodos que pueden anularse como ganchos para realizar acciones o alterar el comportamiento de la clase base. Este ejemplo no es exhaustivo, pero contiene muchos de los métodos comúnmente útiles.

cmd_illustrate_methods.py
# Set up gnureadline as readline if installed.
try:
    import gnureadline
    import sys
    sys.modules['readline'] = gnureadline
except ImportError:
    pass

import cmd


class Illustrate(cmd.Cmd):
    "Illustrate the base class method use."

    def cmdloop(self, intro=None):
        print('cmdloop({})'.format(intro))
        return cmd.Cmd.cmdloop(self, intro)

    def preloop(self):
        print('preloop()')

    def postloop(self):
        print('postloop()')

    def parseline(self, line):
        print('parseline({!r}) =>'.format(line), end='')
        ret = cmd.Cmd.parseline(self, line)
        print(ret)
        return ret

    def onecmd(self, s):
        print('onecmd({})'.format(s))
        return cmd.Cmd.onecmd(self, s)

    def emptyline(self):
        print('emptyline()')
        return cmd.Cmd.emptyline(self)

    def default(self, line):
        print('default({})'.format(line))
        return cmd.Cmd.default(self, line)

    def precmd(self, line):
        print('precmd({})'.format(line))
        return cmd.Cmd.precmd(self, line)

    def postcmd(self, stop, line):
        print('postcmd({}, {})'.format(stop, line))
        return cmd.Cmd.postcmd(self, stop, line)

    def do_greet(self, line):
        print('hello,', line)

    def do_EOF(self, line):
        "Exit"
        return True


if __name__ == '__main__':
    Illustrate().cmdloop('Illustrating the methods of cmd.Cmd')

cmdloop() es el bucle de procesamiento principal del intérprete. Por lo general, no es necesario anularlo, ya que los ganchos preloop() y postloop() están disponibles.

Cada iteración a través de cmdloop() llama a onecmd() para enviar el comando a su controlador. La línea de entrada real se analiza con parseline() para crear una tupla que contenga el comando y la parte restante de la línea.

Si la línea está vacía, se llama a emptyline(). La implementación predeterminada ejecuta el comando anterior nuevamente. Si la línea contiene un comando, primero se llama a precmd(), luego se busca el controlador y se invoca. Si no se encuentra ninguno, se llama default() en su lugar. Finalmente se llama a postcmd().

Aquí hay una sesión de ejemplo con declaraciones print agregadas:

$ python3 cmd_illustrate_methods.py

cmdloop(Illustrating the methods of cmd.Cmd)
preloop()
Illustrating the methods of cmd.Cmd
(Cmd) greet Bob
precmd(greet Bob)
onecmd(greet Bob)
parseline(greet Bob) => ('greet', 'Bob', 'greet Bob')
hello, Bob
postcmd(None, greet Bob)
(Cmd) ^Dprecmd(EOF)
onecmd(EOF)
parseline(EOF) => ('EOF', '', 'EOF')
postcmd(True, EOF)
postloop()

Configurar Cmd a través de atributos

Además de los métodos descritos anteriormente, existen varios atributos para el controlar de comandos. prompt se puede establecer en una cadena para que se imprima cada vez que se le pide al usuario un nuevo comando. intro es el mensaje de «bienvenida» que se imprime al inicio del programa. cmdloop() toma un argumento para este valor, o se puede establecer en la clase directamente. Al imprimir ayuda, los atributos doc_header, misc_header, undoc_header y ruler se utilizan para formatear la salida.

cmd_attributes.py
import cmd


class HelloWorld(cmd.Cmd):

    prompt = 'prompt: '
    intro = "Simple command processor example."

    doc_header = 'doc_header'
    misc_header = 'misc_header'
    undoc_header = 'undoc_header'

    ruler = '-'

    def do_prompt(self, line):
        "Change the interactive prompt"
        self.prompt = line + ': '

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    HelloWorld().cmdloop()

Esta clase de ejemplo muestra un controlador de comandos que permite que el usuario controle la solicitud de la sesión interactiva.

$ python3 cmd_attributes.py

Simple command processor example.
prompt: prompt hello
hello: help

doc_header
----------
help  prompt

undoc_header
------------
EOF

hello:

Ejecutar comandos de shell

Para complementar el procesamiento de comandos estándar, Cmd incluye dos prefijos de comandos especiales. Un signo de interrogación (?) es equivalente al comando help incorporado, y se puede usar de la misma manera. Un signo de exclamación (!) se asigna a do_shell(), y está destinado para ejecutar otros comandos en el shell, como en este ejemplo.

cmd_do_shell.py
import cmd
import subprocess


class ShellEnabled(cmd.Cmd):

    last_output = ''

    def do_shell(self, line):
        "Run a shell command"
        print("running shell command:", line)
        sub_cmd = subprocess.Popen(line,
                                   shell=True,
                                   stdout=subprocess.PIPE)
        output = sub_cmd.communicate()[0].decode('utf-8')
        print(output)
        self.last_output = output

    def do_echo(self, line):
        """Print the input, replacing '$out' with
        the output of the last shell command.
        """
        # Obviously not robust
        print(line.replace('$out', self.last_output))

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    ShellEnabled().cmdloop()

Esta implementación del comando echo reemplaza la cadena $out en su argumento con la salida del comando de shell anterior.

$ python3 cmd_do_shell.py

(Cmd) ?

Documented commands (type help <topic>):
========================================
echo  help  shell

Undocumented commands:
======================
EOF

(Cmd) ? shell
Run a shell command
(Cmd) ? echo
Print the input, replacing '$out' with
        the output of the last shell command
(Cmd) shell pwd
running shell command: pwd
.../pymotw-3/source/cmd

(Cmd) ! pwd
running shell command: pwd
.../pymotw-3/source/cmd

(Cmd) echo $out
.../pymotw-3/source/cmd

Entradas Alternativas

Si bien el modo predeterminado para Cmd() es interactuar con el usuario a través de la biblioteca readline, también es posible pasar una serie de comandos a la entrada estándar utilizando la redirección de shell estándar de Unix.

$ echo help | python3 cmd_do_help.py

(Cmd)
Documented commands (type help <topic>):
========================================
greet  help

Undocumented commands:
======================
EOF

(Cmd)

Para que el programa lea un archivo de secuencia de comandos directamente, pueden ser necesarios algunos otros cambios. Dado que readline interactúa con el dispositivo terminal/tty, en lugar de la secuencia de entrada estándar, debe deshabilitarse cuando la secuencia de comandos va a leer desde un archivo. Además, para evitar imprimir solicitudes superfluas, la solicitud se puede establecer en una cadena vacía. Este ejemplo muestra cómo abrir un archivo y pasarlo como entrada a una versión modificada del ejemplo HelloWorld.

cmd_file.py
import cmd


class HelloWorld(cmd.Cmd):

    # Disable rawinput module use
    use_rawinput = False

    # Do not show a prompt after each command read
    prompt = ''

    def do_greet(self, line):
        print("hello,", line)

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    import sys
    with open(sys.argv[1], 'rt') as input:
        HelloWorld(stdin=input).cmdloop()

Con use_rawinput establecido en False y prompt establecido en una cadena vacía, la secuencia de comando se puede invocar en un archivo de entrada con un comando en cada línea.

cmd_file.txt
greet
greet Alice and Bob

La ejecución de la secuencia de comandos de ejemplo con la entrada de ejemplo produce el siguiente resultado.

$ python3 cmd_file.py cmd_file.txt

hello,
hello, Alice and Bob

Comandos de sys.argv

Los argumentos de la línea de comandos para el programa también se pueden procesar como comandos para la clase de intérprete, en lugar de leer comandos desde la consola o un archivo. Para usar los argumentos de la línea de comandos, llama a onecmd() directamente, como en este ejemplo.

cmd_argv.py
import cmd


class InteractiveOrCommandLine(cmd.Cmd):
    """Accepts commands via the normal interactive
    prompt or on the command line.
    """

    def do_greet(self, line):
        print('hello,', line)

    def do_EOF(self, line):
        return True


if __name__ == '__main__':
    import sys
    if len(sys.argv) > 1:
        InteractiveOrCommandLine().onecmd(' '.join(sys.argv[1:]))
    else:
        InteractiveOrCommandLine().cmdloop()

Como onecmd() toma una sola cadena como entrada, los argumentos del programa deben unirse antes de pasarlos.

$ python3 cmd_argv.py greet Command-Line User

hello, Command-Line User

$ python3 cmd_argv.py

(Cmd) greet Interactive User
hello, Interactive User
(Cmd)

Ver también