---
title: "Clone do grep em Rust"
url: "https://rustlang.com.br/projetos/grep-clone/"
markdown_url: "https://rustlang.com.br/projetos/grep-clone.MD"
description: "Construa um clone simplificado do grep em Rust com busca por regex, saída colorida e busca recursiva em diretórios. Walkthrough completo."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# Clone do grep em Rust

Construa um clone simplificado do grep em Rust com busca por regex, saída colorida e busca recursiva em diretórios. Walkthrough completo.


Neste projeto, vamos construir um clone simplificado do famoso comando `grep` do Unix. O `grep` é uma das ferramentas mais utilizadas no dia a dia de qualquer desenvolvedor — ele busca padrões de texto em arquivos e exibe as linhas correspondentes. Ao recriá-lo em Rust, você vai aprender sobre manipulação de strings, expressões regulares, leitura de arquivos, parsing de argumentos de linha de comando e saída formatada com cores.

Este projeto é ideal para quem já conhece os fundamentos de Rust e quer colocar em prática conceitos como tratamento de erros com `Result`, iteradores e o sistema de módulos. Ao final, você terá uma ferramenta funcional que pode usar no seu terminal.

## O Que Vamos Construir

Nosso `minigrep` terá os seguintes recursos:

- Busca por padrão usando expressões regulares
- Leitura de um ou múltiplos arquivos
- Busca recursiva em diretórios
- Saída colorida destacando os trechos encontrados
- Exibição de números de linha
- Opção de busca case-insensitive
- Contagem de ocorrências

## Estrutura do Projeto

```
minigrep/
├── Cargo.toml
└── src/
    ├── main.rs
    ├── cli.rs
    ├── buscador.rs
    └── formatador.rs
```

## Configurando o Projeto

Crie o projeto com o Cargo:

```bash
cargo new minigrep
cd minigrep
```

Edite o `Cargo.toml` para incluir as dependências:

```toml
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
regex = "1"
walkdir = "2"
colored = "2"
```

Usamos `clap` para parsing de argumentos, `regex` para os padrões de busca, `walkdir` para varredura recursiva de diretórios e `colored` para saída colorida no terminal.

## Passo 1: Definindo a Interface de Linha de Comando

Vamos começar criando o módulo `cli.rs`, que define os argumentos aceitos pelo programa usando o `clap` com a macro `derive`.

```rust
// src/cli.rs
use clap::Parser;

/// minigrep - um clone simplificado do grep em Rust
#[derive(Parser, Debug)]
#[command(name = "minigrep")]
#[command(about = "Busca padrões de texto em arquivos")]
pub struct Argumentos {
    /// O padrão (regex) a ser buscado
    pub padrao: String,

    /// Caminhos de arquivos ou diretórios para buscar
    #[arg(required = true)]
    pub caminhos: Vec<String>,

    /// Busca case-insensitive
    #[arg(short = 'i', long = "ignorar-caso")]
    pub ignorar_caso: bool,

    /// Busca recursiva em diretórios
    #[arg(short = 'r', long = "recursivo")]
    pub recursivo: bool,

    /// Exibe apenas a contagem de ocorrências
    #[arg(short = 'c', long = "contagem")]
    pub contagem: bool,

    /// Exibe números de linha
    #[arg(short = 'n', long = "numero-linha", default_value_t = true)]
    pub numero_linha: bool,
}
```

Cada campo da struct `Argumentos` corresponde a um argumento ou flag do programa. O `clap` cuida de gerar a mensagem de ajuda (`--help`) e validar as entradas automaticamente.

## Passo 2: Implementando o Motor de Busca

O módulo `buscador.rs` contém a lógica central — compilar a regex e buscar correspondências em cada linha de um arquivo.

