Bibliotecas CLI em Zig — Parsing de Argumentos e Ferramentas de Linha de Comando
Ferramentas de linha de comando são um dos usos mais naturais para Zig. A combinação de binários pequenos, startup instantâneo e performance excelente torna Zig ideal para CLIs. O ecossistema oferece bibliotecas maduras para parsing de argumentos que simplificam a criação de interfaces de linha de comando completas.
std.process.ArgIterator — A Abordagem Padrão
A biblioteca padrão oferece iteração básica sobre argumentos:
const std = @import("std");
pub fn main() !void {
var args = std.process.args();
// Pular nome do programa
_ = args.next();
while (args.next()) |arg| {
std.debug.print("Argumento: {s}\n", .{arg});
}
}
zig-clap — A Biblioteca Mais Popular
O zig-clap é a biblioteca de parsing de argumentos mais usada no ecossistema. Inspirada no clap do Rust, oferece uma API declarativa e geração automática de help:
const std = @import("std");
const clap = @import("clap");
pub fn main() !void {
const params = comptime clap.parseParamsComptime(
\\-h, --help Mostrar ajuda
\\-v, --verbose Modo verboso
\\-p, --port <u16> Porta do servidor (padrão: 8080)
\\-H, --host <str> Host do servidor
\\-c, --config <str> Arquivo de configuração
\\<str>... Arquivos de entrada
\\
);
var diag = clap.Diagnostic{};
var res = clap.parse(clap.Help, ¶ms, .{
.string = clap.parsers.string,
.str = clap.parsers.string,
.u16 = clap.parsers.int(u16, 10),
}, .{ .diagnostic = &diag }) catch |err| {
diag.report();
return err;
};
defer res.deinit();
// Acessar argumentos parseados
if (res.args.help != 0) {
clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}) catch {};
return;
}
const porta = res.args.port orelse 8080;
const host = res.args.host orelse "localhost";
const verbose = res.args.verbose != 0;
std.debug.print("Servidor: {s}:{}\n", .{ host, porta });
std.debug.print("Verbose: {}\n", .{verbose});
// Arquivos de entrada (argumentos posicionais)
for (res.positionals) |arquivo| {
std.debug.print("Arquivo: {s}\n", .{arquivo});
}
}
Subcomandos
const Command = union(enum) {
init: struct {
nome: []const u8,
template: ?[]const u8 = null,
},
build: struct {
release: bool = false,
target: ?[]const u8 = null,
},
test_cmd: struct {
filtro: ?[]const u8 = null,
verbose: bool = false,
},
help: void,
const descriptions = .{
.init = "Criar novo projeto",
.build = "Compilar o projeto",
.test_cmd = "Executar testes",
.help = "Mostrar ajuda",
};
};
yazap — Alternativa Ergonômica
O yazap oferece uma API builder pattern familiar:
const yazap = @import("yazap");
pub fn main() !void {
var app = try yazap.App.init(
std.heap.page_allocator,
"minha-ferramenta",
"Descrição da ferramenta",
);
defer app.deinit();
var root = app.rootCommand();
// Flags globais
try root.addArg(yazap.Arg.booleanOption("verbose", 'v', "Modo verboso"));
try root.addArg(yazap.Arg.singleValueOption("config", 'c', "Arquivo de configuração"));
// Subcomando 'init'
var init_cmd = app.createCommand("init", "Inicializar novo projeto");
try init_cmd.addArg(yazap.Arg.singleValueOption("nome", 'n', "Nome do projeto"));
try init_cmd.addArg(yazap.Arg.singleValueOption("template", 't', "Template a usar"));
try root.addSubcommand(init_cmd);
// Subcomando 'build'
var build_cmd = app.createCommand("build", "Compilar projeto");
try build_cmd.addArg(yazap.Arg.booleanOption("release", 'r', "Build de release"));
try root.addSubcommand(build_cmd);
// Parse
const result = try app.parseProcess();
if (result.isSet("verbose")) {
std.debug.print("Modo verboso ativado\n", .{});
}
if (result.subcommand) |sub| {
if (std.mem.eql(u8, sub.name, "init")) {
const nome = sub.getSingleValue("nome") orelse "meu-projeto";
std.debug.print("Inicializando projeto: {s}\n", .{nome});
}
}
}
Construindo CLIs Completas
Barras de Progresso
const ProgressBar = struct {
total: usize,
atual: usize = 0,
largura: usize = 50,
label: []const u8,
pub fn atualizar(self: *ProgressBar, incremento: usize) void {
self.atual += incremento;
const porcentagem = @as(f64, @floatFromInt(self.atual)) /
@as(f64, @floatFromInt(self.total));
const preenchido = @as(usize, @intFromFloat(porcentagem * @as(f64, @floatFromInt(self.largura))));
const writer = std.io.getStdErr().writer();
writer.print("\r{s} [", .{self.label}) catch {};
for (0..self.largura) |i| {
if (i < preenchido) {
writer.writeByte('=') catch {};
} else if (i == preenchido) {
writer.writeByte('>') catch {};
} else {
writer.writeByte(' ') catch {};
}
}
writer.print("] {d:.0}%", .{porcentagem * 100}) catch {};
}
};
Cores no Terminal
const Cor = struct {
const reset = "\x1b[0m";
const vermelho = "\x1b[31m";
const verde = "\x1b[32m";
const amarelo = "\x1b[33m";
const azul = "\x1b[34m";
const negrito = "\x1b[1m";
pub fn formatar(comptime cor: []const u8, texto: []const u8) void {
const writer = std.io.getStdOut().writer();
writer.print("{s}{s}{s}", .{ cor, texto, reset }) catch {};
}
};
pub fn sucesso(msg: []const u8) void {
Cor.formatar(Cor.verde, msg);
}
pub fn erro(msg: []const u8) void {
Cor.formatar(Cor.vermelho, msg);
}
Input Interativo
pub fn lerLinha(prompt: []const u8) ![]const u8 {
const stdout = std.io.getStdOut().writer();
try stdout.print("{s}", .{prompt});
const stdin = std.io.getStdIn().reader();
var buf: [1024]u8 = undefined;
const linha = try stdin.readUntilDelimiter(&buf, '\n');
return linha;
}
pub fn confirmar(pergunta: []const u8) !bool {
const resposta = try lerLinha(
std.fmt.comptimePrint("{s} [s/N]: ", .{pergunta}),
);
return resposta.len > 0 and (resposta[0] == 's' or resposta[0] == 'S');
}
Exemplo Completo: Ferramenta de Gerenciamento de Projetos
const std = @import("std");
const clap = @import("clap");
pub fn main() !void {
const allocator = std.heap.page_allocator;
const params = comptime clap.parseParamsComptime(
\\-h, --help Mostrar ajuda
\\-V, --version Mostrar versão
\\ --verbose Saída detalhada
\\<str> Comando (init, build, test, deploy)
\\<str>... Argumentos do comando
\\
);
var res = clap.parse(clap.Help, ¶ms, .{
.string = clap.parsers.string,
.str = clap.parsers.string,
}, .{}) catch |err| {
return err;
};
defer res.deinit();
if (res.args.version != 0) {
std.debug.print("minha-ferramenta v1.0.0\n", .{});
return;
}
const comando = if (res.positionals.len > 0) res.positionals[0] else {
std.debug.print("Erro: nenhum comando fornecido\n", .{});
return;
};
_ = allocator;
if (std.mem.eql(u8, comando, "init")) {
std.debug.print("Inicializando projeto...\n", .{});
} else if (std.mem.eql(u8, comando, "build")) {
std.debug.print("Compilando projeto...\n", .{});
} else {
std.debug.print("Comando desconhecido: {s}\n", .{comando});
}
}
Boas Práticas
- Sempre forneça –help: Use geração automática quando possível
- Siga convenções POSIX: Flags curtas (-v) e longas (–verbose)
- Mensagens de erro claras: Indique o que deu errado e como corrigir
- Exit codes corretos: 0 para sucesso, 1 para erro do usuário, 2 para erro interno
- Suporte a pipes: Leia de stdin e escreva para stdout para composição Unix
Próximos Passos
Explore as ferramentas de logging para diagnóstico em CLIs, as bibliotecas JSON para configurações, e os frameworks de teste para testar suas ferramentas. Veja nossos tutoriais e receitas para projetos CLI completos.