Bibliotecas CLI em Zig — Parsing de Argumentos e Ferramentas de Linha de Comando

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});
    }
}

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, &params, .{
        .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, &params, .{}) 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, &params, .{
        .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

  1. Sempre forneça –help: Use geração automática quando possível
  2. Siga convenções POSIX: Flags curtas (-v) e longas (–verbose)
  3. Mensagens de erro claras: Indique o que deu errado e como corrigir
  4. Exit codes corretos: 0 para sucesso, 1 para erro do usuário, 2 para erro interno
  5. 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.

Continue aprendendo Zig

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