---
title: "Criar CLI Profissional em Zig Passo a Passo"
url: "https://ziglang.com.br/artigos/zig-cli-aplicacao-linha-comando/"
markdown_url: "https://ziglang.com.br/artigos/zig-cli-aplicacao-linha-comando.MD"
description: "Aprenda a criar aplicações de linha de comando em Zig: parsing de argumentos, subcomandos, flags, saída colorida, exit codes e distribuição multiplataforma."
date: "2026-05-12"
author: ""
---

# Criar CLI Profissional em Zig Passo a Passo

Aprenda a criar aplicações de linha de comando em Zig: parsing de argumentos, subcomandos, flags, saída colorida, exit codes e distribuição multiplataforma.


Ferramentas de linha de comando são uma das aplicações mais naturais para Zig. A combinação de binários estáticos pequenos, [cross-compilation](/artigos/zig-cross-compilation-guia/) trivial e inicialização instantânea torna Zig uma escolha superior para CLIs que precisam ser rápidas, confiáveis e fáceis de distribuir.

Neste tutorial, vamos construir uma CLI completa do zero — um buscador de texto em arquivos chamado `ziggrep` — cobrindo parsing de argumentos, subcomandos, flags, saída colorida e empacotamento para distribuição. Se a sua ferramenta precisar de navegação, painéis e atalhos interativos, veja também o guia de [TUI em Zig para apps de terminal](/artigos/zig-tui-terminal-apps/).

## Atualização 2026: onde uma CLI Zig vira ferramenta de produto

Uma CLI Zig deixa de ser exercício quando entra em uma rotina real: valida um deploy, transforma dados, chama uma API interna, gera relatório, empacota um release ou roda em CI sem depender de runtime. Para esse tipo de uso, pense em três camadas antes de escrever o primeiro `switch` de subcomando.

A primeira camada é **interface**: nomes de comandos, flags curtas e longas, ajuda útil, mensagens de erro e códigos de saída. Um usuário precisa entender rapidamente se deve rodar `ziggrep scan`, `ziggrep report` ou `ziggrep check`. Também precisa saber se o exit code `1` significa resultado inválido, erro de entrada ou falha temporária de rede.

A segunda camada é **operação**. CLIs que viram ferramentas internas precisam de configuração previsível, logs sem segredo, modo JSON para automação e build reproduzível. O guia de [configuração segura em Zig](/artigos/zig-configuracao-segura-segredos-env/) mostra como tratar variáveis, arquivos locais e flags sem vazar token. Para ferramentas que guardam cache, histórico ou estado local, [Zig com SQLite](/artigos/zig-sqlite-ferramentas-locais/) é um caminho natural.

A terceira camada é **distribuição**. O valor de Zig aparece quando a equipe baixa um binário para Linux, macOS ou Windows e ele simplesmente roda. Se a CLI vai circular fora da sua máquina, conecte este tutorial ao fluxo de [GitHub Actions para releases multiplataforma](/artigos/zig-github-actions-release-multiplataforma/) e ao guia de [cross-compilation](/artigos/zig-cross-compilation-guia/). Para times de plataforma, a visão mais ampla está em [ferramentas internas em Zig para DevOps](/artigos/zig-ferramentas-internas-devops/).

Alguns bons próximos passos depois do `ziggrep` deste artigo:

- transformar o parser de argumentos em um módulo `cli.zig` testável;
- adicionar `--format text|json` para uso humano e CI;
- separar erros de uso, erros temporários e falhas de validação;
- adicionar subcomandos pequenos em vez de uma única lista gigante de flags;
- usar [ETL em Zig](/artigos/zig-etl-csv-jsonl-migracao-dados/) quando a CLI processa CSV, JSONL ou migrações;
- publicar artefatos com checksums antes de entregar a ferramenta para outras pessoas.

Esse contexto importa para SEO e para o leitor: quem procura "zig cli" raramente quer só imprimir argumentos. Quer saber se Zig serve para uma ferramenta de linha de comando séria, distribuível e mantida por equipe. A resposta é sim, desde que você trate interface, operação e release como partes do mesmo produto.

## Estrutura do Projeto

