← Índice

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 -- list no funciona: -- lo consume make y nunca llega al programa. Los argumentos se pasan a través de la variable ARGS:

# 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 tq para este capítulo está en 5-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.