Crea un CLI con Python



@eduardo_gpg

Número de visitas 5215

Tiempo de lectura 7 min

15 Septiembre 2020

En una entrega anterior aprendimos a crear nuestro primer CLI con Python (Command Line Interface). Todo funcionó perfectamente, nos apoyamos tanto del módulo sys, como de la librería argparse. En la mayoría de las ocasiones, para proyectos pequeños, ambas herramientas serán más que suficiente, sin embargo ¿Qué pasa si en dado caso nuestro proyecto necesita de múltiples comandos, argumentos, banderas, y por qué no, inclusive sub-comandos ? Pues bien, en esos caso, en proyecto mucho más complejos, utilizar sys o argparse ya no suena a una muy buena idea,🧐 principalmente por la cantidad de validaciones y líneas de código que tendremos que implementar. Ojo, no estoy diciendo que no es posible hacerlo, solo que nuestro código puede llegar a ser difícil de leer y por supuesto, difícil de mantener; algo que sin duda nadie quiere.

Para esos casos, en los cuales necesitamos de un CLI complejo y robusto, les recomiendo ampliamente la librería de click. A través de ella seremos capaces de crear CLI`s utilizando decoradores. Veamos.

Click

Lo primero que debemos hacer será instalar la librería de click.

pip install click

Una vez hecho esto ya podremos utilizarla.

Comencemos con algo sencillo, que te parece si creamos un comando que nos permita saludar a nuestros usuarios. Nuestro script pudiera quedar de la siguiente manera.

import click

@click.command()
def main():
    print('Hola mundo desde PyWombat')

if __name__ == '__main__':
    main()

Para este ejemplo se hace el llamado a la función main cuando el script es ejecutado. Esta función se encuentra decorada por @command, indicando que la función pasa a ser un comando.

Si ejecutamos el script obtendremos nuestro mensaje en consola.

$ python main.py
Hola mundo desde PyWombat

Hasta aquí nada nuevo, y de hecho podemos pensar ¿Para qué decoramos la función? bien, vayamos paso a paso, compliquemos un poco más las cosas, y para ello ahora nuestro comando saludará a un usuario.

import click

@click.command()
@click.argument('name')
def main(name):
    print(f'Hola {name}')

if __name__ == '__main__':
    main()

En esta ocasión nos apoyamos de un segundo decorador, @argument. Este decorador le indica al comando que ahora, de forma obligatoria, debe recibir un argumento, es decir un valor de entrada.

Al nosotros decorar nuestra función con dicho decorador, esta debe poseer un parámetro con el mismo nombre del argumento.

Para nosotros ejecutar nuestro programa será necesario indicar un valor para el argumento name.

$ python main.py Eduardo
Hola Eduardo

Si en caso nuestro comando necesita una n cantidad de argumentos, basta con decorar un n cantidad de veces.

import click

@click.command()
@click.argument('name')
@click.argument('last_name')
@click.argument('email')
def main(name, last_name, email):
    print(f'Hola {name} {last_name}, cuyo correo es:: {email}')

if __name__ == '__main__':
    main()

Ejecutamos.

$ python main.py Eduardo García eduardo78d@gmail.com
Hola Eduardo García, cuyo correo es:: eduardo78d@gmail.com

Si ejecutamos y no colocamos ningún valor para algún argumento, obtendremos un error.

$ python main.py Eduardo García
Usage: main.py [OPTIONS] NAME LAST_NAME EMAIL
Try "main.py --help" for help.

Error: Missing argument "EMAIL".

Para obtener mayor información de nuestro CLI podemos utilizar el --help

$python main.py --help
Usage: main.py [OPTIONS] NAME LAST_NAME EMAIL

Options:
  --help  Show this message and exit.

A considerar

Cuando nos encontramos desarrollando un CLI con click debemos tener considerar 4 puntos claves: Comandos, Argumentos, Opciones y Banderas. Aquí definición muy puntual de cada una de ellas.

  • Comandos: Acciones a realizar. Serán las funciones decoradas por @command.
  • Argumentos: Valores de entrada obligatorios para el comando. Se definen mediante el decorador @argument.
  • Opciones: Valores de entrada opcionales para el comando. Se definen mediante el decorador _@option.
  • Banderas: Valores de entrada opcionales de tipo booleano para el comando. Se definen mediante el decorador _@option.

Hasta el momento hemos visto 2 de ellos (Comandos y argumentos). Así que compliquemos un poco más nuestro ejemplo. Definamos un comando el cual nos permita crear un usuario en el sistema. Para ello crearemos nuestra clase User.

class User():
    def __init__(self, name, password, email, active):
        self.name = name
        self.password = password
        self.email = email
        self.active = active

    def __str__(self):
        return self.name + ' ' + self.password + ' ' +  self.email + ' ' + str(self.active)

Una clase bastante sencilla.

Nuestro comando quedaría de la siguiente manera.

@click.command()
@click.argument('name')
@click.argument('password')
@click.option('--email', '-e', default='')
@click.option('--active', '-a', is_flag=True, default=True)
def main(name, password, email, active):

    user = User(name, password, email, active)
    print(user)

Aquí hay varias cosas que debemos tener en cuenta.

1.- Nuestro comando ahora es mucho más complejo. Recibe y establece los valores necesarios para poder crear un nuevo usuario. 😬

2.- Definimos los valores opcionales utilizando el decorador @option. Por convención dichos valores deberán comenzar con el prefijo doble guión (--). El parámetro para nuestra función tendrá el mismo nombre que la opción (Se elimina el doble guión). Ejemplo: --email opción, email parámetro.

3.- Si deseamos, podemos definir una abreviatura para nuestras opciones. Por convención dichas abreviaturas deberán comenzar con el prefijo guión (-) y serán la primera letra de nuestra opción. Ejemplos:

  • --email abreviatura: -e
  • --active abreviatura: -a

5.- A nuestras opciones podremos definir los valores por default, esto mediante el parámetro default.

6.- Para definir una bandera haremos uso del decorado @option, colocando True para en el parámetro is_flag. Si la bandera es llamada se negará el valor por default.

7.- La función deberá poseer la misma cantidad de parámetros, como la cantidad de argumentos y opciones que posea el comando.

Ok, mucha información de golpe, así que mejor veamos el funcionamiento para comprenderlo de mejor forma.

  • Ejecutamos definiendo los valores para los argumentos (Nombre y Password).
$ python main.py Eduardo Password
Eduardo Password  True
  • Ejecutamos definiendo los valores para los argumentos (Nombre y Password) y un valor opcional (Email).
$ python main.py Eduardo Password --email eduardo@example.com
Eduardo Password eduardo@example.com True
  • Ejecutamos definiendo los valores para los argumentos (Nombre y Password) y los valores opcional (Email y Active).
$ python main.py Eduardo Password --email eduardo@example.com -a
Eduardo Password eduardo@example.com False
  • Ejecutamos definiendo los valores opcionales (Email y Active) y los valores para los argumentos (Nombre y Password) (El orden no influye) 🥳.
$ python main.py -e eduardo@example.com -a Eduardo Password
Eduardo Password eduardo@example.com False

Listo, cómo podemos observar nuestro comando se comportar tal y como lo hemos definido. El comando de forma obligatoria debe recibir dos valores de entrada: Nombre y Password (En ese orden), opcionalmente podrá recibir un valor para el correo electrónico y podrá negar su valor por default en active.

Si nosotros intentáramos replicar lo mismo con sys o argparse, creanme, nos tomaría mucho más líneas de código. 😥

Promt

Algo que sin duda me encanta de click es la facilidad con la cual podremos pedirle al usuarios ingrese valores a través del teclado. Lograremos estos utilizando un prompt. Veamos.

Lo que haremos ahora será modificar un poco nuestro comando, pediremos al usuario ingrese, vía teclado y mediante un prompt, los valores para el nombre y el password del usuario.

@click.command()
@click.option('--name', prompt='Ingresa el nombre del usuario')
@click.option('--password', '-p', prompt='Ingresa la contraseña del usuario', hide_input=False)
@click.option('--email', '-e', default='')
@click.option('--active', '-a', is_flag=True, default=True)
def main(name, password, email, active):

    user = User(name, password, email, active)

Para implementar un prompt será necesario dejar a un lado el decorador @argument y utilizar @option. Mediante el parámetro prompt indicamos el mensaje que el usuario visualizará en consola.

Sin banderas.

$ python main.py
Ingresa el nombre del usuario: Eduardo
Ingresa la contraseña del usuario: Password
Eduardo Password  True

Con banderas.

$ python main.py -e eduardo@example.com -a
Ingresa el nombre del usuario: Eduardo
Ingresa la contraseña del usuario: Password
Eduardo Password eduardo@example.com False

Mucho mejor 🤓, solo que, siguiendo con el ejemplo, pedir al usuario ingrese su contraseña a la vista de todos, no suena a una muy buena idea. 😖Pero no nos desanimemos, click tiene la solución: hide_input=True.

Con el parámetro hide_input seremos capaces de indicar si queremos que, lo que el usuario ingreso vía teclado se visualize en pantalla o no.

@click.option('--password', '-p', prompt='Ingresa la contraseña del usuario', hide_input=True)

La contraseña ahora no se visualiza.

$ python main.py -e eduardo@example.com -a
Ingresa el nombre del usuario: Eduardo
Ingresa la contraseña del usuario: 
Eduardo Password eduardo@example.com False

Cool, ya tenemos un funcionamiento mucho mucho mejor que el que teníamos cuando comenzamos.

Sub comandos

Ok, ya tenemos nuestro comando principal con sus respectivos argumentos, opciones y banderas, pero ¿ y qué pasa si queremos realizar otras acciones? ¿Quizás consultar usuarios, actualizarlos o eliminarlos? Pues bien, en esos caso tendremos que delegar funcionalidades en sub comandos.

Veamos.

@click.group()
def main():
    pass

@main.command()
@click.option('--name', prompt='Ingresa el nombre del usuario')
@click.option('--password', '-p', prompt='Ingresa la contraseña del usuario', hide_input=True)
@click.option('--email', '-e', default='')
@click.option('--active', '-a', is_flag=True, default=True)
def create_user(name, password, email, active):

    user = User(name, password, email, active)
    print(user)

if __name__ == '__main__':
    main()

Para nosotros poder crear sub comandos lo primero que debemos hacer será agruparlos bajo un contexto, para ello creamos un nuevo grupo. Nuestra función main será el grupo principal, y todas aquellas funciones que sean decoradas por ella serán los sub comandos (Cómo lo es la función create_user).

Si en dado caso nuestra función posee uno o más guiones bajos (_), el comando los reemplazará por guiones (-). Ejemplo:

# Función  -> Comando
create_user -> create-user
update_user -> update-user
delete_user -> delete-user

Para este ejemplo al trabajar con un sub comando, la forma de ejecutar el script cambiará ligeramente. Ahora antes de colocar cualquier argumento, opción o bandera será necesario indicar el sub comando a ejecutar.

python main.py create-user -e eduardo@example.com -a
Ingresa el nombre del usuario: Eduardo
Ingresa la contraseña del usuario: 
Eduardo Password123 eduardo@example.com False

A partir de la función main podremos definir la n cantidad de sub comandos que deseemos.

@click.group()
def main():
    pass

@main.command()
def list_users():
    print('Listamos usuarios')

@main.command()
def update_user():
    print('Actualizamos un usuario')

@main.command()
def delete_user():
    print('Eliminamos un usuario')

@main.command()
@click.option('--name', prompt='Ingresa el nombre del usuario')
@click.option('--password', '-p', prompt='Ingresa la contraseña del usuario', hide_input=True)
@click.option('--email', '-e', default='')
@click.option('--active', '-a', is_flag=True, default=True)
def create_user(name, password, email, active):

    user = User(name, password, email, active)
    print(user)

if __name__ == '__main__':
    main()

Si queremos conocer el listado de sub comandos y sus correspondientes argumentos, opciones y banderas podemos utilizar el --help.

python example.py --help
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  create-user
  delete-user
  list-users
  update-user

Llamado a los sub comandos

$ python main.py list-users
Listamos usuarios

$ python main.py update-user
'Actualizamos un usuario

$ python main.py delete-user
Eliminamos un usuario

Conclusión

Perfecto, ya conocemos otra forma de poder crear un CLI con Python. En esta ocasión nos apoyamos de la librería click, la cual como pudimos observar es sumamente poderosa y sobre todo, fácil de implementar. Mediante decoradores seremos capaces de convertir nuestras funciones en comandos de terminal. Los comandos pueden ser tan sencillos o complejos como nosotros deseemos.

Para un mejor funcionamiento, un comando puede recibir una n cantidad de argumentos, así mismo poseer una n cantidad de opciones y banderas, valores que no harán más que modificar el comportamiento de dicho comando.

Si el día de hoy tuvieras que crear un CLI ya tendrás 3 opciones de las cuales elegir: Módulo sys, librería argparse o la librería click, siendo esta última, desde mi opinión, la mejor opción. 🐍


¿El contenido te resulto de ayuda?

Para poder dejar tu opinión es necesario ser un usuario autenticado. Login

Juan_Moya

Como puedo cambiar la palabra de ejecución, en este cado para correr el CLI siempre tenemos que escribir "python", yo quiero que sea otra por ejemplo como lo hace docker o AWS CLI.

Más Tips y Ejercicios 🐍

Adquiere una subscripción PyWombat por tan solo $3 USD. al mes.

Conoce los beneficios de ser usuario premium:
Niveles desbloqueados: Ten accesos a todos los niveles de ejercicios. 🔓
Nuevo límite: Incrementa tu límite de ejercicios por semana. 🚀
Contenido único: Recibe semanalmente recursos exclusivos de Python (Videos, Artículos y Capitulos del libro PyWombat, comienza como desarrollador Python. 🐍