Comece criando o projeto com o [build system](/artigos/zig-build-system-guia/) padrão:

```bash
mkdir ziggrep && cd ziggrep
zig init
```

Isso gera a estrutura com `build.zig`, `build.zig.zon` e `src/main.zig`. A configuração do [package manager](/artigos/zig-package-manager-guia-build-zig-zon/) via `build.zig.zon` permite adicionar dependências externas se necessário, mas para nossa CLI, a biblioteca padrão é suficiente.

## Parsing de Argumentos com std.process

O ponto de partida é capturar os argumentos da linha de comando. Zig oferece `std.process.argsAlloc` para isso:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    if (args.len < 2) {
        try printUsage();
        std.process.exit(1);
    }

    // args[0] é o nome do executável
    // args[1] em diante são os argumentos do usuário
    for (args[1..]) |arg| {
        std.debug.print("Argumento: {s}\n", .{arg});
    }
}

fn printUsage() !void {
    const stderr = std.io.getStdErr().writer();
    try stderr.print(
        \\Uso: ziggrep [opções] <padrão> [arquivo...]
        \\
        \\Opções:
        \\  -i, --ignore-case    Ignorar maiúsculas/minúsculas
        \\  -n, --line-number    Exibir número da linha
        \\  -c, --count          Contar ocorrências
        \\  -r, --recursive      Buscar recursivamente
        \\  -h, --help           Exibir esta ajuda
        \\  -v, --version        Exibir versão
        \\
    , .{});
}
```

O uso do [GeneralPurposeAllocator](/glossario/allocator/) com `defer` para liberação garante que não haja vazamentos de memória — um padrão fundamental que detalhamos no guia de [alocação de memória](/artigos/zig-alocacao-memoria-estrategias/).

## Implementando Flags e Opções

Para uma CLI profissional, precisamos parsear flags como `--ignore-case` e `-n`. Veja uma implementação robusta:

```zig
const std = @import("std");

const Config = struct {
    ignore_case: bool = false,
    line_number: bool = false,
    count_only: bool = false,
    recursive: bool = false,
    pattern: ?[]const u8 = null,
    files: std.ArrayList([]const u8),

    fn init(allocator: std.mem.Allocator) Config {
        return .{
            .files = std.ArrayList([]const u8).init(allocator),
        };
    }

    fn deinit(self: *Config) void {
        self.files.deinit();
    }
};

fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) !Config {
    var config = Config.init(allocator);
    errdefer config.deinit();

    var i: usize = 0;
    while (i < args.len) : (i += 1) {
        const arg = args[i];

        if (std.mem.startsWith(u8, arg, "-")) {
            if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "--ignore-case")) {
                config.ignore_case = true;
            } else if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--line-number")) {
                config.line_number = true;
            } else if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--count")) {
                config.count_only = true;
            } else if (std.mem.eql(u8, arg, "-r") or std.mem.eql(u8, arg, "--recursive")) {
                config.recursive = true;
            } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
                try printUsage();
                std.process.exit(0);
            } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
                const stdout = std.io.getStdOut().writer();
                try stdout.print("ziggrep 1.0.0\n", .{});
                std.process.exit(0);
            } else {
                const stderr = std.io.getStdErr().writer();
                try stderr.print("Opção desconhecida: {s}\n", .{arg});
                std.process.exit(1);
            }
        } else if (config.pattern == null) {
            config.pattern = arg;
        } else {
            try config.files.append(arg);
        }
    }

    return config;
}
```

O uso de `errdefer` é essencial aqui — se qualquer operação falhar durante o parsing, o `ArrayList` é liberado automaticamente. Esse padrão de [error handling](/artigos/zig-error-handling-boas-praticas/) é idiomático em Zig e evita vazamentos em caminhos de erro.

## Busca em Arquivos com Buffered I/O

A lógica central do `ziggrep` lê arquivos linha por linha usando I/O com buffer:

```zig
fn searchFile(
    file_path: []const u8,
    pattern: []const u8,
    config: Config,
    writer: anytype,
) !u32 {
    const file = std.fs.cwd().openFile(file_path, .{}) catch |err| {
        const stderr = std.io.getStdErr().writer();
        try stderr.print("Erro ao abrir '{s}': {}\n", .{ file_path, err });
        return 0;
    };
    defer file.close();

    var buf_reader = std.io.bufferedReader(file.reader());
    var line_buf: [4096]u8 = undefined;
    var line_num: u32 = 0;
    var match_count: u32 = 0;

    while (buf_reader.reader().readUntilDelimiterOrEof(&line_buf, '\n')) |maybe_line| {
        const line = maybe_line orelse break;
        line_num += 1;

        const haystack = if (config.ignore_case)
            std.ascii.lowerString(&line_buf, line)
        else
            line;

        const needle = if (config.ignore_case)
            blk: {
                var lower: [256]u8 = undefined;
                break :blk std.ascii.lowerString(&lower, pattern);
            }
        else
            pattern;

        if (std.mem.indexOf(u8, haystack, needle)) |_| {
            match_count += 1;
            if (!config.count_only) {
                if (config.line_number) {
                    try writer.print("{s}:{d}: {s}\n", .{ file_path, line_num, line });
                } else {
                    try writer.print("{s}: {s}\n", .{ file_path, line });
                }
            }
        }
    } else |err| {
        const stderr = std.io.getStdErr().writer();
        try stderr.print("Erro ao ler '{s}': {}\n", .{ file_path, err });
    }

    return match_count;
}
```

O `bufferedReader` reduz syscalls agrupando leituras em blocos maiores — uma técnica que detalhamos no contexto de [networking e I/O](/artigos/zig-networking-sockets-tcp-udp/).

## Saída Colorida no Terminal

Para uma experiência profissional, adicione cores usando códigos ANSI escape:

```zig
const Color = struct {
    const reset = "\x1b[0m";
    const red = "\x1b[31m";
    const green = "\x1b[32m";
    const yellow = "\x1b[33m";
    const cyan = "\x1b[36m";
    const bold = "\x1b[1m";
};

fn printMatch(
    writer: anytype,
    file_path: []const u8,
    line_num: u32,
    line: []const u8,
    show_line_num: bool,
) !void {
    try writer.print("{s}{s}{s}", .{ Color.cyan, file_path, Color.reset });
    if (show_line_num) {
        try writer.print(":{s}{d}{s}", .{ Color.green, line_num, Color.reset });
    }
    try writer.print(": {s}\n", .{line});
}
```

Verifique se a saída é um terminal antes de usar cores, para evitar caracteres estranhos em pipes ou redirecionamentos:

```zig
const use_color = std.io.getStdOut().supportsAnsiEscapeCodes();
```

## Exit Codes e Convenções

Uma CLI bem comportada usa exit codes padronizados:

```zig
const ExitCode = enum(u8) {
    success = 0,          // Encontrou pelo menos uma ocorrência
    no_match = 1,         // Nenhuma ocorrência encontrada
    error_occurred = 2,   // Erro de I/O ou argumento inválido
};

pub fn main() !void {
    // ... parsing e busca ...

    if (had_errors) {
        std.process.exit(@intFromEnum(ExitCode.error_occurred));
    } else if (total_matches == 0) {
        std.process.exit(@intFromEnum(ExitCode.no_match));
    }
    // Exit 0 implícito — sucesso
}
```

Seguir essas convenções permite que sua ferramenta funcione bem em scripts e pipelines, integrando-se com outras ferramentas em [automação de scripts](/artigos/zig-automacao-scripts-substituir-python-bash/).

## Leitura de stdin

Para suportar pipes como `cat arquivo.txt | ziggrep padrão`, adicione leitura de stdin quando nenhum arquivo é especificado:

```zig
fn searchStdin(pattern: []const u8, config: Config, writer: anytype) !u32 {
    const stdin = std.io.getStdIn();
    var buf_reader = std.io.bufferedReader(stdin.reader());
    var line_buf: [4096]u8 = undefined;
    var line_num: u32 = 0;
    var match_count: u32 = 0;

    while (buf_reader.reader().readUntilDelimiterOrEof(&line_buf, '\n')) |maybe_line| {
        const line = maybe_line orelse break;
        line_num += 1;

        if (std.mem.indexOf(u8, line, pattern)) |_| {
            match_count += 1;
            if (!config.count_only) {
                if (config.line_number) {
                    try writer.print("{d}: {s}\n", .{ line_num, line });
                } else {
                    try writer.print("{s}\n", .{line});
                }
            }
        }
    } else |err| {
        return err;
    }

    return match_count;
}
```

## Configurando o build.zig para Distribuição

Para gerar binários otimizados e prontos para distribuição via [Docker](/artigos/zig-docker-containers/) ou download direto:

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "ziggrep",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .strip = optimize != .Debug,
    });

    b.installArtifact(exe);

    // Step de execução para testes rápidos
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Executar ziggrep");
    run_step.dependOn(&run_cmd.step);
}
```

