24 mayo 2023

Cómo parsear los parámetros de entrada de tu aplicación de consola en Rust usando Clap

En un artículo anterior, expliqué  cómo usar el módulo ArgParse module para leer los argumentos de consola de las aplicaciones desarrolladas en Python. Rust cuenta con crates especializados en el parseo de argumentos de consola. En este artículo vamos a ver cómo usar uno de esos crates: clap.

Una de las cosas maravillosas de Rust es que dentro de ese caparazón rustaceo, duro y árpero hay un corazón pythonista. Al usar clap encontrarás muchas de las funcionalidades de ArgParse. En realidad, los conceptos entre ambos son muy similares: mientras que los verbos recibían el nombre de subparser en ArgParse, en clap reciben el nombre de subcomandos, mientras que ArgParse llama argumentos a los que clap denomina simplemente arg. Por lo tanto, si estás acostumbrado a usar ArgParser, lo más probable es que te sientas en casa usando clap. Por todo lo anterior, voy a asumir que ya has leído mi artículo anterior sobre ArgParse y no voy a repetirme volviendo a explicar los mismos conceptos.

Como cualquier otro crate de Rust, para usar clap hay que incluirlo primero en el fichero Cargo.toml:

Después, ya podrás usar clap en tu código fuente. Para ilustrar las explicaciones, voy a usar como ejemplo el parseo que se realiza en mi proyecto cifra-rust

Como puedes ver allí, puedes usar clap directamente en tu función main(), pero yo prefiero abstraer ese proceso en una función aparte que retorne un struct Configuration específicamente desarrollado por mí. De esa manera, si en el futuro decidiese cambiar y pasar a utilizar otro crate de parseo de argumentos, el proceso sería mucho más fácil al haber reducido el acoplamiento. Por eso, tal y como también hago en Python, he definido una función llamada parse_arguments() que retorna un tipo Configuration. 

En el código se puede ver que el parser raíz se define en clap usando App::new(). Como en otros crates de Rust, clap hace un uso muy intenso del patrón constructor para configurar su parseo. De esa manera, puedes configurar la versión del comando, su autor y la descripción larga ("long_about"), entre otras opciones, tal y como se puede ver entre las líneas 277 y 280:


El comportamiento de clap se puede personalizar usando el método setting(). Un parámetro típico con el que llamar a ese método es AppSettings::ArgRequiredElseHelp, para mostrar la ayuda si se llama al comando sin argumento alguno:


Un subparser se crea llamando al método subcommand() y pasándole una nueva instancia de App:


Usando el método about() se puede definir una descripción corta, sobre el comando o sus argumentos, la cual aparecerá cuando se llame al comando principal con la opción --help.

Lo normal es que tanto el parser, como sus subparser, requieran argumentos. Los argumentos se definen a través del método arg(), en el parser al que pertenezca. Aquí tienes un ejemplo:


En el último ejemplo se puede ver que el subparser create (línea 285) tiene dos argumentos: dictionary_name (línea 287) e initial_words_file (línea 292). Fíjate que todas las llamadas a arg() usan como argumento una instancia de Arg, mientras que la configuración del argumento se hace usando el patrón constructor sobre las instancias de Arg.

El argumento dictionary_name es necesario porque está configurado como required(true) en la línea 288. Ten en cuenta que aunque aquí se utilice un flag, en general se suele recomendar que no se haga. Por definición, todos los argumentos obligatorios deberían ser posicionales (es decir, no deberían usar un flag), el único caso en el que el uso de un flag está bien visto con un argumento obligatorio es cuando ese argumento llama a una operación destructiva y pedirle el flag al usuario es una manera de asegurar que el usuario sabe lo que hace al tener que tomarse ese trabajo extra. Cuando se usa un argumento posicional, se puede usar el método index() para especificar la posición de ese argumento en relación a otros. En realidad, después me he enterado de que puedes dejar el método index() y en ese caso el índice se asignará según el orden de evaluación dentro del código. Precisamente lo que te permite index() e poder saltarte ese orden que sale de la evaluación del código.

Cuando se usa takes_value(true) oen un argumento, el valor facilitado desde consola se guarda en una clave llamada como el argumento. Por ejemplo, el  takes_value(true) de la línea 290 hace que el valor facilitado por el usuario se guarde en una clave llamada dictionary_name. Si estás usando un flag opcional sin valor (es decir un flag booleano), puedes usar takes_value(false) o sencillamente omitirlo.

La llamada al método value_name() es equivalente al parámetro metavar del ArgParse de Python. Te permite definir la cadena que representará al parámetro cuando se llame a la ayuda con el parámetro --help.

Lo raro es que los argumentos no usan el método about() para definir sus cadenas de ayuda, sino el método help().

Puedes ver cómo se define un argumento opcional de la línea 292 a la 298. Allí, la versión larga de flag se define con el método long() y la corta con el método short(). En este ejemplo, el argumento puede llamarse tanto como "--initial_words_file <PATH_TO_FILE_WITH_WORDS>" como "-i <PATH_TO_FILE_WITH_WORDS>".

Puede resultarte útil llamar al método validator(), ya que define una función que se aplicará sobre los argumentos facilitados para asegurar que cumplen las condiciones de lo que se espera recibir. La función que se utilice como validador deberá ser capaz de recibir un argumento string y deberá retornar un Result<(), String>. Si haces tus comprobaciones y encuentras correcto el argumento entonces debes retornar un Ok(()) o, en caso contario, un Err("Here you write your error message").

Encadenando métodos en argumentos anidados y en subcommandos puedes definir un árbol entero de comandos. Una vez que hayas acabado tienes que hacer una última llamada al método get_matches_from() del parser raíz. Para ello, debes pasarle al método un vector de con los strings de cada uno de los argumentos del comando:



Normalmente le pasarás el vector de strings que devuelve la función args(), el cual retorna un vector con cada argumento dado por el usuario al llamar a la aplicación desde la consola: 



Fíjate en que mi función main() está casi vacía. Eso es porque de esa manera puedo llamar a _main() (fíjate en el guión bajo) desde mis tests de integración, metiendo mi propio vector de argumentos para simular a un usuario llamando a la aplicación desde la consola.

los que get_matches_from() devuelve es un tipo ArgMatches. Ese tipo devuelve el valor de cada argumento si se le especifica el nombre de una clave. De las líneas 149 a 245 he implementado un método para crear un tipo Configuration usando el contenido del ArgMatches. 

Ten en cuenta que cuando sólo tienes un parser puedes obtener los valores usando el método value_of(). Pero si tienes subcomandos perimero tienes que conseguir el ArgMatches específico para esa rama de subcomando, haciendo una llamada al método subcommand_matches():


En el último ejemplo puede ver la manera de trabajar normal:
  • Profundizas obteniendo el ArgMatches de la rama en la que estás interesado. (líneas 160-161)
  • Una vez que tienes el ArgMatches que quieres, utilizas value_of() para conseguir el valor de un argumento concreto. (línea 164)
  • Para parámetros opcionales, puedes usar el método is_present() para comprobar si se facilitó el parámetro o no. (línea 165). 
Mediante estos métodos, puedes recuperar los valores de los argumentos facilitados por el usuario y construir la configuración necesaria para ejecutar tu aplicación.

Como puedes ver, puedes hacer un parser realmente potente con clap, con la misma funcionalidad que ofrecía ArgParse en Python, pero en este caso en un entorno de desarrollo con Rust.