Files
BaC/content/2022-12-09-Rust-:-passer-de-clapv2-à-clapv4.md
2025-02-27 12:52:48 +01:00

139 lines
7.9 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

+++
title = "Rust: passer de clapv2 à clapv4"
date = 2022-12-09
[taxonomies]
tags = [ "rust", "clap" ]
+++
> Ah ouais, comme pour IPv4 et IPv6, tas zappé le 3?
>
> Ta gueule…
<!-- more -->
Récemment, je me suis rendu compte que certains des logiciels que javais développés par le passé, en Rust donc, utilisait une «assez vieille» version de la librairie [clap](https://epage.github.io/blog/), la v2 pour être précis.
En fait, jai complètement zappé la v3, parce quelle 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 davoir un moyen «programmatique» dappeler la librairie.
De ce point de vue, la v4 correspond bien plus à ce que jattends dun programme de ce type: le choix entre macros ou pas, la versatilité des options, les cas tordus quon na 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, cest dinvoquer correctement clapv4. Pour cela, on utilise la fonction [`Command::new()`](https://docs.rs/clap/4.0.29/clap/builder/struct.Command.html#method.new) que lon doit invoquer à limport de `clap`.
`App` nexiste plus du tout, même si ce nest pas la première erreur que tu vas rencontrer a priori.
# `Arg::with_name` nexiste plus
Autre chose dont il va falloir se débarasser, `Arg::with_name` nexiste plus, il est remplacé par [`Arg::new()`](https://docs.rs/clap/4.0.29/clap/builder/struct.Arg.html#method.new). On peut ainsi facilement remplacer ce bout de code:
```rust
App::new()
.arg(Arg::with_name("host")
.short("H")
.long("host")
.value_name("HOST"));
```
Par:
```rust
Command::new()
.arg(Arg::new("host")
.short("H")
.long("host")
.value_name("HOST"));
```
Plus simple, plus lisible.
# `takes_value()` nexiste plus non plus
En fait `takes_value()` avait un peu un statut à la con à la lorigine: il permettait de spécifier quun argument devait prendre une valeur, mais il fallait une seconde fonction pour préciser le nombre de valeurs sil y en avait plus dune. Cest maintenant régler sur clapv4 où lon utilise systématiquement la fonction [`num_args()`](https://docs.rs/clap/4.0.29/clap/builder/struct.Arg.html#method.num_args). Il suffit de linvoquer avec la valeur `1` pour retrouver le comportement dorigine et lon 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 quon a beaucoup de mal à faire comprendre au départ à des gens qui font du PHP ou du Python dailleurs…).
# Dites aussi au revoir à `value_of()`
Bon alors là, je ne peux quapplaudir le progrès aussi. Au lieu dutiliser `value_of()` et de décapsuler léventuel résultat, il va maintenant sagir dutiliser [`get_one()`](https://docs.rs/clap/4.0.29/clap/parser/struct.ArgMatches.html#method.get_one) (ou son pendant [`get_many()`](https://docs.rs/clap/4.0.29/clap/parser/struct.ArgMatches.html#method.get_many)) pour récupérer les valeurs que lon souhaite. La (très) bonne nouvelle, cest que [`get_one()`](https://docs.rs/clap/4.0.29/clap/parser/struct.ArgMatches.html#method.get_one) permet de directement spécifier le type quon souhaite récupérer à la fin.
On peut donc parfaitement écrire:
```rust
let my_port = matches.get_one::<String>("host").unwrap_or("default");
```
Plutôt que:
```rust
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 naimez pas lopé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 quil faudra de temps en temps se servir de lopé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:
```rust
let matches = App::new()
.arg(
Arg::with_name("starttls")
.short("t")
.long("starttls"));
let starttls = matches.is_present("starttls");
```
À :
```rust
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 à linstantiation, 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 desprit, vous pouvez maintenant préciser une valeur par défaut (toujours sous la forme dune chaîne de caractères) directement dans les arguments. Donc au lieu de tenter des [`unwrap_or()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.unwrap_or) un peu au pif dans le code, on peut directement préciser une valeur dans largument pris.
Cest, encore une fois, un changement qui va dans le bon sens, dautant plus quil est repris dans laide de la commande quon 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:
```rust
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 quoiquil arrive
```
# Sous-commandes
Et pour finir dans ce petit tour de clapv4, les `subcommand` (qui exigeaint à lorigine leur propres structures) sont maintenant simplement des [`Command::new()`](https://docs.rs/clap/4.0.29/clap/builder/struct.Command.html#method.new). Il ny a donc pas besoin davoir une commande spécifique pour les sous-commandes, on peut directement faire des commandes dans des commandes, en continuant dutiliser [`subcommand`](https://docs.rs/clap/4.0.29/clap/builder/struct.Command.html#method.subcommand).
Seul petit piège, [`subcommand` (celui du matches, pas lautre)](https://docs.rs/clap/4.0.29/clap/parser/struct.ArgMatches.html#method.subcommand) renvoie maintenant un tuple dans une Option au lieu de renvoyer une Option dans un tuple. Cest un changement assez minime dans labsolu, mais ça va forcément foutre en lair ta syntaxe dorigine, cest balot…
# Conclusation
Voilà, cétait un petit retour sur le passage de v2 à v4 de `clap`. `clap`, cest vraiment la librairie un peu indispensable dès quon veut mettre des options dans un programme Rust. Essayer de sen passer, cest un peu comme tenter de faire le Tour de France avec les petites roues en ayant mangé une choucroute avant de se lancer: ya moyen que tu te gamèles au premier virage avec pertes et fracas.
`clap`, cest bon, mangez-en!