− Ah ouais, comme pour IPv4 et IPv6, t’as zappé le 3 ?

− Ta gueule…

Récemment, je me suis rendu compte que certains des logiciels que j’avais développés par le passé, en Rust donc, utilisait une « assez vieille » version de la librairie clap, la v2 pour être précis.

En fait, j’ai complètement zappé la v3, parce qu’elle me semblait à ce moment nettement moins optimale, beaucoup plus brute, imposait tout un tas de choix discutable comme le fait de passer obligatoirement par des macros plutôt que d’avoir un moyen « programmatique » d’appeler la librairie.

De ce point de vue, la v4 correspond bien plus à ce que j’attends d’un programme de ce type : le choix entre macros ou pas, la versatilité des options, les cas tordus qu’on n’a pas forcément vus au départ.

Voici un (petit) guide pour aider à passer de clapv2 à clapv4.

Command::new() remplace App:new()

Voilà, la première des choses à faire, c’est d’invoquer correctement clapv4. Pour cela, on utilise la fonction Command::new() que l’on doit invoquer à l’import de clap.

App n’existe plus du tout, même si ce n’est pas la première erreur que tu vas rencontrer a priori.

Arg::with_name n’existe plus

Autre chose dont il va falloir se débarasser, Arg::with_name n’existe plus, il est remplacé par Arg::new(). On peut ainsi facilement remplacer ce bout de code :

App::new()
    .arg(Arg::with_name("host")
         .short("H")
         .long("host")
         .value_name("HOST"));

Par :

Command::new()
    .arg(Arg::new("host")
         .short("H")
         .long("host")
         .value_name("HOST"));

Plus simple, plus lisible.

takes_value() n’existe plus non plus

En fait takes_value() avait un peu un statut à la con à la l’origine : il permettait de spécifier qu’un argument devait prendre une valeur, mais il fallait une seconde fonction pour préciser le nombre de valeurs s’il y en avait plus d’une. C’est maintenant régler sur clapv4 où l’on utilise systématiquement la fonction num_args(). Il suffit de l’invoquer avec la valeur 1 pour retrouver le comportement d’origine et l’on verra un peu plus bas que ça a des conséquences assez positives sur le reste du code.

short() prend maintenant un caractère et non une chaîne

Voilà un changement qui est le bienvenue : on ne peut plus écrire short("b"), il faut obligatoirement écrire short('b'). Et oui, en Rust, les chaînes de caractères sont entre double quotes alors que les caractères sont obligatoirement entre single quotes (un truc qu’on a beaucoup de mal à faire comprendre au départ à des gens qui font du PHP ou du Python d’ailleurs…).

Dites aussi au revoir à value_of()

Bon alors là, je ne peux qu’applaudir le progrès aussi. Au lieu d’utiliser value_of() et de décapsuler l’éventuel résultat, il va maintenant s’agir d’utiliser get_one() (ou son pendant get_many()) pour récupérer les valeurs que l’on souhaite. La (très) bonne nouvelle, c’est que get_one() permet de directement spécifier le type qu’on souhaite récupérer à la fin.

On peut donc parfaitement écrire :

let my_port = matches.get_one::<String>("host").unwrap_or("default");

Plutôt que :

let my_port = matches.value_of("host").unwrap().to_string();

Je trouve ça bien plus élégant dans le principe et même dans la pratique, dans la mesure où le cast est fait directement par la fonction et non en aval de celle-ci. On récupère donc bien plus directement les valeurs qui nous intéressent (et encore une fois, si vous n’aimez pas l’opérateur turbofish, vous pouvez toujours venir me lécher le cul…).

Attention, toutefois ! Vous ne récupérez pas la valeur directement mais un emprunt à cette valeur. Méfiez-vous donc pour les String par exemple. Ça veut aussi dire qu’il faudra de temps en temps se servir de l’opérateur *, ce qui est relativement inhabituel en Rust.

Les flags doivent maintenant être utilisés systématiquement

Avant pour préciser un flag (comprendre une valeur optionnelle sur la ligne de commande), il fallait préciser un argument sans takes_value() ou avec takes_value(false). Désormais, il faut transformer cette argument en flag (un drapeau donc) avec une valeur par défaut (typiquement si faux, si vrai, etc…).

On passe donc de :

let matches = App::new()
    .arg(
        Arg::with_name("starttls")
        .short("t")
        .long("starttls"));

let starttls = matches.is_present("starttls");

À :

let matches = Command::new()
    .arg(Arg::new("starttls")
        .short('t')
        .long("starttls")
        .action(ArgAction::SetTrue));

let starttls = matches.get_flag("starttls");

Voilà, encore une fois un peu plus lourd à l’instantiation, mais du coup bien plus « explicite » quand on lit juste la partie définition les arguments.

Précisez les valeurs par défaut directement dans les arguments

Toujours dans le même état d’esprit, vous pouvez maintenant préciser une valeur par défaut (toujours sous la forme d’une chaîne de caractères) directement dans les arguments. Donc au lieu de tenter des unwrap_or() un peu au pif dans le code, on peut directement préciser une valeur dans l’argument pris.

C’est, encore une fois, un changement qui va dans le bon sens, d’autant plus qu’il est repris dans l’aide de la commande qu’on tape (entre crochet à côté de la valeur). Donc une très bonne option pour rendre les choses bien plus claires.

En gros, on peut maintenant faire :

let matches = Command::New()
        .arg(Arg::new("port")
        .short('p')
        .long("port")
        .num_args(1)
        .default_value("993"));

let port = matches.get_one::<u16>("port").unwrap(); // va contenir 993 quoiqu’il arrive

Sous-commandes

Et pour finir dans ce petit tour de clapv4, les subcommand (qui exigeaint à l’origine leur propres structures) sont maintenant simplement des Command::new(). Il n’y a donc pas besoin d’avoir une commande spécifique pour les sous-commandes, on peut directement faire des commandes dans des commandes, en continuant d’utiliser subcommand.

Seul petit piège, subcommand (celui du matches, pas l’autre) renvoie maintenant un tuple dans une Option au lieu de renvoyer une Option dans un tuple. C’est un changement assez minime dans l’absolu, mais ça va forcément foutre en l’air ta syntaxe d’origine, c’est balot…

Conclusation

Voilà, c’était un petit retour sur le passage de v2 à v4 de clap. clap, c’est vraiment la librairie un peu indispensable dès qu’on veut mettre des options dans un programme Rust. Essayer de s’en passer, c’est un peu comme tenter de faire le Tour de France avec les petites roues en ayant mangé une choucroute avant de se lancer : y’a moyen que tu te gamèles au premier virage avec pertes et fracas.

clap, c’est bon, mangez-en !