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.fse buffered readers - Processamento de streams byte a byte
- Argumentos de linha de comando com
std.process - Generics com
anytypepara duck typing - Composição de resultados com funções puras
Próximos Passos
- Aprenda sobre I/O de arquivos para operações avançadas
- Explore argumentos CLI para parsing mais sofisticado
- Construa o próximo projeto: Validador de CPF