```rust
// src/buscador.rs
use regex::Regex;
use std::fs;
use std::io;
use std::path::Path;
use walkdir::WalkDir;

/// Representa uma correspondência encontrada em um arquivo
#[derive(Debug)]
pub struct Correspondencia {
    pub arquivo: String,
    pub numero_linha: usize,
    pub conteudo: String,
    pub inicio: usize,
    pub fim: usize,
}

/// Compila o padrão regex com suporte a case-insensitive
pub fn compilar_regex(padrao: &str, ignorar_caso: bool) -> Result<Regex, regex::Error> {
    let padrao_final = if ignorar_caso {
        format!("(?i){}", padrao)
    } else {
        padrao.to_string()
    };
    Regex::new(&padrao_final)
}

/// Busca o padrão em um único arquivo e retorna as correspondências
pub fn buscar_em_arquivo(
    regex: &Regex,
    caminho: &Path,
) -> io::Result<Vec<Correspondencia>> {
    let conteudo = fs::read_to_string(caminho)?;
    let nome_arquivo = caminho.display().to_string();

    let mut resultados = Vec::new();

    for (indice, linha) in conteudo.lines().enumerate() {
        if let Some(encontrado) = regex.find(linha) {
            resultados.push(Correspondencia {
                arquivo: nome_arquivo.clone(),
                numero_linha: indice + 1,
                conteudo: linha.to_string(),
                inicio: encontrado.start(),
                fim: encontrado.end(),
            });
        }
    }

    Ok(resultados)
}

/// Coleta todos os arquivos a serem buscados, expandindo diretórios se recursivo
pub fn coletar_arquivos(caminhos: &[String], recursivo: bool) -> Vec<String> {
    let mut arquivos = Vec::new();

    for caminho in caminhos {
        let path = Path::new(caminho);
        if path.is_file() {
            arquivos.push(caminho.clone());
        } else if path.is_dir() && recursivo {
            for entrada in WalkDir::new(caminho)
                .into_iter()
                .filter_map(|e| e.ok())
            {
                if entrada.file_type().is_file() {
                    arquivos.push(entrada.path().display().to_string());
                }
            }
        } else if path.is_dir() {
            eprintln!(
                "minigrep: '{}' é um diretório. Use -r para busca recursiva.",
                caminho
            );
        }
    }

    arquivos
}
```

A função `buscar_em_arquivo` lê o arquivo inteiro para a memória e itera linha a linha. Para cada linha que contém o padrão, criamos uma `Correspondencia` com a posição exata do trecho encontrado — isso será usado pelo formatador para colorir a saída.

## Passo 3: Formatando a Saída com Cores

O módulo `formatador.rs` é responsável por exibir os resultados de forma legível, com destaque colorido nos trechos encontrados.

```rust
// src/formatador.rs
use crate::buscador::Correspondencia;
use colored::*;

/// Exibe uma correspondência com cores e número de linha
pub fn exibir_correspondencia(resultado: &Correspondencia, mostrar_arquivo: bool, mostrar_numero: bool) {
    let mut saida = String::new();

    // Nome do arquivo em magenta
    if mostrar_arquivo {
        saida.push_str(&format!("{}:", resultado.arquivo.magenta()));
    }

    // Número da linha em verde
    if mostrar_numero {
        saida.push_str(&format!("{}:", resultado.numero_linha.to_string().green()));
    }

    // Linha com o trecho encontrado em vermelho/negrito
    let linha = &resultado.conteudo;
    let antes = &linha[..resultado.inicio];
    let destaque = &linha[resultado.inicio..resultado.fim];
    let depois = &linha[resultado.fim..];

    saida.push_str(&format!("{}{}{}", antes, destaque.red().bold(), depois));

    println!("{}", saida);
}

/// Exibe o resumo de contagem por arquivo
pub fn exibir_contagem(arquivo: &str, contagem: usize) {
    println!("{}:{}", arquivo.magenta(), contagem.to_string().green());
}
```

Usamos a crate `colored` para aplicar cores ANSI ao terminal. O trecho encontrado aparece em vermelho e negrito, o nome do arquivo em magenta e o número da linha em verde — similar ao `grep --color=always`.

## Passo 4: Juntando Tudo no main.rs

Agora vamos integrar todos os módulos no `main.rs`:

