Créer des outils CLI professionnels en Rust : clap, indicatif, distribution
Rust est devenu le langage de choix pour les outils CLI de nouvelle génération. ripgrep (rg) est 5 à 10x plus rapide que grep. fd remplace find avec une ergonomie moderne. bat remplace cat avec coloration syntaxique. delta améliore git diff. starshipest le prompt de shell le plus rapide disponible. Ce n'est pas un hasard : Rust combine la performance native, un binaire statique sans dépendances, un démarrage instantané et une gestion d'erreurs expressive. Ce guide couvre les outils et patterns pour construire des CLI professionnels — de l'argument parsing à la distribution.
Clap : la référence pour l'argument parsing
clap(Command Line Argument Parser) est la bibliothèque d'argument parsing standard de l'écosystème Rust. Sa version 4.x, avec l'API derive, permet de définir la structure de commandes entière via des annotations sur des structs — le code de parsing est généré automatiquement.
use clap::Parser;
#[derive(Parser)]
#[command(name = "myapp", about = "Mon outil CLI", version)]
struct Cli {
/// Fichier d'entrée à traiter
#[arg(short, long, value_name = "FILE")]
input: PathBuf,
/// Niveau de verbosité
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Analyser le fichier
Analyze { #[arg(long)] strict: bool },
/// Générer un rapport
Report { #[arg(short, long)] output: PathBuf },
}L'API derive de clap génère automatiquement l'aide (--help), la validation des types, les sous-commandes et la complétion shell (bash, zsh, fish, PowerShell viaclap_complete). La doc-string Rust devient automatiquement la description de l'argument dans le --help. Résultat : un CLI professionnel avec validation, aide contextuelle et complétion shell en quelques dizaines de lignes.
Gestion d'erreurs : anyhow et thiserror
La gestion d'erreurs en CLI a deux faces : les erreurs internes (pour le développeur, avec du contexte) et les erreurs utilisateur (courtes, actionnables). anyhowest parfait pour la couche applicative des CLI : il permet d'additionner du contexte sur chaque erreur sans définir une hiérarchie de types complexe.
use anyhow::{Context, Result};
fn process_file(path: &Path) -> Result<()> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("impossible de lire {:?}", path))?;
// ...
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
process_file(&cli.input)
.context("échec du traitement")?;
Ok(())
}La valeur de retour Result<()> de main avec anyhow affiche automatiquement la chaîne de contexte sur stderr avec Error: ...et un code de sortie 1. Pour les libraries CLI qui exposent leurs propres types d'erreur, thiserror est préférable — il génère des implémentations std::error::Error propres via derive. Pour les patterns avancés de gestion d'erreurs en Rust, ces deux crates se complètent.
indicatif : barres de progression et spinners
Pour les opérations longues, le feedback visuel est essentiel. indicatif est la bibliothèque de référence pour les barres de progression et spinners en Rust, avec un support natif des terminaux modernes (Unicode, couleurs, Unicode width).
use indicatif::{ProgressBar, ProgressStyle};
let pb = ProgressBar::new(total_items);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})")
.unwrap());
for item in items {
process(item);
pb.inc(1);
}
pb.finish_with_message("terminé");indicatifsupporte plusieurs barres simultanées (pour les opérations parallèles), les spinners pour les opérations de durée inconnue, et la suppression automatique quand la sortie n'est pas un TTY (utile pour les scripts CI). Son architecture thread-safe permet de l'utiliser avec rayon pour les opérations parallèles avec barre de progression globale.
console et dialoguer : couleurs et interactions
console expose une API simple pour les couleurs, les styles et la détection de terminal :style("Succès").green().bold(). Elle gère automatiquement la désactivation des couleurs quand NO_COLORest défini ou quand la sortie n'est pas un TTY, ce qui est le comportement attendu par les utilisateurs Unix.
dialoguer (construit sur console) fournit des prompts interactifs : confirmation (Confirm), sélection dans une liste (Select, MultiSelect), saisie texte (Input), saisie mot de passe (Password). Ces composants gèrent la navigation clavier, la validation et le rendu propre dans le terminal.
use dialoguer::{Confirm, Select};
let proceed = Confirm::new()
.with_prompt("Supprimer les 42 fichiers sélectionnés ?")
.default(false)
.interact()?;
if proceed {
let style = Select::new()
.with_prompt("Format de sortie")
.items(&["JSON", "CSV", "Tableau"])
.default(0)
.interact()?;
// ...
}Performance et distribution : le pipeline complet
L'un des avantages les plus sous-estimés de Rust pour les CLI est la distribution. Un binaire Rust compilé en release est statiquement lié (sur Linux avec musl), standalone, et fonctionne sans installation de runtime. L'ensemble de la chaîne :
- Build release optimisé :
cargo build --releaseavecopt-level = 3etlto = truedansCargo.toml. Le link-time optimization (LTO) réduit la taille du binaire de 20 à 40 % et améliore les performances inter-crate. - Strip des symboles :
strip = truedans le profil release supprime les symboles de debug, réduisant la taille du binaire de 50 à 80 %. Un CLI Rust typique pèse entre 500 Ko et 3 Mo après strip, contre 30 à 100 Mo pour l'équivalent Go ou Electron. - Cross-compilation : avec
cargo-crossou les targetsx86_64-unknown-linux-musl,aarch64-apple-darwin,x86_64-pc-windows-gnu, vous produisez des binaires pour Linux, macOS (Intel/ARM) et Windows depuis un même poste. - Distribution :
cargo installpour les développeurs, GitHub Releases pour les binaires pré-compilés (aveccargo-distqui automatise le workflow), Homebrew tap pour macOS, packages .deb/.rpm aveccargo-deb/cargo-rpm.
La taille mémoire à l'exécution est également un avantage : un CLI Rust tient généralement dans 10 à 30 Mo de RAM (heap inclus), contre 50 à 200 Mo pour l'équivalent Python ou Node.js chargé avec ses dépendances. Pour les environnements containerisés, cette efficacité se traduit directement en densité plus élevée et coûts inférieurs.
Async dans les CLI : quand et comment
Tous les CLIs n'ont pas besoin d'async. Pour les outils qui traitent des fichiers locaux ou font peu d'I/O réseau, le code synchrone est plus simple et suffisamment rapide. L'async devient pertinent quand le CLI doit effectuer plusieurs opérations réseau concurrentes — télécharger plusieurs fichiers en parallèle, interroger plusieurs APIs simultanément, ou maintenir une connexion WebSocket longue durée. Dans ce cas, ajouter Tokio et reqwest(le client HTTP async de référence) reste cohérent avec l'approche décrite dans notre article sur l'async Rust avec Tokio.
“Le meilleur CLI est celui qu'on oublie — parce qu'il démarre instantanément, ne plante jamais, et fait exactement ce qui est demandé sans surprise. C'est la promesse que Rust tient mieux que n'importe quel autre langage.”
Construire un CLI Rust de qualité production demande peu de code et beaucoup de soin : une interface cohérente, des messages d'erreur actionnables, un feedback visuel adapté et une distribution sans friction pour l'utilisateur final. Ces détails font la différence entre un outil qu'on installe une fois et qu'on oublie, et un outil qu'on recommande. Pour développer des outils CLI Rust sur mesure adaptés à vos workflows internes ou à vos utilisateurs, nous intervenons de la conception à la distribution multi-plateforme.