INI Parser em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um parser de arquivos INI completo em Zig. Arquivos INI são um dos formatos de configuração mais simples e amplamente usados. Nosso parser vai ler, modificar e salvar configurações com suporte a seções, comentários e tipos de dados.
O Que Vamos Construir
Nosso INI parser vai:
- Parsear arquivos INI com seções
[nome], chaveschave=valore comentários; ... - Suportar valores string, inteiro, float e boolean
- Permitir leitura e escrita de configurações
- Preservar comentários ao salvar
- Fornecer API ergonômica com getters tipados
- Funcionar como biblioteca e como ferramenta CLI
Por Que Este Projeto?
Parsear formatos de configuração é uma habilidade essencial. O INI é simples o suficiente para um tutorial mas complexo o bastante para ensinar parsing de texto, gerenciamento de memória com HashMap e API design em Zig.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com strings e slices
- Conhecimento de HashMap
Passo 1: Estrutura do Projeto
mkdir ini-parser
cd ini-parser
zig init
Passo 2: Estrutura do Parser
const std = @import("std");
const fs = std.fs;
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
/// Representa um valor em um arquivo INI.
/// Armazena tanto o valor raw (string) quanto permite
/// conversão tipada para inteiros, floats e booleans.
const IniValue = struct {
raw: []const u8,
comentario: ?[]const u8, // comentário inline, se houver
pub fn asString(self: IniValue) []const u8 {
return self.raw;
}
pub fn asInt(self: IniValue) ?i64 {
return fmt.parseInt(i64, self.raw, 10) catch null;
}
pub fn asFloat(self: IniValue) ?f64 {
return fmt.parseFloat(f64, self.raw) catch null;
}
pub fn asBool(self: IniValue) ?bool {
if (mem.eql(u8, self.raw, "true") or mem.eql(u8, self.raw, "yes") or
mem.eql(u8, self.raw, "1") or mem.eql(u8, self.raw, "on"))
return true;
if (mem.eql(u8, self.raw, "false") or mem.eql(u8, self.raw, "no") or
mem.eql(u8, self.raw, "0") or mem.eql(u8, self.raw, "off"))
return false;
return null;
}
};
/// Uma seção do arquivo INI, contendo pares chave-valor.
const IniSection = struct {
valores: std.StringHashMap(IniValue),
ordem: std.ArrayList([]const u8), // preserva ordem de inserção
pub fn init(allocator: mem.Allocator) IniSection {
return .{
.valores = std.StringHashMap(IniValue).init(allocator),
.ordem = std.ArrayList([]const u8).init(allocator),
};
}
pub fn deinit(self: *IniSection, allocator: mem.Allocator) void {
var it = self.valores.keyIterator();
while (it.next()) |key| {
allocator.free(key.*);
}
var vit = self.valores.valueIterator();
while (vit.next()) |val| {
allocator.free(val.raw);
if (val.comentario) |c| allocator.free(c);
}
self.valores.deinit();
self.ordem.deinit();
}
};
/// Parser e armazenamento de configurações INI.
const IniConfig = struct {
secoes: std.StringHashMap(IniSection),
ordem_secoes: std.ArrayList([]const u8),
comentarios_globais: std.ArrayList([]const u8),
allocator: mem.Allocator,
const Self = @This();
pub fn init(allocator: mem.Allocator) Self {
return .{
.secoes = std.StringHashMap(IniSection).init(allocator),
.ordem_secoes = std.ArrayList([]const u8).init(allocator),
.comentarios_globais = std.ArrayList([]const u8).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
var it = self.secoes.iterator();
while (it.next()) |entry| {
self.allocator.free(entry.key_ptr.*);
entry.value_ptr.deinit(self.allocator);
}
self.secoes.deinit();
self.ordem_secoes.deinit();
for (self.comentarios_globais.items) |c| self.allocator.free(c);
self.comentarios_globais.deinit();
}
/// Parseia o conteúdo de um arquivo INI.
pub fn parsear(self: *Self, conteudo: []const u8) !void {
var secao_atual: []const u8 = "default";
// Garantir que a seção default existe
if (!self.secoes.contains("default")) {
const nome = try self.allocator.dupe(u8, "default");
try self.secoes.put(nome, IniSection.init(self.allocator));
try self.ordem_secoes.append(nome);
}
var linhas = mem.splitScalar(u8, conteudo, '\n');
while (linhas.next()) |linha_raw| {
const linha = mem.trim(u8, linha_raw, " \t\r");
// Linha vazia
if (linha.len == 0) continue;
// Comentário
if (linha[0] == ';' or linha[0] == '#') {
const comentario = try self.allocator.dupe(u8, linha);
try self.comentarios_globais.append(comentario);
continue;
}
// Seção: [nome]
if (linha[0] == '[') {
if (mem.indexOf(u8, linha, "]")) |fim| {
const nome_secao = linha[1..fim];
secao_atual = nome_secao;
if (!self.secoes.contains(nome_secao)) {
const nome = try self.allocator.dupe(u8, nome_secao);
try self.secoes.put(nome, IniSection.init(self.allocator));
try self.ordem_secoes.append(nome);
secao_atual = nome;
}
}
continue;
}
// Chave = Valor
if (mem.indexOf(u8, linha, "=")) |pos_igual| {
const chave = mem.trim(u8, linha[0..pos_igual], " \t");
var valor_str = mem.trim(u8, linha[pos_igual + 1 ..], " \t");
// Verificar comentário inline
var comentario_inline: ?[]const u8 = null;
if (mem.indexOf(u8, valor_str, " ;")) |pos_com| {
comentario_inline = try self.allocator.dupe(u8, valor_str[pos_com + 1 ..]);
valor_str = mem.trim(u8, valor_str[0..pos_com], " \t");
}
// Remover aspas
if (valor_str.len >= 2 and valor_str[0] == '"' and valor_str[valor_str.len - 1] == '"') {
valor_str = valor_str[1 .. valor_str.len - 1];
}
if (self.secoes.getPtr(secao_atual)) |secao| {
const chave_copia = try self.allocator.dupe(u8, chave);
const valor_copia = try self.allocator.dupe(u8, valor_str);
try secao.valores.put(chave_copia, .{
.raw = valor_copia,
.comentario = comentario_inline,
});
try secao.ordem.append(chave_copia);
}
}
}
}
/// Carrega um arquivo INI do disco.
pub fn carregarArquivo(self: *Self, caminho: []const u8) !void {
const conteudo = try fs.cwd().readFileAlloc(self.allocator, caminho, 10 * 1024 * 1024);
defer self.allocator.free(conteudo);
try self.parsear(conteudo);
}
/// Obtém um valor de uma seção.
pub fn get(self: *const Self, secao: []const u8, chave: []const u8) ?IniValue {
if (self.secoes.get(secao)) |sec| {
return sec.valores.get(chave);
}
return null;
}
/// Obtém um valor como string com fallback padrão.
pub fn getString(self: *const Self, secao: []const u8, chave: []const u8, padrao: []const u8) []const u8 {
if (self.get(secao, chave)) |val| return val.asString();
return padrao;
}
/// Obtém um valor como inteiro com fallback padrão.
pub fn getInt(self: *const Self, secao: []const u8, chave: []const u8, padrao: i64) i64 {
if (self.get(secao, chave)) |val| {
if (val.asInt()) |i| return i;
}
return padrao;
}
/// Obtém um valor como boolean com fallback padrão.
pub fn getBool(self: *const Self, secao: []const u8, chave: []const u8, padrao: bool) bool {
if (self.get(secao, chave)) |val| {
if (val.asBool()) |b| return b;
}
return padrao;
}
/// Serializa a configuração de volta para formato INI.
pub fn serializar(self: *const Self, writer: anytype) !void {
// Comentários globais
for (self.comentarios_globais.items) |c| {
try writer.print("{s}\n", .{c});
}
for (self.ordem_secoes.items) |nome_secao| {
if (self.secoes.get(nome_secao)) |secao| {
if (!mem.eql(u8, nome_secao, "default")) {
try writer.print("\n[{s}]\n", .{nome_secao});
}
for (secao.ordem.items) |chave| {
if (secao.valores.get(chave)) |valor| {
try writer.print("{s} = {s}", .{ chave, valor.raw });
if (valor.comentario) |com| {
try writer.print(" {s}", .{com});
}
try writer.print("\n", .{});
}
}
}
}
}
/// Salva a configuração em um arquivo.
pub fn salvarArquivo(self: *const Self, caminho: []const u8) !void {
const arquivo = try fs.cwd().createFile(caminho, .{});
defer arquivo.close();
try self.serializar(arquivo.writer());
}
/// Lista todas as seções.
pub fn listSecoes(self: *const Self) []const []const u8 {
return self.ordem_secoes.items;
}
};
Passo 3: Função Main com CLI
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const stdout = io.getStdOut().writer();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
var config = IniConfig.init(allocator);
defer config.deinit();
if (args.len >= 2 and !mem.startsWith(u8, args[1], "--")) {
// Carregar arquivo
config.carregarArquivo(args[1]) catch |err| {
try stdout.print("Erro ao carregar '{s}': {}\n", .{ args[1], err });
return;
};
try stdout.print("Arquivo '{s}' carregado.\n\n", .{args[1]});
} else {
// Demonstração com INI de exemplo
const exemplo =
\\; Configuração do servidor web
\\; Gerado pelo INI Parser em Zig
\\
\\[servidor]
\\host = 0.0.0.0
\\porta = 8080
\\workers = 4
\\debug = false
\\
\\[banco_de_dados]
\\driver = postgresql
\\host = localhost
\\porta = 5432
\\nome = meu_app
\\usuario = admin
\\max_conexoes = 20
\\
\\[log]
\\nivel = info ; niveis: debug, info, warn, error
\\arquivo = /var/log/app.log
\\rotacao = true
\\max_tamanho_mb = 100
;
try config.parsear(exemplo);
try stdout.print(" Configuracao de exemplo carregada.\n\n", .{});
}
// Exibir configuração parseada
try stdout.print(" ==========================================\n", .{});
try stdout.print(" INI PARSER - Zig\n", .{});
try stdout.print(" ==========================================\n\n", .{});
for (config.listSecoes()) |secao| {
try stdout.print(" [{s}]\n", .{secao});
if (config.secoes.get(secao)) |sec| {
for (sec.ordem.items) |chave| {
if (sec.valores.get(chave)) |valor| {
try stdout.print(" {s} = {s}", .{ chave, valor.raw });
// Detectar tipo
if (valor.asInt() != null) {
try stdout.print(" (int)", .{});
} else if (valor.asBool() != null) {
try stdout.print(" (bool)", .{});
} else {
try stdout.print(" (string)", .{});
}
try stdout.print("\n", .{});
}
}
}
try stdout.print("\n", .{});
}
// Demonstrar API tipada
try stdout.print(" --- API Tipada ---\n", .{});
try stdout.print(" servidor.porta (int): {d}\n", .{config.getInt("servidor", "porta", 3000)});
try stdout.print(" servidor.debug (bool): {}\n", .{config.getBool("servidor", "debug", false)});
try stdout.print(" banco_de_dados.driver: {s}\n", .{config.getString("banco_de_dados", "driver", "sqlite")});
try stdout.print(" log.nivel: {s}\n", .{config.getString("log", "nivel", "warn")});
// Serializar de volta
try stdout.print("\n --- Serializado ---\n", .{});
try config.serializar(stdout);
}
Testes
test "parsear seção e chave simples" {
const allocator = std.testing.allocator;
var config = IniConfig.init(allocator);
defer config.deinit();
try config.parsear("[teste]\nchave = valor\n");
const val = config.get("teste", "chave");
try std.testing.expect(val != null);
try std.testing.expectEqualStrings("valor", val.?.asString());
}
test "tipos de valor" {
const allocator = std.testing.allocator;
var config = IniConfig.init(allocator);
defer config.deinit();
try config.parsear(
\\[tipos]
\\inteiro = 42
\\decimal = 3.14
\\booleano = true
\\texto = hello world
);
try std.testing.expectEqual(@as(i64, 42), config.getInt("tipos", "inteiro", 0));
try std.testing.expect(config.getBool("tipos", "booleano", false));
try std.testing.expectEqualStrings("hello world", config.getString("tipos", "texto", ""));
}
test "fallback padrao" {
const allocator = std.testing.allocator;
var config = IniConfig.init(allocator);
defer config.deinit();
try config.parsear("[s]\nk = v\n");
try std.testing.expectEqual(@as(i64, 99), config.getInt("s", "inexistente", 99));
try std.testing.expectEqualStrings("padrao", config.getString("s", "inexistente", "padrao"));
}
test "IniValue conversoes" {
const val = IniValue{ .raw = "true", .comentario = null };
try std.testing.expect(val.asBool().? == true);
const num = IniValue{ .raw = "42", .comentario = null };
try std.testing.expectEqual(@as(i64, 42), num.asInt().?);
}
Compilando e Executando
# Executar com dados de demonstração
zig build run
# Parsear um arquivo INI existente
zig build run -- config.ini
# Rodar testes
zig build test
Conceitos Aprendidos
- Parsing de texto linha por linha com máquina de estados
- HashMap aninhado (seções contendo HashMaps de valores)
- API tipada com conversão segura e fallbacks
- Serialização preservando formato e comentários
- Gerenciamento de memória com ownership de strings alocadas
Próximos Passos
- Explore manipulação de strings para parsing
- Veja o JSON Config Parser para outro formato
- Construa o Cron Parser para parsing de expressões
- Consulte a stdlib de arquivos para leitura/escrita