Nesta aula, vamos colocar em prática os conhecimentos adquiridos ao longo do curso, desenvolvendo uma ferramenta de linha de comando (CLI) completa em Rust. Você aprenderá a estruturar um projeto, utilizar a crate clap para processar argumentos, ler arquivos de forma eficiente e apresentar resultados de maneira clara e formatada.

Ao final, você terá uma ferramenta funcional que pode ser estendida e adaptada para suas próprias necessidades. Vamos construir um utilitário de busca textual simples, mas poderoso, que lê um arquivo e exibe as linhas que correspondem a um padrão, com numeração de linhas e destaque.

Estrutura

Um projeto Rust bem organizado é essencial para manutenção e escalabilidade. Para uma CLI, a estrutura mais comum é:

meu_comando/

Vamos criar um novo projeto com o Cargo:

cargo new meu_comando --bin

Isso gera a seguinte estrutura:

meu_comando/

Dentro do diretório, temos o arquivo Cargo.toml que define as dependências e metadados, e o diretório src com o arquivo main.rs. Para projetos maiores, podemos dividir o código em módulos. Por exemplo, podemos ter um módulo cli.rs para a definição da interface de linha de comando, um módulo file_reader.rs para leitura de arquivos, e um módulo output.rs para formatação da saída. A estrutura ficaria assim:

meu_comando/

Em main.rs, declaramos os módulos e a função principal:

mod cli;

Essa organização ajuda a manter o código limpo e facilita a testabilidade.

clap

A crate clap é a mais popular para construção de interfaces de linha de comando em Rust. Ela permite definir argumentos, opções e subcomandos de forma declarativa, com geração automática de ajuda e mensagens de erro.

Primeiro, adicionamos a dependência ao Cargo.toml:

[dependencies]

Em seguida, podemos usar a API derivada (derive) para definir nossa estrutura de argumentos. Vamos criar um comando que aceita um padrão de busca e um caminho de arquivo:

use clap::Parser;

Aqui, Parser é um trait que gera o parsing a partir da estrutura. O campo pattern é o primeiro argumento posicional, e path é o segundo. A docstring (///) é usada para gerar a mensagem de ajuda.

No main.rs, chamamos Cli::parse() para obter os argumentos:

let args = Cli::parse();

Podemos testar a execução: cargo run -- "padrão" "arquivo.txt". O clap cuida de validações, como número de argumentos, e exibe ajuda se necessário.

Leitura de arquivos

Para ler o conteúdo de um arquivo, usamos a biblioteca padrão. Vamos usar std::fs::read_to_string, que lê o arquivo inteiro em uma String. Isso é adequado para arquivos de texto moderados.

use std::fs;

Tratamos o erro com expect ou melhor, com match:

let content = fs::read_to_string(&args.path)

Para arquivos grandes, seria melhor usar BufReader e ler linha por linha. Vamos implementar uma função que retorna um iterador de linhas:

use std::io::{BufRead, BufReader};

Com isso, podemos percorrer as linhas sem carregar o arquivo inteiro na memória.

Saída formatada

Para apresentar os resultados, vamos formatar a saída de forma legível. Queremos exibir o número da linha e a linha correspondente, talvez destacando o padrão. Vamos usar a função println! com formatação.

for (index, line) in lines.enumerate() {

Para destacar o padrão, podemos usar a crate colored (opcional) ou simplesmente usar códigos ANSI. Vamos manter simples: exibir a linha na íntegra.

Exemplo de saída:

1: linha de exemplo com padrão

Podemos adicionar uma opção --verbose para mostrar mais detalhes, como o nome do arquivo.

Também é importante tratar o caso em que nenhuma linha corresponde: exibir uma mensagem informativa.

Boas práticas e observações finais

Ao construir uma CLI, é importante:

  • Usar clap para validação e ajuda automática.
  • Tratar erros de forma clara, usando Result e mensagens amigáveis.
  • Separar a lógica de negócio da apresentação.
  • Escrever testes para as funções principais.
  • Documentar o código e o uso da ferramenta.

Com esses conceitos, você pode estender a ferramenta com opções como ignorar maiúsculas, recursão em diretórios, etc.

Referências

Exercícios

  1. Modifique a estrutura para incluir um módulo search.rs que contenha uma função search_lines que recebe o padrão e o conteúdo e retorna um vetor de tuplas (número da linha, texto). Implemente a função e use-a no main.rs.

✓ Resposta:
// src/search.rs

Em main.rs:

mod search;
  1. Adicione uma opção --ignore-case para que a busca seja insensível a maiúsculas/minúsculas. Use a crate regex? Não, use to_lowercase().

✓ Resposta:
use clap::Parser;

Na função de busca, compare com to_lowercase().

  1. Implemente a leitura do arquivo linha por linha usando BufReader e lines(), retornando um iterador. Ajuste o main para usar essa leitura.

✓ Resposta:
use std::fs::File;
  1. Adicione uma opção --count que, em vez de exibir as linhas, exibe apenas o número de correspondências.

✓ Resposta:
if args.count {
  1. Escreva testes unitários para a função search_lines (ou similar) cobrindo casos com e sem correspondências, e com ignore_case.

✓ Resposta:
#[cfg(test)]