---
title: "Mini Grep em Zig — Tutorial Passo a Passo"
url: "https://ziglang.com.br/projetos/mini-grep-em-zig-tutorial-passo-a-passo/"
markdown_url: "https://ziglang.com.br/projetos/mini-grep-em-zig-tutorial-passo-a-passo.MD"
description: "Construa uma implementação simplificada do grep em Zig com busca por regex básico, highlight, contagem e busca recursiva."
date: "2026-02-21"
author: "Zig Brasil"
---

# Mini Grep em Zig — Tutorial Passo a Passo

Construa uma implementação simplificada do grep em Zig com busca por regex básico, highlight, contagem e busca recursiva.


# Mini Grep em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir uma **implementação simplificada do grep** — a clássica ferramenta Unix de busca de texto. Nosso mini-grep busca padrões em arquivos, suporta highlight de matches, contagem de ocorrências e busca recursiva em diretórios.

## O Que Vamos Construir

Nosso mini-grep vai:

- Buscar padrões de texto em um ou mais arquivos
- Suportar busca case-insensitive
- Highlight das ocorrências encontradas com cores ANSI
- Exibir números de linha
- Buscar recursivamente em diretórios
- Contar ocorrências por arquivo
- Suportar padrões glob simples (*, ?)

## Pré-requisitos

- Zig 0.13+ instalado ([guia de instalação](/tutoriais/instalacao/))
- Familiaridade com [I/O de arquivos](/receitas/io-arquivos/) em Zig

## Passo 1: Estrutura do Projeto

```bash
mkdir mini-grep
cd mini-grep
zig init
```

## Passo 2: Opções de Busca e Matching

```zig
const std = @import("std");
const fs = std.fs;
const mem = std.mem;
const io = std.io;
const fmt = std.fmt;
const Allocator = std.mem.Allocator;

/// Opções de busca configuráveis.
const OpcoesBusca = struct {
    case_insensitive: bool = false,
    mostrar_numeros_linha: bool = true,
    highlight: bool = true,
    contar_apenas: bool = false,
    recursivo: bool = false,
    max_resultados: usize = 1000,
    contexto_antes: usize = 0,
    contexto_depois: usize = 0,
};

/// Resultado de uma busca em uma linha.
const Match = struct {
    linha_num: usize,
    coluna: usize,
    comprimento: usize,
    linha: []const u8,
};

/// Busca um padrão em uma linha, suportando case-insensitive.
/// Retorna a posição da primeira ocorrência ou null.
fn buscarPadrao(linha: []const u8, padrao: []const u8, case_insensitive: bool) ?usize {
    if (padrao.len == 0 or padrao.len > linha.len) return null;

    var i: usize = 0;
    while (i + padrao.len <= linha.len) : (i += 1) {
        var match = true;
        for (linha[i .. i + padrao.len], padrao) |lc, pc| {
            const a = if (case_insensitive and lc >= 'A' and lc <= 'Z') lc + 32 else lc;
            const b = if (case_insensitive and pc >= 'A' and pc <= 'Z') pc + 32 else pc;
            if (a != b) {
                match = false;
                break;
            }
        }
        if (match) return i;
    }
    return null;
}

/// Conta todas as ocorrências de um padrão em uma linha.
fn contarOcorrencias(linha: []const u8, padrao: []const u8, case_insensitive: bool) usize {
    var count: usize = 0;
    var pos: usize = 0;

    while (pos + padrao.len <= linha.len) {
        if (buscarPadrao(linha[pos..], padrao, case_insensitive)) |offset| {
            count += 1;
            pos += offset + 1;
        } else break;
    }

    return count;
}
```

## Passo 3: Motor de Busca em Arquivos

