#18 Construire un outil CLI
clap, args, sous-commandes
Parser les arguments
La méthode la plus simple pour lire les arguments de ligne de commande est std::env::args(). Elle retourne un itérateur sur les arguments passés au programme.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
// args[0] est le nom du programme
println!("Programme : {}", args[0]);
if args.len() < 2 {
println!("Utilisation : {} <nom>", args[0]);
return;
}
println!("Bonjour, {} !", args[1]);
}
// $ cargo run -- Alice
// Programme : target/debug/mon-outil
// Bonjour, Alice !Parser manuellement les options
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let mut verbose = false;
let mut fichier: Option<String> = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-v" | "--verbose" => verbose = true,
"-f" | "--file" => {
i += 1;
fichier = args.get(i).cloned();
}
_ => println!("Option inconnue : {}", args[i]),
}
i += 1;
}
if verbose {
println!("Mode verbeux activé");
}
if let Some(f) = fichier {
println!("Fichier : {}", f);
}
}Utiliser clap
Le crate clap est la référence pour parser les arguments en Rust. Il génère automatiquement l'aide, valide les arguments et gère les sous-commandes.
[dependencies]
clap = { version = "4", features = ["derive"] }use clap::Parser;
/// Un outil CLI de démonstration en Rust
#[derive(Parser)]
#[command(version, about)]
struct Cli {
/// Le fichier à traiter
fichier: String,
/// Motif à chercher dans le fichier
#[arg(short, long)]
motif: Option<String>,
/// Activer le mode verbeux
#[arg(short, long)]
verbose: bool,
/// Nombre de résultats à afficher
#[arg(short, long, default_value_t = 10)]
nombre: usize,
}
fn main() {
let cli = Cli::parse();
println!("Fichier : {}", cli.fichier);
if let Some(motif) = &cli.motif {
println!("Recherche de : {}", motif);
}
if cli.verbose {
println!("Mode verbeux activé");
println!("Nombre max de résultats : {}", cli.nombre);
}
}
// $ cargo run -- data.txt --motif "Rust" -v -n 5Sous-commandes
Les sous-commandes permettent de structurer votre outil comme git (avec git add, git commit, etc.). clap les gère nativement.
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(version, about = "Un outil CLI de démonstration")]
struct Cli {
#[command(subcommand)]
commande: Commandes,
}
#[derive(Subcommand)]
enum Commandes {
/// Compter les lignes d'un fichier
Compter {
/// Le fichier à analyser
fichier: String,
/// Compter aussi les mots
#[arg(short, long)]
mots: bool,
},
/// Chercher un motif dans un fichier
Chercher {
/// Le motif à chercher
motif: String,
/// Le fichier où chercher
fichier: String,
/// Ignorer la casse
#[arg(short, long)]
ignorer_casse: bool,
},
}
fn main() {
let cli = Cli::parse();
match cli.commande {
Commandes::Compter { fichier, mots } => {
println!("Compter les lignes de {}", fichier);
if mots {
println!("(et aussi les mots)");
}
}
Commandes::Chercher { motif, fichier, ignorer_casse } => {
println!("Chercher '{}' dans {}", motif, fichier);
if ignorer_casse {
println!("(casse ignorée)");
}
}
}
}
// $ cargo run -- compter data.txt --mots
// $ cargo run -- chercher "Rust" data.txt -iLire depuis stdin
Un bon outil CLI peut aussi lire depuis l'entrée standard (stdin), ce qui permet de l'utiliser dans des pipelines avec |.
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
let stdin = io::stdin();
println!("Entrez du texte (Ctrl+D pour terminer) :");
let mut total_lignes = 0;
let mut total_mots = 0;
for ligne in stdin.lock().lines() {
let ligne = ligne?;
total_lignes += 1;
total_mots += ligne.split_whitespace().count();
}
println!("{} lignes, {} mots", total_lignes, total_mots);
Ok(())
}
// Utilisation en pipeline :
// $ cat fichier.txt | cargo run
// $ echo "Bonjour le monde" | cargo runAssemblage final
Voici un exemple complet qui combine tout : clap pour les arguments, lecture de fichiers, gestion d'erreurs et sortie formatée.
use clap::{Parser, Subcommand};
use std::fs;
#[derive(Parser)]
#[command(name = "cli-tool", version, about = "Un outil CLI en Rust")]
struct Cli {
#[command(subcommand)]
commande: Commandes,
}
#[derive(Subcommand)]
enum Commandes {
/// Compter les lignes d'un fichier
Compter { fichier: String },
/// Chercher un motif dans un fichier
Chercher {
motif: String,
fichier: String,
},
}
fn compter(fichier: &str) -> Result<(), Box<dyn std::error::Error>> {
let contenu = fs::read_to_string(fichier)?;
let lignes = contenu.lines().count();
let mots = contenu.split_whitespace().count();
let caracteres = contenu.chars().count();
println!(" {} lignes", lignes);
println!(" {} mots", mots);
println!(" {} caractères", caracteres);
Ok(())
}
fn chercher(motif: &str, fichier: &str) -> Result<(), Box<dyn std::error::Error>> {
let contenu = fs::read_to_string(fichier)?;
for (num, ligne) in contenu.lines().enumerate() {
if ligne.contains(motif) {
println!(" {}:{} {}", fichier, num + 1, ligne);
}
}
Ok(())
}
fn main() {
let cli = Cli::parse();
let resultat = match cli.commande {
Commandes::Compter { ref fichier } => compter(fichier),
Commandes::Chercher { ref motif, ref fichier } => chercher(motif, fichier),
};
if let Err(e) = resultat {
eprintln!("Erreur : {}", e);
std::process::exit(1);
}
}À vous de jouer
Essayez les commandes ci-dessous pour compiler et exécuter le programme :