Phase 5Projet pratique

#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.

Arguments basiques
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

Parser des options simples
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.

Cargo.toml
[dependencies]
clap = { version = "4", features = ["derive"] }
Utiliser clap avec 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 5

Sous-commandes

Les sous-commandes permettent de structurer votre outil comme git (avec git add, git commit, etc.). clap les gère nativement.

Sous-commandes avec clap
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 -i

Lire 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 |.

Lire depuis stdin
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 run

Assemblage final

Voici un exemple complet qui combine tout : clap pour les arguments, lecture de fichiers, gestion d'erreurs et sortie formatée.

Outil CLI complet
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 :

terminal — cargo
user@stemlegacy:~/cli-tool$