Introdução
Uma aplicação útil de linha de comando conecta várias partes: argumentos tipados, dados do domínio, armazenamento persistente, saída clara, erros recuperáveis e verificações automatizadas. Criar tudo isso a partir de um arquivo vazio esconderia o design sob uma grande quantidade de código digitado.
Neste laboratório, a configuração inicial fornece o esqueleto completo de uma lista de tarefas. Você lerá o mapa de módulos e, em seguida, concluirá uma parte por vez: decodificar e salvar o armazenamento, adicionar tarefas, listar tarefas, marcar tarefas como concluídas e gerar uma compilação de release testada. Você nunca precisará colar um arquivo-fonte inteiro.
Leia o mapa do projeto e a interface de comandos
Nesta etapa, você se familiarizará com o projeto preparado e relacionará os arquivos ao fluxo de dados antes de editar o código.
Entre no projeto e liste os arquivos de código-fonte:
cd /home/labex/project/tasker
ls src
lib.rs main.rs model.rs store.rs
Cada arquivo tem uma responsabilidade principal:
main.rscontrola a fronteira do processo: analisa os argumentos, imprime mensagens de sucesso e informa falhas;lib.rscontém as operações de tarefas chamadas pela CLI e pelos testes;model.rsdefine umaTaske a converte de e para uma linha de armazenamento;store.rslê e grava a coleção de tarefas.
Essa separação impede que a análise de argumentos, as operações do domínio e os detalhes do arquivo se transformem em uma única função grande. A configuração inicial já escreveu as declarações dos módulos e o analisador mais longo do modelo; você concluirá apenas as partes marcadas nas operações.
Inspecione o ponto de entrada da CLI:
nano src/main.rs
#[command(subcommand)] informa ao clap que a próxima palavra do comando seleciona uma variante do enum Commands. A opção --file está marcada como global = true, portanto os usuários podem colocá-la antes ou depois de um subcomando. Seu valor PathBuf padrão é tasks.db. Pressione Ctrl+X sem alterar o arquivo.
Exiba a ajuda de nível superior gerada:
cargo run --quiet -- --help
A ajuda lista add, list e done. Solicite a ajuda específica de add:
cargo run --quiet -- add --help
O <TITLE> obrigatório vem do campo title: String da variante Add. A interface já existe; nas etapas seguintes, você fará com que a operação de biblioteca de cada comando funcione.
Conclua a fronteira de armazenamento
Nesta etapa, você concluirá as duas pequenas transformações que conectam os valores das tarefas a um arquivo de texto local.
Abra o módulo de armazenamento preparado:
nano src/store.rs
O formato do arquivo usa uma tarefa por linha, com três campos separados por tabulações:
id<TAB>status<TAB>title
model.rs já fornece Task::encode e Task::decode. O módulo de armazenamento precisa apenas aplicar esses auxiliares à coleção completa.
Substitua o TODO de carregamento e as duas linhas abaixo dele por:
let tasks = contents
.lines()
.filter(|line| !line.is_empty())
.map(Task::decode)
.collect::<Result<Vec<_>, _>>()?;
Ok(tasks)
O iterador transforma cada linha não vazia em um Result<Task, String>. Coletar em Result<Vec<_>, _> interrompe a operação na primeira linha inválida ou retorna todas as tarefas decodificadas. O ponto de interrogação propaga esse erro a partir de load.
Em save, substitua o TODO e as três linhas finais por:
fs::write(path, contents)
.map_err(|error| format!("could not write {}: {error}", path.display()))
fs::write cria ou substitui o arquivo de armazenamento. map_err adiciona o caminho que falhou, preservando um Result recuperável.
Salve e saia do nano. Execute apenas o teste específico de armazenamento:
cargo test store::tests::saves_and_loads_tasks
Um único teste aprovado comprova que uma coleção de tarefas pode atravessar a fronteira do arquivo e retornar como valores Rust iguais.
Adicione e persista novas tarefas
Nesta etapa, você implementará a operação de biblioteca por trás do subcomando add.
Abra o ponto de entrada da biblioteca:
nano src/lib.rs
Substitua o TODO de add_task e o corpo de placeholder 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)
A operação primeiro carrega o estado atual. max().unwrap_or(0) + 1 produz o ID 1 para um arquivo vazio e, caso contrário, um valor maior em um que o maior ID existente. A tarefa é clonada uma vez porque uma cópia pertencente ao vetor, enquanto a cópia retornada permite que a CLI descreva o que foi adicionado.
Salve e saia do nano. Adicione duas tarefas a um arquivo separado para demonstração:
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
O segundo ID comprova que o comando carregou o primeiro registro antes de escolher e salvar o próximo.
Formate a lista de tarefas
Nesta etapa, você converterá as tarefas armazenadas em uma saída de comando estável e legível.
Abra src/lib.rs novamente:
nano src/lib.rs
Substitua o TODO de list_tasks e o corpo de placeholder 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())
O marcador é uma visualização compacta do status: [ ] significa aberta e [x] significa concluída. Essa função retorna linhas de exibição em vez de imprimi-las, permitindo que os testes e outros chamadores inspecionem o resultado sem capturar a saída do terminal.
Salve e saia do nano. Reutilize o arquivo criado na etapa anterior:
cargo run --quiet -- --file add-demo.db list
[ ] 1: Write release notes
[ ] 2: Tag version
A biblioteca é responsável por formatar as linhas das tarefas, enquanto main.rs continua responsável apenas por imprimir as linhas retornadas.
Marque uma tarefa como concluída
Nesta etapa, você atualizará uma tarefa preservando o restante da coleção armazenada.
Abra o código-fonte da biblioteca:
nano src/lib.rs
Substitua o TODO de complete_task e o corpo de placeholder 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() fornece referências mutáveis para que o registro correspondente possa ser alterado no próprio lugar. find retorna None quando o ID não existe; ok_or_else converte essa ausência no erro explicativo da função. A tarefa concluída é clonada antes do salvamento porque o empréstimo mutável pertence ao vetor que será salvo.
Salve e saia do nano. Conclua a tarefa 1 no arquivo de demonstração:
cargo run --quiet -- --file add-demo.db done 1
Completed 1: Write release notes
Liste novamente o estado armazenado:
cargo run --quiet -- --file add-demo.db list
[x] 1: Write release notes
[ ] 2: Tag version
Teste um ID inexistente:
cargo run --quiet -- --file add-demo.db done 99
Esse comando deve falhar. A mensagem é enviada para stderr e o processo termina com um código diferente de zero porque main.rs já converte o Err da biblioteca na fronteira do processo.
Teste e faça a compilação de release
Nesta etapa, você aplicará o ciclo de qualidade para entrega e produzirá um executável de release a partir do projeto concluído.
Comece formatando as edições feitas nos módulos de armazenamento e da biblioteca:
cargo fmt
Confirme que o formatador não deixou alterações pendentes:
cargo fmt -- --check
Execute o Clippy em modo rigoroso:
cargo clippy -- -D warnings
Execute o conjunto completo de testes:
cargo test
Dois testes devem passar: o teste específico de ida e volta do armazenamento e o fluxo completo de biblioteca add-list-done. Esses testes usam arquivos temporários, portanto exercitam a persistência real sem depender do banco de dados de demonstração.
Compile o destino de release otimizado:
cargo build --release --locked
--release seleciona o perfil de release otimizado do Cargo, em vez do perfil de desenvolvimento, que é mais rápido para compilar. --locked exige exatamente o grafo de dependências já registrado em Cargo.lock. Execute diretamente o executável 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
O caminho direto comprova que você está executando o artefato compilado, em vez de pedir ao Cargo que o compile e execute para você.
Resumo
Você concluiu uma CLI Rust com vários comandos sem redigitar sua arquitetura. O projeto final analisa subcomandos tipados com clap, persiste os modelos de tarefas por meio de um módulo de armazenamento dedicado, propaga erros com contexto, mantém a saída do processo em main, passa nos testes específicos e de ponta a ponta da biblioteca, atende ao ciclo de qualidade e produz um executável de release bloqueado.