```zig
/// Resultado da busca em um arquivo.
const ResultadoArquivo = struct {
    caminho: [512]u8,
    caminho_len: usize,
    matches: usize,
    linhas_com_match: usize,

    pub fn caminhoStr(self: *const ResultadoArquivo) []const u8 {
        return self.caminho[0..self.caminho_len];
    }
};

/// Busca um padrão em um arquivo, exibindo resultados.
fn buscarEmArquivo(
    caminho: []const u8,
    padrao: []const u8,
    opcoes: *const OpcoesBusca,
    writer: anytype,
) !ResultadoArquivo {
    var resultado = ResultadoArquivo{
        .caminho = undefined,
        .caminho_len = @min(caminho.len, 512),
        .matches = 0,
        .linhas_com_match = 0,
    };
    @memcpy(resultado.caminho[0..resultado.caminho_len], caminho[0..resultado.caminho_len]);

    const file = fs.cwd().openFile(caminho, .{}) catch |err| {
        try writer.print("  Erro ao abrir {s}: {any}\n", .{ caminho, err });
        return resultado;
    };
    defer file.close();

    var buf: [4096]u8 = undefined;
    const reader = file.reader();
    var linha_num: usize = 0;

    while (reader.readUntilDelimiterOrEof(&buf, '\n')) |maybe_line| {
        const linha = maybe_line orelse break;
        linha_num += 1;

        const ocorrencias = contarOcorrencias(linha, padrao, opcoes.case_insensitive);
        if (ocorrencias == 0) continue;

        resultado.matches += ocorrencias;
        resultado.linhas_com_match += 1;

        if (!opcoes.contar_apenas) {
            // Exibir a linha com highlight
            const reset = "\x1b[0m";
            const cor_match = "\x1b[1;31m";
            const cor_arquivo = "\x1b[35m";
            const cor_num = "\x1b[32m";

            try writer.print("{s}{s}{s}:", .{ cor_arquivo, caminho, reset });
            if (opcoes.mostrar_numeros_linha) {
                try writer.print("{s}{d}{s}:", .{ cor_num, linha_num, reset });
            }

            // Highlight das ocorrências
            if (opcoes.highlight) {
                try exibirComHighlight(linha, padrao, opcoes.case_insensitive, cor_match, reset, writer);
            } else {
                try writer.print("{s}", .{linha});
            }
            try writer.print("\n", .{});
        }
    } else |_| {}

    return resultado;
}

/// Exibe uma linha com as ocorrências do padrão em destaque.
fn exibirComHighlight(
    linha: []const u8,
    padrao: []const u8,
    case_insensitive: bool,
    cor: []const u8,
    reset: []const u8,
    writer: anytype,
) !void {
    var pos: usize = 0;

    while (pos < linha.len) {
        if (buscarPadrao(linha[pos..], padrao, case_insensitive)) |offset| {
            // Texto antes do match
            try writer.print("{s}", .{linha[pos .. pos + offset]});
            // Match em destaque
            try writer.print("{s}{s}{s}", .{ cor, linha[pos + offset .. pos + offset + padrao.len], reset });
            pos += offset + padrao.len;
        } else {
            // Resto da linha
            try writer.print("{s}", .{linha[pos..]});
            break;
        }
    }
}
```

## Passo 4: Busca Recursiva

```zig
/// Busca recursivamente em um diretório.
fn buscarRecursivo(
    dir_path: []const u8,
    padrao: []const u8,
    opcoes: *const OpcoesBusca,
    writer: anytype,
    resultados: []ResultadoArquivo,
    num_resultados: *usize,
) !void {
    var dir = fs.cwd().openDir(dir_path, .{ .iterate = true }) catch return;
    defer dir.close();

    var iter = dir.iterate();
    while (try iter.next()) |entry| {
        var path_buf: [1024]u8 = undefined;
        const full_path = fmt.bufPrint(&path_buf, "{s}/{s}", .{ dir_path, entry.name }) catch continue;

        switch (entry.kind) {
            .file => {
                // Ignora arquivos binários (heurística: verificar extensão)
                if (mem.endsWith(u8, entry.name, ".o") or
                    mem.endsWith(u8, entry.name, ".exe") or
                    mem.endsWith(u8, entry.name, ".bin") or
                    mem.endsWith(u8, entry.name, ".png") or
                    mem.endsWith(u8, entry.name, ".jpg"))
                {
                    continue;
                }

                if (num_resultados.* < resultados.len) {
                    resultados[num_resultados.*] = try buscarEmArquivo(
                        full_path, padrao, opcoes, writer,
                    );
                    if (resultados[num_resultados.*].matches > 0) {
                        num_resultados.* += 1;
                    }
                }
            },
            .directory => {
                // Ignora diretórios ocultos e de build
                if (entry.name[0] == '.' or
                    mem.eql(u8, entry.name, "zig-out") or
                    mem.eql(u8, entry.name, "zig-cache") or
                    mem.eql(u8, entry.name, "node_modules"))
                {
                    continue;
                }
                try buscarRecursivo(full_path, padrao, opcoes, writer, resultados, num_resultados);
            },
            else => {},
        }
    }
}
```

## Passo 5: Interface CLI

