Contador de Palavras em Zig — Tutorial Passo a Passo

Contador de Palavras em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um contador de palavras inspirado no utilitário Unix wc. Este projeto é ideal para aprender sobre leitura de arquivos, processamento de streams e argumentos de linha de comando em Zig.

O Que Vamos Construir

Nosso contador vai:

  • Contar linhas, palavras e caracteres de arquivos
  • Aceitar múltiplos arquivos como argumento
  • Ler da entrada padrão (stdin) quando nenhum arquivo for passado
  • Exibir totais quando múltiplos arquivos forem processados
  • Suportar flags: -l (linhas), -w (palavras), -c (caracteres)

Por Que Este Projeto?

O wc é um utilitário Unix clássico que ilustra perfeitamente o processamento de streams — lemos dados byte a byte e mantemos contadores. Em Zig, isso nos permite explorar a API de I/O buffered, argumentos de processo e o padrão de processar arquivos como streams.

Passo 1: Estrutura de Contagem

const std = @import("std");
const fs = std.fs;
const io = std.io;
const mem = std.mem;
const process = std.process;

/// Resultado da contagem para um arquivo.
/// Struct simples e imutável — representa um valor, não um estado.
const Contagem = struct {
    linhas: usize = 0,
    palavras: usize = 0,
    bytes: usize = 0,
    nome: []const u8 = "",

    /// Soma duas contagens (útil para o total).
    pub fn somar(a: Contagem, b: Contagem) Contagem {
        return .{
            .linhas = a.linhas + b.linhas,
            .palavras = a.palavras + b.palavras,
            .bytes = a.bytes + b.bytes,
            .nome = "total",
        };
    }
};

/// Flags de exibição.
const Flags = struct {
    linhas: bool = false,
    palavras: bool = false,
    bytes: bool = false,

    /// Se nenhuma flag foi especificada, mostra tudo.
    pub fn mostrarTudo(self: Flags) bool {
        return !self.linhas and !self.palavras and !self.bytes;
    }
};

Passo 2: Algoritmo de Contagem

/// Conta linhas, palavras e bytes de um reader genérico.
///
/// Decisão de design: a função aceita anytype (reader genérico)
/// em vez de um tipo específico de arquivo. Isso permite usá-la
/// tanto com arquivos quanto com stdin, sem duplicar código.
/// É o equivalente Zig de uma interface/trait — duck typing em
/// tempo de compilação.
fn contar(reader: anytype) Contagem {
    var resultado = Contagem{};
    var em_palavra = false;

    // Processamos byte a byte. Para arquivos muito grandes,
    // isso é eficiente porque o reader já faz buffering interno.
    while (reader.readByte()) |byte| {
        resultado.bytes += 1;

        if (byte == '\n') {
            resultado.linhas += 1;
        }

        const eh_espaco = switch (byte) {
            ' ', '\t', '\n', '\r' => true,
            else => false,
        };

        if (eh_espaco) {
            em_palavra = false;
        } else if (!em_palavra) {
            em_palavra = true;
            resultado.palavras += 1;
        }
    } else |_| {} // EOF ou erro — terminamos

    return resultado;
}

/// Conta palavras de um arquivo específico.
fn contarArquivo(caminho: []const u8) !Contagem {
    const arquivo = try fs.cwd().openFile(caminho, .{});
    defer arquivo.close();

    var buf_reader = io.bufferedReader(arquivo.reader());
    var resultado = contar(buf_reader.reader());
    resultado.nome = caminho;
    return resultado;
}

Por que anytype? Em linguagens OOP, usaríamos uma interface Reader. Em Zig, anytype com duck typing em comptime serve o mesmo propósito: qualquer tipo que tenha um método readByte() !u8 funciona. O compilador gera código especializado para cada tipo concreto, sem overhead de vtable. Veja mais em generics em Zig.

Passo 3: Formatação de Saída

/// Exibe uma contagem formatada.
fn exibirContagem(writer: anytype, contagem: Contagem, flags: Flags) !void {
    const tudo = flags.mostrarTudo();

    if (tudo or flags.linhas) {
        try writer.print("{d:>8}", .{contagem.linhas});
    }
    if (tudo or flags.palavras) {
        try writer.print("{d:>8}", .{contagem.palavras});
    }
    if (tudo or flags.bytes) {
        try writer.print("{d:>8}", .{contagem.bytes});
    }

    if (contagem.nome.len > 0) {
        try writer.print(" {s}", .{contagem.nome});
    }
    try writer.print("\n", .{});
}

