Mini Grep em Zig — Tutorial Passo a Passo

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

Passo 1: Estrutura do Projeto

mkdir mini-grep
cd mini-grep
zig init

Passo 2: Opções de Busca e Matching

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

/// 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

/// 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

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

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

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.