Introducción
Una aplicación de línea de comandos útil conecta varios límites: argumentos tipados, datos del dominio, almacenamiento persistente, una salida clara, errores recuperables y comprobaciones automatizadas. Crear todo desde un archivo vacío ocultaría el diseño entre una gran cantidad de código que tendría que escribir.
En este laboratorio, la configuración proporciona el esqueleto completo de un gestor de tareas. Primero leerá el mapa de módulos y después completará un límite pequeño cada vez: descodificar y guardar el almacenamiento, añadir tareas, listar tareas, marcar tareas como completadas y generar una compilación de lanzamiento probada. No tendrá que pegar nunca un archivo fuente completo.
Lea el mapa del proyecto y la interfaz de comandos
En este paso, se familiarizará con el proyecto preparado y relacionará sus archivos con el flujo de datos antes de editar el código.
Entre en el proyecto y liste sus archivos fuente:
cd /home/labex/project/tasker
ls src
lib.rs main.rs model.rs store.rs
Cada archivo tiene una responsabilidad principal:
main.rsgestiona el límite del proceso: analiza los argumentos, muestra los mensajes de éxito e informa de los errores;lib.rscontiene las operaciones sobre tareas que utilizan la CLI y las pruebas;model.rsdefine unaTasky la convierte desde y hacia una línea de almacenamiento;store.rslee y escribe la colección de tareas.
Esta separación evita que el análisis de argumentos, las operaciones del dominio y los detalles de los archivos terminen en una única función demasiado grande. La configuración ya ha escrito las declaraciones de los módulos y el analizador más extenso del modelo; usted solo completará los límites de operación marcados.
Inspeccione el punto de entrada de la CLI:
nano src/main.rs
#[command(subcommand)] indica a clap que la siguiente palabra del comando selecciona una variante del enumerado Commands. La opción --file está marcada como global = true, por lo que los usuarios pueden colocarla antes o después de un subcomando. Su valor PathBuf predeterminado es tasks.db. Pulse Ctrl+X sin modificar el archivo.
Muestre la ayuda general generada:
cargo run --quiet -- --help
La ayuda muestra add, list y done. Solicite ayuda específica para add:
cargo run --quiet -- add --help
El argumento obligatorio <TITLE> procede del campo title: String de la variante Add. La interfaz ya existe; en los pasos siguientes hará funcionar la operación de biblioteca de cada comando.
Complete el límite de almacenamiento
En este paso, terminará las dos pequeñas transformaciones que conectan los valores de las tareas con un archivo de texto local.
Abra el módulo de almacenamiento preparado:
nano src/store.rs
El formato de archivo utiliza una tarea por línea, con tres campos separados por tabuladores:
id<TAB>status<TAB>title
model.rs ya proporciona Task::encode y Task::decode. El módulo de almacenamiento solo necesita aplicar esas funciones auxiliares a la colección completa.
Reemplace el TODO de carga y las dos líneas que aparecen debajo por:
let tasks = contents
.lines()
.filter(|line| !line.is_empty())
.map(Task::decode)
.collect::<Result<Vec<_>, _>>()?;
Ok(tasks)
El iterador convierte cada línea no vacía en un Result<Task, String>. Al recopilar el resultado en Result<Vec<_>, _>, se detiene en la primera línea no válida o devuelve todas las tareas descodificadas. El signo de interrogación propaga ese error desde load.
En save, reemplace su TODO y las tres últimas líneas por:
fs::write(path, contents)
.map_err(|error| format!("could not write {}: {error}", path.display()))
fs::write crea o reemplaza el archivo de almacenamiento. map_err añade la ruta que produjo el error y conserva un Result recuperable.
Guarde el archivo, salga de nano y ejecute solo la prueba específica del almacenamiento:
cargo test store::tests::saves_and_loads_tasks
Una única prueba aprobada demuestra que una colección de tareas puede atravesar el límite del archivo y volver como valores de Rust iguales.
Añada y persista tareas nuevas
En este paso, implementará la operación de biblioteca que utiliza el subcomando add.
Abra el punto de entrada de la biblioteca:
nano src/lib.rs
Reemplace el TODO de add_task y el cuerpo provisional por:
let mut tasks = store::load(path)?;
let next_id = tasks.iter().map(|task| task.id).max().unwrap_or(0) + 1;
let task = Task::new(next_id, title);
tasks.push(task.clone());
store::save(path, &tasks)?;
Ok(task)
La operación carga primero el estado actual. max().unwrap_or(0) + 1 produce el ID 1 para un archivo vacío y, en los demás casos, un ID superior en uno al mayor ID existente. La tarea se clona una vez porque una copia propia entra en el vector, mientras que la copia devuelta permite que la CLI describa lo que se añadió.
Guarde el archivo y salga de nano. Añada dos tareas a un archivo de demostración independiente:
cargo run --quiet -- --file add-demo.db add "Write release notes"
Added 1: Write release notes
cargo run --quiet -- --file add-demo.db add "Tag version"
Added 2: Tag version
El segundo ID demuestra que el comando cargó el primer registro antes de elegir y guardar el siguiente.
Dé formato a la lista de tareas
En este paso, convertirá las tareas almacenadas en una salida de comando estable y fácil de leer.
Abra de nuevo src/lib.rs:
nano src/lib.rs
Reemplace el TODO de list_tasks y el cuerpo provisional por:
let tasks = store::load(path)?;
Ok(tasks
.iter()
.map(|task| {
let marker = if task.done { "x" } else { " " };
format!("[{marker}] {}: {}", task.id, task.title)
})
.collect())
El marcador ofrece una vista compacta del estado: [ ] significa que la tarea está abierta y [x] que está completada. Esta función devuelve filas para mostrar en lugar de imprimirlas, de modo que las pruebas y otros consumidores puedan inspeccionar el resultado sin capturar la salida del terminal.
Guarde el archivo y salga de nano. Vuelva a utilizar el archivo creado en el paso anterior:
cargo run --quiet -- --file add-demo.db list
[ ] 1: Write release notes
[ ] 2: Tag version
La biblioteca se encarga de dar formato a las filas de tareas, mientras que main.rs sigue siendo responsable únicamente de imprimir las líneas devueltas.
Marque una tarea como completada
En este paso, actualizará una tarea y conservará el resto de la colección almacenada.
Abra el código fuente de la biblioteca:
nano src/lib.rs
Reemplace el TODO de complete_task y el cuerpo provisional por:
let mut tasks = store::load(path)?;
let task = tasks
.iter_mut()
.find(|task| task.id == id)
.ok_or_else(|| format!("task {id} was not found"))?;
task.done = true;
let completed = task.clone();
store::save(path, &tasks)?;
Ok(completed)
iter_mut() proporciona referencias mutables para que el registro coincidente pueda cambiar directamente. find devuelve None cuando el ID no existe; ok_or_else convierte esa ausencia en el error explicativo de la función. La tarea completada se clona antes de guardar porque el préstamo mutable pertenece al vector que se va a guardar.
Guarde el archivo y salga de nano. Complete la tarea 1 en el archivo de demostración:
cargo run --quiet -- --file add-demo.db done 1
Completed 1: Write release notes
Vuelva a listar el estado almacenado:
cargo run --quiet -- --file add-demo.db list
[x] 1: Write release notes
[ ] 2: Tag version
Pruebe con un ID inexistente:
cargo run --quiet -- --file add-demo.db done 99
Se espera que este comando falle. El mensaje se envía a stderr y el proceso termina con un código distinto de cero porque main.rs ya convierte el Err de la biblioteca en el límite del proceso.
Pruebe y compile la versión de lanzamiento
En este paso, aplicará el ciclo de calidad de entrega y generará un ejecutable de lanzamiento a partir del proyecto completado.
Comience dando formato a las modificaciones realizadas en los módulos de almacenamiento y de la biblioteca:
cargo fmt
Confirme que el formateador no tiene cambios pendientes:
cargo fmt -- --check
Ejecute Clippy en modo estricto:
cargo clippy -- -D warnings
Ejecute el conjunto completo de pruebas:
cargo test
Deben aprobarse dos pruebas: la prueba específica del recorrido de ida y vuelta del almacenamiento y el flujo completo de biblioteca add-list-done. Esas pruebas utilizan archivos temporales, por lo que comprueban la persistencia real sin depender de la base de datos de demostración.
Compile el destino de lanzamiento optimizado:
cargo build --release --locked
--release selecciona el perfil de lanzamiento optimizado de Cargo en lugar del perfil de desarrollo, que se compila más rápidamente. --locked exige utilizar exactamente el grafo de dependencias registrado en Cargo.lock. Ejecute directamente el ejecutable resultante:
./target/release/tasker --file release-demo.db add "Publish tasker"
Added 1: Publish tasker
./target/release/tasker --file release-demo.db list
[ ] 1: Publish tasker
La ruta directa demuestra que está ejecutando el artefacto compilado, en lugar de pedir a Cargo que lo compile y lo ejecute.
Resumen
Ha completado una CLI de Rust con varios comandos sin tener que volver a escribir su arquitectura. El proyecto terminado analiza subcomandos tipados con clap, persiste los modelos de tareas mediante un módulo de almacenamiento específico, propaga errores con contexto, mantiene la salida del proceso en main, supera las pruebas específicas y de extremo a extremo de la biblioteca, cumple el ciclo de calidad y genera un ejecutable de lanzamiento bloqueado.


