El módulo cmd en Python ofrece una estructura lista para crear intérpretes de comandos orientados a líneas. En lugar de escribir manualmente un bucle con input(), extraer el nombre de la acción, buscar una función e implementar ayuda, heredas de cmd.Cmd y defines métodos cuyo nombre comienza con do_.
Este modelo resulta útil en consolas administrativas, herramientas de pruebas, prototipos, simuladores, utilidades de base de datos e interfaces locales para servicios. No sustituye una CLI tradicional basada en argumentos ni una aplicación gráfica, pero proporciona una sesión interactiva persistente con prompt, historial y autocompletado cuando readline está disponible.
Primera consola con cmd.Cmd
import cmd
class Consola(cmd.Cmd):
intro = 'Consola iniciada. Escribe help o ?.'
prompt = '(app) '
def do_estado(self, arg):
'Muestra el estado actual: estado'
print('Sistema operativo')
def do_salir(self, arg):
'Cierra la consola: salir'
print('Hasta luego')
return True
if __name__ == '__main__':
Consola().cmdloop()El texto después de do_ se convierte en el nombre del comando. El valor devuelto llega a postcmd(); cuando el resultado final es verdadero, cmdloop() termina.
Cómo funciona el despacho
La clase interpreta el primer prefijo de la línea como comando y entrega el resto como una única cadena. Por tanto, exportar clientes.csv --compacto llama a do_exportar() con clientes.csv --compacto.
def do_echo(self, arg):
self.stdout.write(arg + '\n')El framework no analiza opciones complejas automáticamente. Utiliza shlex.split() para respetar comillas o integra argparse dentro de cada acción. La guía de shlex en Python explica esta tokenización.
Ayuda incorporada
Toda subclase hereda el comando help. La docstring de do_estado() aparece en help estado. Para una explicación más extensa, implementa help_estado().
def do_backup(self, arg):
'Crea un backup: backup DESTINO'
...
def help_backup(self):
self.stdout.write('Uso: backup DESTINO\n')
self.stdout.write('Copia datos a una carpeta aprobada.\n')Sin argumento, la ayuda lista temas documentados, no documentados y auxiliares. Los encabezados pueden personalizarse con doc_header, undoc_header y misc_header.
Autocompletar comandos y argumentos
Cuando readline está disponible, los nombres de comandos se completan automáticamente. Para argumentos específicos, implementa complete_nombre().
ENTORNOS = ['dev', 'staging', 'produccion']
def complete_entorno(self, text, line, begidx, endidx):
return [nombre for nombre in ENTORNOS if nombre.startswith(text)]
def do_entorno(self, arg):
if arg not in ENTORNOS:
self.stdout.write('Entorno inválido\n')
return
self.entorno = argLos parámetros de línea e índices permiten variar las sugerencias según la posición. No muestres secretos, rutas restringidas ni recursos que el usuario no pueda consultar.
Validar todos los argumentos
La entrada del prompt no es confiable. Separa tokens, valida cantidad, tipos, rangos, rutas y permisos. Nunca reenvíes el texto directamente a eval(), exec() o un shell.
import shlex
class Consola(cmd.Cmd):
def do_usuario(self, arg):
try:
tokens = shlex.split(arg)
except ValueError as error:
self.stdout.write(f'Entrada inválida: {error}\n')
return
if len(tokens) != 1:
self.stdout.write('Uso: usuario NOMBRE\n')
return
nombre = tokens[0]
if not nombre.isidentifier():
self.stdout.write('Nombre inválido\n')
return
seleccionar_usuario(nombre)Una lista permitida de acciones y valores es más segura que intentar eliminar caracteres peligrosos.
Comandos desconocidos con default()
Si no existe un método do_*, se llama a default(). Úsalo para un error claro o una sugerencia.
import difflib
COMANDOS = ['estado', 'backup', 'usuario', 'salir']
def default(self, line):
nombre = line.split(maxsplit=1)[0]
opciones = difflib.get_close_matches(nombre, COMANDOS, n=1)
if opciones:
self.stdout.write(f'Comando desconocido. Quizá: {opciones[0]}\n')
else:
self.stdout.write('Comando desconocido. Escribe help.\n')No uses default() para ejecutar comandos arbitrarios del sistema. Eso convierte una herramienta restringida en un shell abierto.
Comportamiento de líneas vacías
Por defecto, emptyline() repite el último comando no vacío. Puede ser peligroso para acciones destructivas, cobros o despliegues. Sobrescribe el método para no hacer nada.
def emptyline(self):
passTambién puedes repetir solo acciones conocidas como idempotentes, pero la regla debe estar documentada y probada.
Hooks precmd() y postcmd()
precmd() recibe la línea antes del despacho y puede normalizarla, auditarla o bloquearla. postcmd() se ejecuta después y puede modificar la decisión de finalizar.
def precmd(self, line):
normalizada = line.strip()
registrar_intento(self.usuario, normalizada)
return normalizada
def postcmd(self, stop, line):
registrar_resultado(self.usuario, line, stop)
return stopNo registres contraseñas, tokens ni argumentos sensibles. Para credenciales, utiliza técnicas de lectura segura en el terminal.
preloop() y postloop()
preloop() se ejecuta una vez antes del primer prompt. Sirve para abrir conexiones, cargar configuración o comprobar permisos. postloop() debe liberar recursos.
def preloop(self):
self.conexion = conectar()
def postloop(self):
self.conexion.close()Usa context managers o ExitStack con varios recursos y trata interrupciones para que Ctrl+C no deje locks o transacciones abiertas.
Ejecutar una línea con onecmd()
onecmd() interpreta una cadena como si hubiera sido escrita por el usuario. Resulta útil en pruebas e integraciones.
consola = Consola()
resultado = consola.onecmd('estado')Normalmente no necesitas sobrescribirlo. Los hooks antes y después del comando son puntos de extensión más seguros.
Cola de comandos con cmdqueue
cmdqueue contiene líneas procesadas antes de solicitar nueva entrada. Permite scripts, macros y reproducción.
consola = Consola()
consola.cmdqueue.extend([
'estado',
'entorno staging',
'salir',
])
consola.cmdloop()Cuando las líneas provienen de un archivo, limita tamaño y cantidad, valida la ruta y trata el contenido como no confiable. Para archivos por líneas, consulta fileinput en Python.
Streams de entrada y salida
El constructor acepta stdin y stdout. Para usar un stream de entrada proporcionado, configura use_rawinput=False.
from io import StringIO
entrada = StringIO('estado\nsalir\n')
salida = StringIO()
consola = Consola(stdin=entrada, stdout=salida)
consola.use_rawinput = False
consola.cmdloop()
print(salida.getvalue())Escribe en self.stdout en lugar de utilizar siempre print() cuando necesites redirección consistente.
EOF e interrupciones
El fin de archivo se entrega como el comando especial EOF. Implementa do_EOF() para cerrar limpiamente.
def do_EOF(self, arg):
self.stdout.write('\nCerrando\n')
return TrueTrata KeyboardInterrupt en un límite apropiado y garantiza que la limpieza se ejecuta. Distingue cancelación de error de comando.
Autorización por comando
Una consola administrativa necesita autorización, no solo autenticación. Verifica el rol para cada acción crítica.
def do_limpiar_cache(self, arg):
if 'admin' not in self.permisos:
self.stdout.write('Acceso denegado\n')
return
confirmar_y_limpiar_cache()Ocultar una acción de la ayuda no es control de acceso. El usuario aún puede escribir su nombre.
Pruebas con streams en memoria
Prueba comandos válidos, desconocidos, argumentos incompletos, EOF, líneas vacías, excepciones y permisos.
def test_estado():
entrada = StringIO('estado\nEOF\n')
salida = StringIO()
consola = Consola(stdin=entrada, stdout=salida)
consola.use_rawinput = False
consola.cmdloop()
assert 'operativo' in salida.getvalue()Inyecta servicios falsos en lugar de conectar bases de datos o redes reales en pruebas unitarias.
Errores frecuentes
- Ejecutar argumentos con
shell=True. - Permitir que una línea vacía repita acciones destructivas.
- Olvidar
do_EOF(). - Ignorar
self.stdout. - Mostrar secretos en autocompletado.
- Confundir comandos ocultos con autorizados.
- Cargar archivos de comandos sin límites.
Buenas prácticas
- Mantén pequeños los métodos
do_*. - Delega la lógica a servicios.
- Valida argumentos explícitamente.
- Ofrece ayuda y ejemplos consistentes.
- Audita sin datos sensibles.
- Prueba con streams en memoria.
- Libera recursos en
postloop().
Conclusión
cmd en Python reduce el código repetitivo de las consolas interactivas. Incluye despacho, ayuda, historial, autocompletado, hooks de ciclo de vida, streams y colas.
La seguridad depende de tu diseño: valida datos, autoriza acciones, evita shells y desconfía de archivos de comandos. Consulta la documentación oficial de cmd y la documentación de readline.







