Operações de entrada e saída com arquivos estão entre as tarefas mais fundamentais em qualquer linguagem de programação de sistemas. Em Zig lang, a biblioteca padrão oferece uma API poderosa e segura para trabalhar com o sistema de arquivos através do módulo std.fs. Ao contrário de C, onde fopen e fread exigem cuidado constante com ponteiros e buffers, a linguagem Zig fornece abstrações que previnem erros comuns sem sacrificar o controle de baixo nível.
Neste tutorial, vamos explorar todas as operações essenciais de File I/O em Zig: desde a leitura e escrita básica de arquivos até navegação em diretórios, manipulação de caminhos e um exemplo prático completo de parser de logs.
Abrindo e Fechando Arquivos com std.fs
O ponto de partida para qualquer operação de arquivo em Zig é o módulo std.fs. A abertura de arquivos retorna um std.fs.File, e a prática idiomática é usar defer para garantir que o arquivo seja fechado ao sair do escopo.
const std = @import("std");
pub fn main() !void {
// Abrir um arquivo existente para leitura
const file = try std.fs.cwd().openFile("dados.txt", .{});
defer file.close();
// O arquivo está pronto para leitura
// defer garante que file.close() será chamado
// mesmo se ocorrer um erro nas operações seguintes
const stat = try file.stat();
std.debug.print("Tamanho: {} bytes\n", .{stat.size});
std.debug.print("Última modificação: {}\n", .{stat.mtime});
}
As opções de abertura permitem controle fino sobre o modo de acesso:
// Abrir para leitura e escrita
const file_rw = try std.fs.cwd().openFile("config.txt", .{
.mode = .read_write,
});
defer file_rw.close();
// Abrir em um caminho absoluto
const abs_file = try std.fs.openFileAbsolute("/etc/hostname", .{});
defer abs_file.close();
Leitura de Arquivos: readToEndAlloc e Buffered Reader
Zig oferece duas abordagens principais para leitura: carregar o arquivo inteiro na memória ou ler linha por linha com um buffered reader.
Leitura Completa com readToEndAlloc
Para arquivos de tamanho moderado, readToEndAlloc é a forma mais direta (o conteudo retornado e um slice de bytes):
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const file = try std.fs.cwd().openFile("poema.txt", .{});
defer file.close();
// Lê o arquivo inteiro — máximo de 10 MB
const conteudo = try file.readToEndAlloc(allocator, 10 * 1024 * 1024);
defer allocator.free(conteudo);
std.debug.print("Conteúdo ({} bytes):\n{s}\n", .{ conteudo.len, conteudo });
}
Leitura Linha por Linha com Buffered Reader
Para arquivos grandes ou quando você precisa processar uma linha por vez, o buffered reader é mais eficiente em memória:
const std = @import("std");
pub fn main() !void {
const file = try std.fs.cwd().openFile("access.log", .{});
defer file.close();
// Criar um buffered reader para leitura eficiente
var buf_reader = std.io.bufferedReader(file.reader());
const reader = buf_reader.reader();
var line_buf: [4096]u8 = undefined;
var line_count: usize = 0;
// Ler linha por linha
while (reader.readUntilDelimiterOrEof(&line_buf, '\n')) |line| {
if (line) |data| {
line_count += 1;
std.debug.print("Linha {}: {s}\n", .{ line_count, data });
} else {
break; // EOF
}
} else |err| {
std.debug.print("Erro de leitura: {}\n", .{err});
}
std.debug.print("\nTotal: {} linhas\n", .{line_count});
}
Leitura com Alocação por Linha
Quando as linhas podem ter tamanho variável e você precisa armazená-las:
const std = @import("std");
pub fn lerLinhas(allocator: std.mem.Allocator, path: []const u8) !std.ArrayList([]u8) {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
var buf_reader = std.io.bufferedReader(file.reader());
const reader = buf_reader.reader();
var linhas = std.ArrayList([]u8).init(allocator);
errdefer {
for (linhas.items) |linha| allocator.free(linha);
linhas.deinit();
}
while (true) {
const linha = reader.readUntilDelimiterAlloc(allocator, '\n', 8192) catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
try linhas.append(linha);
}
return linhas;
}
Escrita de Arquivos: createFile e Writer Interface
Para criar ou sobrescrever arquivos, usamos createFile. Para adicionar conteúdo ao final, abrimos com .mode = .read_write e fazemos seek.
const std = @import("std");
pub fn main() !void {
// Criar (ou sobrescrever) um arquivo
const file = try std.fs.cwd().createFile("saida.txt", .{});
defer file.close();
const writer = file.writer();
// Escrita formatada — similar a printf
try writer.print("Relatório gerado em: {}\n", .{std.time.timestamp()});
try writer.print("Versão: {s}\n", .{"1.0.0"});
// Escrever bytes diretamente
try writer.writeAll("---\nDados do sistema:\n");
// Escrever múltiplas vezes em um loop
for (0..5) |i| {
try writer.print(" Item {}: valor_{}\n", .{ i + 1, i * 10 });
}
std.debug.print("Arquivo 'saida.txt' criado com sucesso!\n", .{});
}
Buffered Writer para Performance
Quando você faz muitas escritas pequenas, o buffered writer reduz as chamadas de sistema:
const std = @import("std");
pub fn gerarCSV(path: []const u8, dados: []const [3][]const u8) !void {
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
// Buffered writer acumula dados antes de escrever no disco
var buf_writer = std.io.bufferedWriter(file.writer());
const writer = buf_writer.writer();
// Cabeçalho
try writer.writeAll("nome,idade,cidade\n");
// Dados
for (dados) |row| {
try writer.print("{s},{s},{s}\n", .{ row[0], row[1], row[2] });
}
// IMPORTANTE: flush() envia os dados pendentes para o disco
try buf_writer.flush();
}
pub fn main() !void {
const dados = [_][3][]const u8{
.{ "Ana", "28", "São Paulo" },
.{ "Carlos", "35", "Rio de Janeiro" },
.{ "Maria", "42", "Belo Horizonte" },
};
try gerarCSV("usuarios.csv", &dados);
std.debug.print("CSV gerado com sucesso!\n", .{});
}
Metadados de Arquivos: stat, Permissões e Timestamps
O método stat() retorna informações detalhadas sobre um arquivo:
const std = @import("std");
pub fn infoArquivo(path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const stat = try file.stat();
std.debug.print("Arquivo: {s}\n", .{path});
std.debug.print(" Tamanho: {} bytes\n", .{stat.size});
std.debug.print(" Tipo: {}\n", .{stat.kind});
// Timestamps em nanosegundos desde epoch
const mtime_s = @divFloor(stat.mtime, std.time.ns_per_s);
std.debug.print(" Modificado: {} (epoch seconds)\n", .{mtime_s});
// Verificar permissões (em sistemas POSIX)
const mode = stat.mode;
const owner_read = (mode & 0o400) != 0;
const owner_write = (mode & 0o200) != 0;
const owner_exec = (mode & 0o100) != 0;
std.debug.print(" Permissões do dono: {s}{s}{s}\n", .{
if (owner_read) "r" else "-",
if (owner_write) "w" else "-",
if (owner_exec) "x" else "-",
});
}
Operações com Diretórios
Zig fornece APIs completas para criar, listar e percorrer diretórios.
Listando Arquivos em um Diretório
const std = @import("std");
pub fn listarDiretorio(path: []const u8) !void {
var dir = try std.fs.cwd().openDir(path, .{ .iterate = true });
defer dir.close();
std.debug.print("Conteúdo de '{s}':\n", .{path});
var iter = dir.iterate();
while (try iter.next()) |entry| {
const tipo = switch (entry.kind) {
.file => "ARQ",
.directory => "DIR",
.sym_link => "LNK",
else => "???",
};
std.debug.print(" [{s}] {s}\n", .{ tipo, entry.name });
}
}
pub fn main() !void {
try listarDiretorio(".");
}
Criando Diretórios e Percorrendo Árvores
const std = @import("std");
pub fn criarEstrutura(allocator: std.mem.Allocator) !void {
const cwd = std.fs.cwd();
// Criar diretório simples
cwd.makeDir("projeto") catch |err| switch (err) {
error.PathAlreadyExists => {},
else => return err,
};
// Criar diretórios aninhados (equivalente a mkdir -p)
try cwd.makePath("projeto/src/utils");
try cwd.makePath("projeto/tests");
try cwd.makePath("projeto/docs");
std.debug.print("Estrutura criada!\n", .{});
// Percorrer a árvore recursivamente
var walker = try cwd.openDir("projeto", .{ .iterate = true });
defer walker.close();
var iter = walker.iterate();
while (try iter.next()) |entry| {
std.debug.print(" {s} ({s})\n", .{
entry.name,
@tagName(entry.kind),
});
}
_ = allocator;
}
Manipulação de Caminhos com std.fs.path
O módulo std.fs.path oferece funções para manipular caminhos de forma portável:
const std = @import("std");
pub fn main() void {
const caminho = "/home/usuario/projetos/app/src/main.zig";
// Extrair componentes do caminho
std.debug.print("Diretório: {s}\n", .{std.fs.path.dirname(caminho) orelse "(nenhum)"});
std.debug.print("Nome do arquivo: {s}\n", .{std.fs.path.basename(caminho)});
std.debug.print("Extensão: {s}\n", .{std.fs.path.extension(caminho)});
// Verificar se é absoluto
std.debug.print("É absoluto: {}\n", .{std.fs.path.isAbsolute(caminho)});
// Juntar caminhos
const base = "/home/usuario";
const relativo = "documentos/notas.txt";
var buf: [std.fs.max_path_bytes]u8 = undefined;
const junto = std.fmt.bufPrint(&buf, "{s}/{s}", .{ base, relativo }) catch unreachable;
std.debug.print("Caminho completo: {s}\n", .{junto});
}
Exemplo Prático: Parser de Arquivo de Log
Vamos construir um parser completo que lê um arquivo de log, extrai informações e gera um relatório:
const std = @import("std");
const LogEntry = struct {
level: Level,
message: []const u8,
count: usize,
const Level = enum { INFO, WARN, ERROR, DEBUG };
};
const LogStats = struct {
total: usize = 0,
info_count: usize = 0,
warn_count: usize = 0,
error_count: usize = 0,
debug_count: usize = 0,
error_messages: std.ArrayList([]const u8),
pub fn init(allocator: std.mem.Allocator) LogStats {
return .{
.error_messages = std.ArrayList([]const u8).init(allocator),
};
}
pub fn deinit(self: *LogStats) void {
self.error_messages.deinit();
}
pub fn registrar(self: *LogStats, line: []const u8) !void {
self.total += 1;
if (std.mem.startsWith(u8, line, "[INFO]")) {
self.info_count += 1;
} else if (std.mem.startsWith(u8, line, "[WARN]")) {
self.warn_count += 1;
} else if (std.mem.startsWith(u8, line, "[ERROR]")) {
self.error_count += 1;
try self.error_messages.append(line);
} else if (std.mem.startsWith(u8, line, "[DEBUG]")) {
self.debug_count += 1;
}
}
};
pub fn parseLog(allocator: std.mem.Allocator, input_path: []const u8, output_path: []const u8) !void {
// Abrir arquivo de entrada
const input = try std.fs.cwd().openFile(input_path, .{});
defer input.close();
var buf_reader = std.io.bufferedReader(input.reader());
const reader = buf_reader.reader();
var stats = LogStats.init(allocator);
defer stats.deinit();
// Processar cada linha
var line_buf: [8192]u8 = undefined;
while (reader.readUntilDelimiterOrEof(&line_buf, '\n')) |maybe_line| {
const line = maybe_line orelse break;
if (line.len == 0) continue;
try stats.registrar(line);
} else |err| {
return err;
}
// Gerar relatório
const output = try std.fs.cwd().createFile(output_path, .{});
defer output.close();
var buf_writer = std.io.bufferedWriter(output.writer());
const writer = buf_writer.writer();
try writer.writeAll("=== RELATÓRIO DE LOG ===\n\n");
try writer.print("Total de linhas: {}\n", .{stats.total});
try writer.print("INFO: {} ({d:.1}%)\n", .{
stats.info_count,
percentual(stats.info_count, stats.total),
});
try writer.print("WARN: {} ({d:.1}%)\n", .{
stats.warn_count,
percentual(stats.warn_count, stats.total),
});
try writer.print("ERROR: {} ({d:.1}%)\n", .{
stats.error_count,
percentual(stats.error_count, stats.total),
});
try writer.print("DEBUG: {} ({d:.1}%)\n", .{
stats.debug_count,
percentual(stats.debug_count, stats.total),
});
if (stats.error_messages.items.len > 0) {
try writer.writeAll("\n--- ERROS ENCONTRADOS ---\n");
for (stats.error_messages.items) |msg| {
try writer.print(" {s}\n", .{msg});
}
}
try buf_writer.flush();
std.debug.print("Relatório salvo em '{s}'\n", .{output_path});
}
fn percentual(parte: usize, total: usize) f64 {
if (total == 0) return 0.0;
return @as(f64, @floatFromInt(parte)) / @as(f64, @floatFromInt(total)) * 100.0;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
try parseLog(gpa.allocator(), "app.log", "relatorio.txt");
}
Tratamento de Erros em Operações de Arquivo
Operações de I/O são inerentemente sujeitas a falhas. Zig torna o tratamento de erros explícito e composível:
const std = @import("std");
pub fn lerArquivoSeguro(path: []const u8) ![]const u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
switch (err) {
error.FileNotFound => {
std.debug.print("Arquivo '{s}' não encontrado.\n", .{path});
return err;
},
error.AccessDenied => {
std.debug.print("Sem permissão para ler '{s}'.\n", .{path});
return err;
},
else => {
std.debug.print("Erro ao abrir '{s}': {}\n", .{ path, err });
return err;
},
}
};
defer file.close();
const conteudo = file.readToEndAlloc(allocator, 1024 * 1024) catch |err| {
switch (err) {
error.OutOfMemory => {
std.debug.print("Arquivo muito grande para ler na memória.\n", .{});
return err;
},
else => return err,
}
};
return conteudo;
}
pub fn copiarArquivo(origem: []const u8, destino: []const u8) !void {
// Abrir origem
const src = try std.fs.cwd().openFile(origem, .{});
defer src.close();
// Criar destino
const dst = try std.fs.cwd().createFile(destino, .{});
errdefer {
dst.close();
// Se falhar, remover arquivo parcial
std.fs.cwd().deleteFile(destino) catch {};
}
defer dst.close();
// Copiar em blocos
var buf: [4096]u8 = undefined;
while (true) {
const bytes_read = try src.read(&buf);
if (bytes_read == 0) break;
try dst.writeAll(buf[0..bytes_read]);
}
std.debug.print("Copiado: {s} -> {s}\n", .{ origem, destino });
}
Note o uso de errdefer para limpar o arquivo de destino parcial em caso de falha durante a cópia. Esse padrão garante que operações incompletas não deixem arquivos corrompidos no sistema.
Conclusão
O módulo std.fs do Zig oferece uma API completa e segura para operações de arquivo. Os pontos principais são: use defer para garantir que arquivos sejam fechados, prefira buffered readers/writers para performance, trate cada erro de forma específica e utilize errdefer para limpeza em caso de falha. Com essas ferramentas, você pode construir desde scripts simples de processamento de texto até sistemas complexos de gerenciamento de arquivos com total controle e segurança.