Este último ejemplo cose varios de los anteriores en algo que se puede usar de verdad: una herramienta que lee un CSV, admite subórdenes y da mensajes de error útiles. Es el patrón de casi cualquier utilidad de línea de órdenes escrita en Rust.
También te puede interesar
Las dependencias
cargo add clap --features derive cargo add csv cargo add serde --features derive cargo add anyhow
[dependencies]
anyhow = "1.0.104"
clap = { version = "4.6.7", features = ["derive"] }
csv = "1.4.0"
serde = { version = "1.0.229", features = ["derive"] }
La herramienta entera
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Parser)]
#[command(version, about = "Consulta un inventario en CSV")]
struct Args {
/// Archivo CSV de entrada
#[arg(short, long, default_value = "inventario.csv")]
archivo: PathBuf,
#[command(subcommand)]
orden: Orden,
}
#[derive(Subcommand)]
enum Orden {
/// Lista los artículos
Listar {
/// Solo los que tengan al menos estas unidades
#[arg(short, long, default_value_t = 0)]
minimo: u32,
},
/// Muestra el valor total del inventario
Total,
}
#[derive(Debug, Deserialize)]
struct Articulo {
nombre: String,
precio: f64,
unidades: u32,
}
fn leer(ruta: &PathBuf) -> Result<Vec<Articulo>> {
let mut lector = csv::Reader::from_path(ruta)
.with_context(|| format!("no se pudo abrir {}", ruta.display()))?;
lector
.deserialize()
.collect::<Result<Vec<Articulo>, _>>()
.context("el CSV tiene filas con formato incorrecto")
}
fn main() -> Result<()> {
let args = Args::parse();
let articulos = leer(&args.archivo)?;
match args.orden {
Orden::Listar { minimo } => {
for a in articulos.iter().filter(|a| a.unidades >= minimo) {
println!("{:<12} {:>6.2} x {:>4} = {:>9.2}", a.nombre, a.precio, a.unidades, a.precio * a.unidades as f64);
}
}
Orden::Total => {
let total: f64 = articulos.iter().map(|a| a.precio * a.unidades as f64).sum();
println!("{} artículos, valor total {:.2}", articulos.len(), total);
}
}
Ok(())
}
cargo run -- listartornillos 1.50 x 120 = 180.00 tuercas 0.80 x 80 = 64.00 arandelas 0.25 x 45 = 11.25 clavos 0.10 x 500 = 50.00
cargo run -- listar --minimo 100tornillos 1.50 x 120 = 180.00 clavos 0.10 x 500 = 50.00
cargo run -- total4 artículos, valor total 305.25
Lo que aporta cada pieza
Las subórdenes son un enum, y cada variante lleva sus propios argumentos. Eso significa que minimo solo existe dentro de Listar: no hay forma de leerlo al ejecutar total, porque el tipo no lo permite. Es el conjunto cerrado de la lección de structs y enums puesto a trabajar.
anyhow resuelve el problema de que las funciones devuelven errores de tipos distintos: uno de archivo, otro del analizador de CSV. En una aplicación, a diferencia de una biblioteca, casi nunca hace falta distinguirlos por tipo, basta con propagarlos y contar bien qué pasó. Result<T> de anyhow acepta cualquiera de ellos.
Por qué importa .context()
Esa es la pieza que convierte un error inútil en uno que resuelve el problema. Con un archivo que no existe:
cargo run -- --archivo nada.csv totalError: no se pudo abrir nada.csv
Caused by:
No such file or directory (os error 2)
Sin el with_context, el usuario solo vería «No such file or directory», sin saber qué archivo. Con él, el mensaje dice qué se intentaba hacer y debajo la causa técnica. Esa cadena se puede encadenar en varios niveles, y cada capa añade su parte.
thiserror ayuda a escribir.--release. cargo build --release deja un ejecutable en target/release/ que no depende de nada instalado: se copia y funciona. Es una de las razones por las que Rust se ha vuelto popular para herramientas de línea de órdenes, junto con que el binario arranca al instante.El código de esta página se compiló y ejecutó con rustc 1.96.1 antes de publicarla, con las tres órdenes y con un archivo inexistente; todas las salidas, incluida la cadena de errores, están copiadas de la ejecución real. Documentación oficial: clap y anyhow.