5.1 La voz en la puerta
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
El letrero sobre la entrada de un taller no es decoración. Le dice al visitante adónde ha llegado antes de llamar. En ningún lugar está escrito que deba haber alguien al otro lado - eso es demasiado obvio para gastar tinta. Un taller sin su dueño es solo una habitación con un letrero.
Lo que necesitas
El main del capítulo 4.5 ejecuta la misma secuencia en cada invocación: cargar la config,
añadir tres tareas, imprimir la lista, completar una. La línea de comandos no tiene efecto
en nada de eso. Escribir tq list o tq add "Buy coffee" es como hablar a una pared.
Se necesita un analizador de argumentos de línea de comandos. clap es la elección estándar
en el ecosistema de Rust: su interfaz derive permite describir una CLI como un struct, y
--help se genera a partir de ella automáticamente.
La construcción
Dos cambios en Cargo.toml: [[bin]] añadido a mano, clap mediante
cargo add clap --features derive:
# Cargo.toml - CAMBIADO
[[bin]]
name = "tq"
path = "src/main.rs"
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "1.1"
[[bin]] en Cargo.toml nombra el binario tq - ese nombre aparecerá en Usage: en
--help y en target/debug/. Sin él, el binario toma su nombre del campo name en
[package] - correcto si el paquete ya se llama tq. En el código de ejemplo del capítulo
bajo tq/ el paquete se nombra por su capítulo, así que [[bin]] es necesario allí.
Los tipos y la lógica de la CLI viven en un src/cli/mod.rs separado. src/main.rs sigue
siendo el punto de entrada: analizar los argumentos, cargar la config, ceder el control.
En src/cli/mod.rs:
// src/cli/mod.rs - NUEVO
use crate::config::Config;
use crate::error::TqResult;
use crate::persistence;
use crate::store::{Store, TaskStore};
use crate::task::Task;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(about = "Task queue manager", version)]
pub struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Add a new task
Add {
/// Task title
title: String,
},
/// List all tasks
List,
/// Show a task
Get {
/// Task id
id: u64,
},
/// Mark a task done
Done {
/// Task id
id: u64,
},
}
pub fn run(cli: Cli, config: Config) -> TqResult<()> {
let path = config.data_dir.join("tasks.json");
let tasks = persistence::load(&path)?;
let mut store = TaskStore::new(tasks);
match cli.command {
Commands::Add { title } => {
let task = Task::new(0, &title)?;
store.add(task)?;
persistence::save(store.all(), &path)?;
println!("added");
}
Commands::List => {
for task in store.all() {
let marker = if task.is_done() { "✓" } else { "·" };
println!("{marker} {task}");
}
}
Commands::Get { id } => {
println!("{}", store.get(id)?);
}
Commands::Done { id } => {
store.get_mut(id)?.complete();
persistence::save(store.all(), &path)?;
println!("done");
}
}
Ok(())
}
#[derive(Parser)] pide a clap que genere un analizador a partir de Cli. #[command(...)]
describe la herramienta: about es la cadena de descripción; version sin argumento - clap
toma la versión del propio Cargo.toml. #[command(subcommand)] dice que el campo
command es un distribuidor: clap busca el primer argumento posicional entre las variantes
de Commands.
run recibe un Cli ya analizado y un Config cargado - leer el entorno se queda en el
lado de main. Add crea una tarea a través de Task::new - un título vacío devolverá un
error antes de que se escriba nada; 0 como id es un marcador de posición, el almacén lo
sobreescribirá con su siguiente valor. List marca las tareas completadas con ✓, las
pendientes con ·.
src/main.rs añade mod cli y reduce fn main a tres líneas:
// src/main.rs - CAMBIADO
mod cli;
mod config;
mod error;
mod persistence;
mod store;
mod task;
use clap::Parser;
use cli::Cli;
use config::Config;
use error::TqResult;
use std::path::Path;
fn main() -> TqResult<()> {
let cli = Cli::parse();
let config = Config::load(Path::new("config.toml"))?;
cli::run(cli, config)
}
parse_from comprueba el análisis de argumentos sin leer env::args(). En
src/cli/tests.rs:
// src/cli/tests.rs - NUEVO
use super::*;
#[test]
fn cli_parse_add() {
let cli = Cli::parse_from(["tq", "add", "Buy coffee"]);
let Commands::Add { title } = cli.command else { panic!("expected Add") };
assert_eq!(title, "Buy coffee");
}
#[test]
fn cli_parse_list() {
let cli = Cli::parse_from(["tq", "list"]);
assert!(matches!(cli.command, Commands::List));
}
#[test]
fn cli_parse_done() {
let cli = Cli::parse_from(["tq", "done", "42"]);
assert!(matches!(cli.command, Commands::Done { id: 42 }));
}
El resultado
$ cargo run -- --help
Task queue manager
Usage: tq <COMMAND>
Commands:
add Add a new task
list List all tasks
get Show a task
done Mark a task done
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
-- separa los argumentos de Cargo de los propios del programa - sin él, Cargo intentaría
analizar add como uno de sus propios comandos.
$ cargo run -- add --help
Add a new task
Usage: tq add <TITLE>
Arguments:
<TITLE> Task title
Options:
-h, --help Print help
cargo run -- add "Buy coffee" añade una tarea. cargo run -- list imprime la lista.
cargo run -- done 1 la completa.
make ci pasa.
Nota.
make run -- listno funciona:--lo consume make y nunca llega al programa. Los argumentos se pasan a través de la variableARGS:
# Makefile - CAMBIADO
run:
@cargo run -- $(ARGS)
Después de esto:
make run ARGS="list",make run ARGS="add 'Buy coffee'".
El código completo de
tqpara este capítulo está en5-interface/01-the-voice-at-the-door/.
Lore: mod declara, use encuentra
En main.rs hay líneas que fn main no usa directamente:
mod persistence;
mod store;
mod task;
mod foo; no es una importación. Es una declaración: “el archivo src/foo.rs existe y se
compila con el programa.” Todas estas declaraciones viven en main.rs - el punto de entrada
donde el compilador comienza la compilación. src/cli/mod.rs las alcanza a través de
crate::persistence, crate::store, crate::task - pero solo porque main.rs declaró su
existencia primero.
Elimina mod persistence; y crate::persistence en src/cli/mod.rs deja de existir,
aunque el archivo en disco no vaya a ningún lado.
Lore: Cómo lee clap la documentación
Las líneas /// Add a new task son comentarios de documentación de Rust. El compilador los
almacena como un atributo #[doc = "..."] en el tipo o campo. clap lee exactamente ese
atributo - no los literales de cadena directamente.
La consecuencia práctica: elimina el comentario y la variante permanece, pero --help
muestra un espacio en blanco. No es un error. Solo ayuda sin explicación - un estado que
ocurre más a menudo de lo que uno desearía.