Passo 4: Parsing de Argumentos

/// Processa argumentos da linha de comando.
/// Retorna as flags e a lista de arquivos.
fn processarArgumentos(allocator: std.mem.Allocator) !struct {
    flags: Flags,
    arquivos: std.ArrayList([]const u8),
} {
    var flags = Flags{};
    var arquivos = std.ArrayList([]const u8).init(allocator);

    var args = try process.argsWithAllocator(allocator);
    defer args.deinit();

    _ = args.next(); // Pula o nome do programa

    while (args.next()) |arg| {
        if (arg.len > 0 and arg[0] == '-') {
            // Parse de flags
            for (arg[1..]) |c| {
                switch (c) {
                    'l' => flags.linhas = true,
                    'w' => flags.palavras = true,
                    'c' => flags.bytes = true,
                    else => {
                        std.debug.print("Flag desconhecida: -{c}\n", .{c});
                    },
                }
            }
        } else {
            try arquivos.append(arg);
        }
    }

    return .{ .flags = flags, .arquivos = arquivos };
}

Passo 5: Função Principal

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const stdout = io.getStdOut().writer();

    var resultado = try processarArgumentos(allocator);
    defer resultado.arquivos.deinit();

    const flags = resultado.flags;
    const arquivos = resultado.arquivos.items;

    if (arquivos.len == 0) {
        // Sem arquivos — lê da stdin
        var buf_reader = io.bufferedReader(io.getStdIn().reader());
        const contagem = contar(buf_reader.reader());
        try exibirContagem(stdout, contagem, flags);
        return;
    }

    var total = Contagem{};

    for (arquivos) |caminho| {
        const contagem = contarArquivo(caminho) catch |err| {
            try stdout.print("wc: {s}: {}\n", .{ caminho, err });
            continue;
        };
        try exibirContagem(stdout, contagem, flags);
        total = total.somar(contagem);
    }

    // Exibe total se houver mais de um arquivo
    if (arquivos.len > 1) {
        try exibirContagem(stdout, total, flags);
    }
}

Passo 6: Testes

test "contar texto simples" {
    const texto = "Olá mundo\nSegunda linha\n";
    var stream = std.io.fixedBufferStream(texto);
    const resultado = contar(stream.reader());

    try std.testing.expectEqual(@as(usize, 2), resultado.linhas);
    try std.testing.expectEqual(@as(usize, 4), resultado.palavras);
    try std.testing.expectEqual(@as(usize, texto.len), resultado.bytes);
}

test "contar texto vazio" {
    const texto = "";
    var stream = std.io.fixedBufferStream(texto);
    const resultado = contar(stream.reader());

    try std.testing.expectEqual(@as(usize, 0), resultado.linhas);
    try std.testing.expectEqual(@as(usize, 0), resultado.palavras);
}

test "contar múltiplos espaços" {
    const texto = "a  b   c\n";
    var stream = std.io.fixedBufferStream(texto);
    const resultado = contar(stream.reader());

    try std.testing.expectEqual(@as(usize, 3), resultado.palavras);
}

test "somar contagens" {
    const a = Contagem{ .linhas = 10, .palavras = 50, .bytes = 200 };
    const b = Contagem{ .linhas = 5, .palavras = 30, .bytes = 100 };
    const total = a.somar(b);

    try std.testing.expectEqual(@as(usize, 15), total.linhas);
    try std.testing.expectEqual(@as(usize, 80), total.palavras);
    try std.testing.expectEqual(@as(usize, 300), total.bytes);
}

Compilando e Executando

zig build

# Contar palavras de um arquivo
./zig-out/bin/contador-palavras arquivo.txt

# Contar apenas linhas
./zig-out/bin/contador-palavras -l arquivo.txt

# Múltiplos arquivos
./zig-out/bin/contador-palavras arquivo1.txt arquivo2.txt

# Ler da stdin
echo "Olá mundo" | ./zig-out/bin/contador-palavras

Conceitos Aprendidos

  • Leitura de arquivos com std.fs e buffered readers
  • Processamento de streams byte a byte
  • Argumentos de linha de comando com std.process
  • Generics com anytype para duck typing
  • Composição de resultados com funções puras

Próximos Passos

Continue aprendendo Zig

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