clap en Rust: CLIs con subcomandos, derive API, validación de argumentos y ayuda automática

clap es la biblioteca de parsing de argumentos de línea de comandos más usada en Rust. Su Derive API permite definir la interfaz de una CLI directamente sobre structs y enums anotados, sin código de parsing manual. El resultado son herramientas con subcomandos, validación de tipos, ayuda automática y variables de entorno con muy pocas líneas de código.

Derive API básica: Parser y argumentos tipados

Con #[derive(Parser)] y las anotaciones #[arg], clap infiere el tipo de cada argumento, si es obligatorio u opcional, si admite múltiples valores y cómo se llama en la ayuda. Los tipos Option<T> producen argumentos opcionales; los tipos Vec<T> permiten repetir el flag.

// [dependencies]
// clap = { version = "4", features = ["derive"] }

use clap::Parser;

/// Herramienta para comprimir archivos
#[derive(Parser, Debug)]
#[command(name = "compresor", version = "1.0", author)]
struct Args {
    /// Archivos de entrada (uno o más)
    #[arg(required = true)]
    entradas: Vec<String>,

    /// Archivo de salida
    #[arg(short, long, default_value = "salida.zip")]
    output: String,

    /// Nivel de compresión (1-9)
    #[arg(short, long, default_value_t = 6, value_parser = clap::value_parser!(u8).range(1..=9))]
    nivel: u8,

    /// Modo verboso
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let args = Args::parse();
    if args.verbose {
        println!("Comprimiendo {:?} ? {} (nivel {})", args.entradas, args.output, args.nivel);
    }
    // ... lógica de compresión
}

Subcomandos con Subcommand

Los subcomandos permiten que una misma herramienta tenga comportamientos completamente distintos (como git add, git commit, git push). Se modelan con un enum anotado con #[derive(Subcommand)].

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(name = "repo", about = "Gestor de repositorios")]
struct Cli {
    #[command(subcommand)]
    comando: Comando,
}

#[derive(Subcommand)]
enum Comando {
    /// Clonar un repositorio
    Clonar {
        #[arg(help = "URL del repositorio")]
        url: String,
        #[arg(short, long, help = "Directorio destino")]
        dir: Option<String>,
    },
    /// Publicar cambios
    Push {
        #[arg(default_value = "origin")]
        remoto: String,
        #[arg(default_value = "main")]
        rama: String,
        #[arg(short, long)]
        force: bool,
    },
    /// Mostrar estado
    Status,
}

fn main() {
    let cli = Cli::parse();
    match cli.comando {
        Comando::Clonar { url, dir } => {
            let destino = dir.unwrap_or_else(|| url.split('/').last().unwrap_or("repo").into());
            println!("Clonando {url} en {destino}");
        }
        Comando::Push { remoto, rama, force } => {
            let flag = if force { " --force" } else { "" };
            println!("Push a {remoto}/{rama}{flag}");
        }
        Comando::Status => println!("Sin cambios pendientes"),
    }
}

value_parser personalizado y variables de entorno

Los argumentos pueden validarse con value_parser, que acepta cualquier función que devuelva Result. Las variables de entorno se vinculan con env, de modo que el usuario puede configurar la herramienta tanto por argumentos como por entorno.

use clap::Parser;
use std::net::SocketAddr;

fn parsear_url(s: &str) -> Result<String, String> {
    if s.starts_with("http://") || s.starts_with("https://") {
        Ok(s.to_string())
    } else {
        Err(format!("'{s}' no es una URL válida (debe comenzar con http:// o https://)"))
    }
}

#[derive(Parser)]
#[command(name = "servidor")]
struct Config {
    /// Dirección de escucha
    #[arg(long, default_value = "0.0.0.0:8080", env = "LISTEN_ADDR")]
    addr: SocketAddr,

    /// URL de la base de datos
    #[arg(long, env = "DATABASE_URL", value_parser = parsear_url)]
    db_url: String,

    /// Nivel de log
    #[arg(long, env = "LOG_LEVEL", default_value = "info",
          value_parser = ["trace","debug","info","warn","error"])]
    log_level: String,
}

fn main() {
    let cfg = Config::parse();
    println!("Escuchando en {}", cfg.addr);
    println!("DB: {}", cfg.db_url);
    println!("Log: {}", cfg.log_level);
}

Grupos mutuamente excluyentes y ArgGroup

Cuando dos flags no pueden usarse a la vez, ArgGroup expressa esa restricción en el nivel de la API, no en código de validación manual.

use clap::{Parser, ArgGroup};

#[derive(Parser)]
#[command(group(
    ArgGroup::new("formato")
        .required(true)
        .args(["json", "csv", "texto"])
))]
struct ExportArgs {
    /// Exportar como JSON
    #[arg(long)]
    json: bool,

    /// Exportar como CSV
    #[arg(long)]
    csv: bool,

    /// Exportar como texto plano
    #[arg(long)]
    texto: bool,

    /// Archivo de salida
    #[arg(short, long, default_value = "export")]
    output: String,
}

fn main() {
    let args = ExportArgs::parse();
    let formato = if args.json { "JSON" } else if args.csv { "CSV" } else { "texto" };
    println!("Exportando como {formato} ? {}", args.output);
    // Sin ArgGroup: habría que validar manualmente que solo uno está activo
}

La Derive API de clap es la forma más productiva de construir CLIs en Rust: los tipos y las anotaciones documentan la interfaz, el parseo es automático y seguro en tipos, y la ayuda generada (--help) siempre está sincronizada con el código sin esfuerzo adicional.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP