Interface console
Motivation
Si vous avez déjà écrit des programmes qui acceptent des arguments de l’utilisateur en ligne de commande, vous savez que c’est un travail de longue haleine :
- Il faut gérer mille variantes de la même syntaxe (ex:
-xyz
vs-x -y -z
,--param=valeur
vs--param valeur
). - Dès qu’on essaie d’écrire une commande à la
perf
oudocker run
qui retransmet une partie des arguments un autre programme, les ambiguités de parsing par rapport à nos propres arguments arrivent très vite. Même au sein d’un même programme, elles peuvent être un problème, pensez par exemple à l’ambiguité entre noms de branches et de fichiers dansgit
. - On doit être prêt à ce que l’utilisateur écrive absolument n’importe quoi, y compris de l’Unicode invalide là où un entier est attendu. Sur des petits utilitaires, le code dédié à la gestion de ces erreurs de syntaxe et la production de messages d’erreurs propres pour l’utilisateur peut très facilement prendre plus de place que le reste du programme.
- Maintenir un texte
--help
et sa version raccourcie-h
en cohérence avec le reste du code au fur et à mesure de son évolution demande une vigilance de tous les instants. - Et je ne vous parle pas de l’autocomplétion, qui nécessite l’écriture de code shell spécifique à chaque shell que vos utilisateurs pourraient vouloir utiliser.
En C et en C++, si vous ne tenez pas à la portabilité vers les OS exotiques, le
getopt()
de POSIX est un outil trop peu exploité qui peut faire une partie du
travail pour vous.
Mais même après le parsing de getopt()
il en reste encore bien trop à faire
pour obtenir une interface en ligne de commande ergonomique et en cohérence
avec les conventions des utilitaires Unix usuels, par rapport à l’intérêt que
le programmeur moyen a pour cette tâche.
C’est pourquoi l’écosystème Rust a produit des solutions très puissantes à ce
problème, qui encapsulent l’essentiel de la complexité dans des bibliothèque
générique pouvant être partagée par un grand nombre de programmes. La plus
connue d’entre elles est clap
.
Utilisation de clap
On commence par ajouter clap
en dépendance à son projet avec cargo add
, en
activant l’excellent support optionnel de derive()
:
cargo add clap --features derive
Puis on définit une structure de données décrivant les arguments que nous attendons en entrée, avec des types standard du langages (on peut aussi ajouter le support de ses propres types moyennant un peu plus de travail) :
#![allow(unused)] fn main() { struct Arguments { speed: f32, iterations: u32, } }
On documente les différents membres ainsi que la structure. La documentation de
la structure servira d’introduction au test --help
:
#![allow(unused)] fn main() { /// Gray-Scott reaction computation /// /// This program simulates the Gray-Scott reaction, a sort of Game of Life for /// chemists. It was developed for the "Gray-Scott Battle" training. struct Arguments { /// Reaction speed /// /// If you tune this higher, the reaction goes faster. speed: f32, /// Number of iterations /// /// The higher you tune this, the longer the reaction will be simulated. iterations: u32, } }
Et enfin, on demande au compilateur de dériver le trait clap::Parser
, qui a
comme prérequis le trait Debug
, en ajoutant quelques annotations pour
clarifier ce qu’on veut que clap génère automatiquement (forme courte des
arguments, etc) :
use clap::Parser;
/// Gray-Scott reaction computation
///
/// This program simulates the Gray-Scott reaction, a sort of Game of Life for
/// chemists. It was developed for the "Gray-Scott Battle" training.
#[derive(Debug, Parser)]
#[command(author, version, about, long_about)]
struct Args {
/// Reaction speed
///
/// If you tune this higher, the reaction goes faster.
#[arg(short, long, default_value_t = 1.0)]
speed: f32,
/// Number of iterations
///
/// The higher you tune this, the longer the reaction will be simulated.
iterations: u32,
}
Ensuite, plus qu’à déclencher le parsing des arguments dans notre fonction
main()
…
use clap::Parser;
/// Gray-Scott reaction computation
///
/// This program simulates the Gray-Scott reaction, a sort of Game of Life for
/// chemists. It was developed for the "Gray-Scott Battle" training.
#[derive(Debug, Parser)]
#[command(author, version, about, long_about)]
struct Args {
/// Reaction speed
///
/// If you tune this higher, the reaction goes faster.
#[arg(short, long, default_value_t = 1.0)]
speed: f32,
/// Number of iterations
///
/// The higher you tune this, the longer the reaction will be simulated.
iterations: u32,
}
fn main() {
let args = Args::parse();
println!("{args:?}");
}
…et en ayant fait juste ça, on a une interface en ligne de commande complète avec :
- Des textes
--help
et-h
automatiquement maintenus en cohérence avec la définition des arguments au niveau de la struct :cargo run -- -h
Gray-Scott reaction computation Usage: grayscott [OPTIONS] <ITERATIONS> Arguments: <ITERATIONS> Number of iterations Options: -s, --speed <SPEED> Reaction speed [default: 1] -h, --help Print help (see more with '--help') -V, --version Print version
- Un parsing automatique des arguments vers le type désiré, avec des valeurs
par défaut :
cargo run -- 5
Args { speed: 1.0, iterations: 5 }
- Une vérification des erreurs de saisie de l’utilisateur, avec des messages
d’erreurs automatiques quand la saisie est mauvaise, par exemple si l’argument
positionnel obligatoire
iterations
est oublié ici :cargo run
error: the following required arguments were not provided: <ITERATIONS> Usage: grayscott <ITERATIONS> For more information, try '--help'.
- Un accès facile à des fonctionnalités plus avancées telles que…
- La possibilité de passer aussi les arguments par variable d’environnement.
- Le chargement automatique des valeurs d’arguments depuis un fichier.
- La génération de pages de man.
- La génération de scripts shells d’autocomplétion.
Conclusion
On comprend pourquoi autant d’utilitaires en ligne de commande sont développés en Rust aujourd’hui. Le niveau de maturité du langage pour ce type d’utilisation est excellent, grâce au typage explicite des arguments il parvient même à dépasser le niveau de confort fourni par Python qui a longtemps été la référence du domaine.
Si d’aventure vous voulez écrire des interfaces textuelles plus sophistiquées
qui reproduisent le comportement des GUIs, à la manière de perf report
et
zenith
, je vous encourage à étudier aussi les bibliothèques
ratatui
et cursive
qui sont conçues pour ce type d’utilisation. La première est plus spécialisée
dans la visualisation temps réel (pensez “moniteur système”), la seconde dans
les interfaces hautement interactives (pensez “perf report
”).