A flag `strip` remove símbolos de debug no binário final, reduzindo o tamanho. Para distribuição multiplataforma, use [cross-compilation](/artigos/zig-cross-compilation-guia/):

```bash
zig build -Dtarget=x86_64-linux -Doptimize=ReleaseSafe
zig build -Dtarget=aarch64-macos -Doptimize=ReleaseSafe
zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSafe
```

Com três comandos, você gera binários para Linux, macOS e Windows — sem instalar toolchains adicionais.

## Testando a CLI

Integre [testes automatizados](/artigos/zig-testes-guia-completo/) diretamente no código da CLI:

```zig
test "parseArgs reconhece flags corretamente" {
    const allocator = std.testing.allocator;
    const args = &[_][]const u8{ "-i", "-n", "padrão", "arquivo.txt" };
    var config = try parseArgs(allocator, args);
    defer config.deinit();

    try std.testing.expect(config.ignore_case);
    try std.testing.expect(config.line_number);
    try std.testing.expectEqualStrings("padrão", config.pattern.?);
    try std.testing.expectEqual(@as(usize, 1), config.files.items.len);
}

test "parseArgs sem argumentos retorna pattern null" {
    const allocator = std.testing.allocator;
    const args = &[_][]const u8{};
    var config = try parseArgs(allocator, args);
    defer config.deinit();

    try std.testing.expect(config.pattern == null);
}
```

O `std.testing.allocator` detecta automaticamente vazamentos de memória nos testes — se algum `defer` estiver faltando, o teste falha com erro de leak.

## Comparação com CLIs em Outras Linguagens

Zig se destaca para CLIs que precisam de inicialização rápida e distribuição simples. Enquanto <a href="https://golang.com.br/artigos/go-cli-cobra/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go usa frameworks como Cobra</a> com reflexão em runtime, Zig resolve tudo em tempo de compilação com [comptime](/artigos/comptime-zig-metaprogramacao/).

Para quem vem de <a href="https://rustlang.com.br/artigos/rust-clap-cli/" target="_blank" rel="noopener" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust com clap</a>, a abordagem de Zig é mais manual, mas produz binários menores e com tempo de compilação significativamente inferior. A tabela abaixo ilustra as diferenças:

| Aspecto | Zig | Go | Rust |
|---------|-----|-----|------|
| Binário estático | Sim (padrão) | Sim (CGO_ENABLED=0) | Sim (musl) |
| Tamanho típico | 200-500 KB | 5-10 MB | 1-3 MB |
| Tempo de compilação | Rápido | Rápido | Lento |
| Cross-compilation | Nativo | Nativo | Via rustup target |
| Framework CLI | Biblioteca padrão | Cobra/urfave | clap/structopt |

## Próximos Passos

Com sua CLI funcional, explore:

- [Benchmarking](/artigos/zig-benchmarking-medir-performance/) para medir a performance da sua ferramenta contra alternativas
- [Error handling avançado](/artigos/zig-error-handling-boas-praticas/) para mensagens de erro mais informativas
- [Ecossistema de ferramentas](/artigos/zig-ecossistema-ferramentas/) para descobrir bibliotecas CLI da comunidade
- [Concorrência](/artigos/zig-concorrencia-padroes-avancados/) para busca paralela em múltiplos arquivos
- [Containers Docker](/artigos/zig-docker-containers/) para empacotar e distribuir sua CLI