```rust
// src/main.rs
mod cli;
mod buscador;
mod formatador;

use clap::Parser;
use std::process;

fn main() {
    let args = cli::Argumentos::parse();

    // Compilar a expressão regular
    let regex = match buscador::compilar_regex(&args.padrao, args.ignorar_caso) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("Erro no padrão regex '{}': {}", args.padrao, e);
            process::exit(1);
        }
    };

    // Coletar todos os arquivos para busca
    let arquivos = buscador::coletar_arquivos(&args.caminhos, args.recursivo);

    if arquivos.is_empty() {
        eprintln!("minigrep: nenhum arquivo encontrado para buscar.");
        process::exit(1);
    }

    let mostrar_nome_arquivo = arquivos.len() > 1;
    let mut total_correspondencias: usize = 0;
    let mut houve_erro = false;

    for arquivo in &arquivos {
        let caminho = std::path::Path::new(arquivo);

        match buscador::buscar_em_arquivo(&regex, caminho) {
            Ok(resultados) => {
                if args.contagem {
                    // Modo contagem: exibe apenas o total por arquivo
                    if !resultados.is_empty() {
                        formatador::exibir_contagem(arquivo, resultados.len());
                    }
                } else {
                    // Modo normal: exibe cada correspondência
                    for resultado in &resultados {
                        formatador::exibir_correspondencia(
                            resultado,
                            mostrar_nome_arquivo,
                            args.numero_linha,
                        );
                    }
                }
                total_correspondencias += resultados.len();
            }
            Err(e) => {
                eprintln!("minigrep: erro ao ler '{}': {}", arquivo, e);
                houve_erro = true;
            }
        }
    }

    // Código de saída: 0 = encontrou, 1 = não encontrou, 2 = erro
    if houve_erro {
        process::exit(2);
    } else if total_correspondencias == 0 {
        process::exit(1);
    }
}
```

O fluxo principal é direto: parsear argumentos, compilar a regex, coletar arquivos, buscar em cada um e exibir os resultados. Seguimos a convenção do `grep` real para os códigos de saída: 0 quando encontra correspondências, 1 quando não encontra e 2 quando ocorre um erro.

## Como Executar

Compile e execute o projeto:

```bash
cargo build --release
```

Exemplos de uso:

```bash
# Buscar "fn main" em um arquivo
./target/release/minigrep "fn main" src/main.rs

# Saída:
# 10:fn main() {

# Busca case-insensitive recursiva
./target/release/minigrep -i -r "struct" src/

# Saída:
# src/cli.rs:8:pub struct Argumentos {
# src/buscador.rs:8:pub struct Correspondencia {

# Contar ocorrências de "use" em todos os arquivos .rs
./target/release/minigrep -c -r "use" src/

# Saída:
# src/main.rs:2
# src/cli.rs:1
# src/buscador.rs:4
# src/formatador.rs:2

# Buscar com regex: linhas que começam com "pub"
./target/release/minigrep -r "^pub" src/

# Buscar endereços de email em arquivos de texto
./target/release/minigrep "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}" emails.txt
```

## Desafios para Expandir

1. **Suporte a glob patterns**: Permita que o usuário passe padrões como `*.rs` ou `src/**/*.txt` em vez de caminhos fixos. Use a crate `glob` para expandir os padrões.

2. **Contexto ao redor**: Implemente as flags `-A` (linhas depois), `-B` (linhas antes) e `-C` (linhas ao redor), como o grep real faz. Isso exige manter um buffer das linhas anteriores.

3. **Busca invertida**: Adicione a flag `-v` que exibe apenas as linhas que *não* correspondem ao padrão — útil para filtrar logs.

4. **Saída em formato JSON**: Adicione uma flag `--json` que retorna os resultados em formato JSON estruturado, facilitando a integração com outras ferramentas via pipe.

5. **Busca paralela com threads**: Use `rayon` ou `std::thread` para buscar em múltiplos arquivos simultaneamente, melhorando a performance em diretórios grandes.

## Veja Também

- [Manipulação de Strings](/stdlib/string/) — operações com `String` e `&str`
- [Módulo fs](/stdlib/fs-module/) — leitura e escrita de arquivos
- [Módulo io](/stdlib/io-module/) — entrada e saída no Rust
- [Usando Expressões Regulares](/receitas/usar-regex/) — receita prática com a crate `regex`
- [Lendo Arquivos](/receitas/ler-arquivo/) — padrões para leitura de arquivos
- [Lendo Argumentos CLI](/receitas/ler-argumentos-cli/) — como parsear argumentos de linha de comando