```zig
pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    try stdout.print(
        \\
        \\  ==========================================
        \\       MINI GREP - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [1024]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Buscar em arquivo
            \\  [2] Buscar recursivo em diretorio
            \\  [3] Contar ocorrencias
            \\  [4] Sair
            \\
            \\  Opcao:
        , .{});

        const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, opcao_raw, " \t\r\n");

        if (mem.eql(u8, opcao, "4")) break;

        try stdout.print("\n  Padrao de busca: ", .{});
        const padrao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
        const padrao = mem.trim(u8, padrao_raw, " \t\r\n");
        if (padrao.len == 0) continue;

        // Copiar padrão para buffer persistente
        var padrao_buf: [256]u8 = undefined;
        const padrao_len = @min(padrao.len, padrao_buf.len);
        @memcpy(padrao_buf[0..padrao_len], padrao[0..padrao_len]);
        const padrao_fixo = padrao_buf[0..padrao_len];

        try stdout.print("  Case insensitive? (s/n): ", .{});
        const ci_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch "n" orelse "n";
        const ci = mem.eql(u8, mem.trim(u8, ci_raw, " \t\r\n"), "s");

        var opcoes = OpcoesBusca{
            .case_insensitive = ci,
            .contar_apenas = mem.eql(u8, opcao, "3"),
        };

        if (mem.eql(u8, opcao, "2")) {
            opcoes.recursivo = true;
            try stdout.print("  Diretorio: ", .{});
            const dir_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const dir_path = mem.trim(u8, dir_raw, " \t\r\n");

            var resultados: [100]ResultadoArquivo = undefined;
            var num_resultados: usize = 0;

            try stdout.print("\n", .{});
            try buscarRecursivo(dir_path, padrao_fixo, &opcoes, stdout, &resultados, &num_resultados);

            // Resumo
            var total_matches: usize = 0;
            for (resultados[0..num_resultados]) |r| total_matches += r.matches;

            try stdout.print(
                \\
                \\  --- Resumo ---
                \\  Arquivos com match: {d}
                \\  Total de matches:   {d}
                \\
            , .{ num_resultados, total_matches });
        } else {
            try stdout.print("  Arquivo: ", .{});
            const path_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const path = mem.trim(u8, path_raw, " \t\r\n");

            try stdout.print("\n", .{});
            const resultado = try buscarEmArquivo(path, padrao_fixo, &opcoes, stdout);

            if (opcoes.contar_apenas) {
                try stdout.print("  {d} ocorrencias em {d} linhas\n", .{
                    resultado.matches, resultado.linhas_com_match,
                });
            }
        }
    }

    try stdout.print("\n  Ate logo!\n", .{});
}
```

## Testes

```zig
test "buscar padrao simples" {
    try std.testing.expectEqual(@as(?usize, 0), buscarPadrao("hello world", "hello", false));
    try std.testing.expectEqual(@as(?usize, 6), buscarPadrao("hello world", "world", false));
    try std.testing.expect(buscarPadrao("hello", "xyz", false) == null);
}

test "buscar case insensitive" {
    try std.testing.expectEqual(@as(?usize, 0), buscarPadrao("Hello World", "hello", true));
    try std.testing.expect(buscarPadrao("Hello World", "hello", false) == null);
}

test "contar ocorrencias" {
    try std.testing.expectEqual(@as(usize, 3), contarOcorrencias("aaa", "a", false));
    try std.testing.expectEqual(@as(usize, 2), contarOcorrencias("abab", "ab", false));
    try std.testing.expectEqual(@as(usize, 0), contarOcorrencias("hello", "xyz", false));
}

test "buscar padrao vazio" {
    try std.testing.expect(buscarPadrao("hello", "", false) == null);
}

test "buscar padrao maior que linha" {
    try std.testing.expect(buscarPadrao("hi", "hello world", false) == null);
}
```

## Compilando e Executando

```bash
zig build test
zig build run
```

## Conceitos Aprendidos

- Busca de padrões em texto (string matching)
- Iteração recursiva de diretórios
- Cores ANSI para highlight no terminal
- Leitura linha a linha de arquivos
- Filtragem de arquivos binários
- Structs de configuração para opções de busca

## Próximos Passos

- Evolua para suportar [expressões regulares](/projetos/regex-engine/)
- Explore a [documentação std.fs](/stdlib/fs/) para operações de filesystem
- Construa o próximo projeto: [Compressor RLE](/projetos/compressor-rle/)
