---
title: "Configuração para CLI em Zig: XDG, Env Vars e Arquivos Locais"
url: "https://ziglang.com.br/artigos/zig-cli-config-xdg-env/"
markdown_url: "https://ziglang.com.br/artigos/zig-cli-config-xdg-env.MD"
description: "Como desenhar configuração para ferramentas CLI em Zig: precedência entre flags, variáveis de ambiente e arquivos XDG, validação, segredos e testes."
date: "2026-06-08"
author: ""
---

# Configuração para CLI em Zig: XDG, Env Vars e Arquivos Locais

Como desenhar configuração para ferramentas CLI em Zig: precedência entre flags, variáveis de ambiente e arquivos XDG, validação, segredos e testes.


Uma CLI útil quase sempre começa simples: alguns argumentos posicionais, uma flag `--verbose` e talvez um caminho de saída. Depois aparecem tokens de API, formato de relatório, URL do servidor, cache local, timeout, perfil de ambiente e preferências que ninguém quer digitar toda vez. Se essa configuração cresce sem regra clara, a ferramenta fica imprevisível: um valor vem da flag, outro do `.env`, outro de um arquivo escondido, e ninguém sabe qual venceu.

Zig combina bem com CLIs porque força decisões explícitas. A mesma disciplina vale para configuração. Em vez de depender de um framework que mistura tudo, desenhe uma camada pequena que leia fontes conhecidas, valide o resultado e entregue uma struct final para o resto do programa.

Este guia mostra uma abordagem prática para **configuração de CLI em Zig** usando flags, variáveis de ambiente, diretórios XDG, arquivos locais e defaults seguros. Ele complementa o artigo de [criar CLI profissional em Zig](/artigos/zig-cli-aplicacao-linha-comando/), a receita de [argumentos de linha de comando](/receitas/zig-argumentos-cli/), a receita de [variáveis de ambiente](/receitas/zig-variaveis-ambiente/), o guia de [ferramentas internas em Zig](/artigos/zig-ferramentas-internas-devops/) e o material de [SQLite para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/).

## O problema que a configuração resolve

Uma CLI usada por uma pessoa pode sobreviver com flags longas. Uma CLI usada por uma equipe precisa de memória operacional. Exemplos:

- `zigdeploy` precisa saber o endpoint padrão, timeout e diretório de artefatos;
- um analisador de logs precisa lembrar filtros comuns e formato de saída;
- uma ferramenta de build precisa descobrir cache, profile e target;
- um cliente interno precisa ler token de API sem gravar segredo no repositório;
- um agente local precisa diferenciar configuração global, projeto atual e execução pontual.

A regra de ouro é separar **configuração persistente** de **intenção do comando atual**. Um arquivo pode dizer que o endpoint padrão é produção, mas `--endpoint staging` no comando atual deve vencer. Um token pode vir de variável de ambiente, mas nunca deve aparecer em logs ou em `--help`.

## Precedência recomendada

Defina a ordem antes de escrever o parser. Uma precedência comum e fácil de explicar:

1. **Flags de CLI**: intenção explícita da execução atual.
2. **Variáveis de ambiente**: boas para CI, containers e segredos.
3. **Arquivo local do projeto**: configuração versionável ou por repositório.
4. **Arquivo do usuário em XDG config**: preferência pessoal persistente.
5. **Defaults compilados**: valores seguros para começar.

Em forma de tabela:

| Fonte | Exemplo | Deve guardar segredo? | Quando usar |
|---|---|---:|---|
| Flag | `--timeout 10s` | Não | Ajuste pontual |
| Env var | `ZIGTOOL_TOKEN` | Sim, com cuidado | CI, sessão local, secret manager |
| Projeto | `.zigtool/config.ini` | Não | Convenções do repo |
| Usuário | `~/.config/zigtool/config.ini` | Evite | Preferências pessoais |
| Default | timeout `30s` | Não | Experiência inicial |

A vantagem de explicitar isso no código e na documentação é que suporte fica simples: rode `zigtool config explain` ou imprima uma visão sem segredos indicando de onde cada valor veio.

## Modele a configuração final como struct

O resto da aplicação não deve saber se o valor veio de flag, env ou arquivo. Ele deve receber uma struct validada:

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

const OutputFormat = enum { text, json, markdown };

const Config = struct {
    endpoint: []const u8,
    token: ?[]const u8,
    cache_dir: []const u8,
    timeout_ms: u32,
    output: OutputFormat,
    verbose: bool,
};
```

Essa struct representa a verdade final. Se `endpoint` está vazio, se `timeout_ms` é zero ou se `output` tem valor inválido, o erro deve acontecer antes de rodar qualquer ação. Uma CLI previsível falha cedo.

Para debug, você pode manter metadados separados:

```zig
const Source = enum { flag, env, project_file, user_file, default };

const FieldSource = struct {
    endpoint: Source = .default,
    token: Source = .default,
    cache_dir: Source = .default,
    timeout_ms: Source = .default,
    output: Source = .default,
    verbose: Source = .default,
};
```

Não misture `FieldSource` com a lógica de negócio. Ele existe para `config explain`, testes e mensagens de diagnóstico.

## Comece pelos defaults seguros

Defaults não devem ser convenientes ao ponto de perigosos. Para uma ferramenta que fala com API, talvez o default seja endpoint de leitura, timeout moderado e saída humana. Para uma ferramenta que altera estado, exija flag explícita.

```zig
fn defaultConfig(allocator: std.mem.Allocator) !Config {
    const cache_dir = try std.fs.getAppDataDir(allocator, "zigtool");

    return .{
        .endpoint = "https://api.exemplo.local",
        .token = null,
        .cache_dir = cache_dir,
        .timeout_ms = 30_000,
        .output = .text,
        .verbose = false,
    };
}
```

`std.fs.getAppDataDir` ajuda a respeitar convenções do sistema. Em Linux, isso tende a seguir a família XDG; em outros sistemas, usa o local apropriado para dados de aplicação. Para configuração textual, você pode calcular caminhos de forma parecida ou aceitar `--config` para apontar um arquivo específico.

## Leia variáveis de ambiente com nomes estáveis

Variáveis de ambiente funcionam muito bem para CI e segredos. Use prefixo claro para evitar colisão:

```zig
fn applyEnv(config: *Config, sources: *FieldSource) void {
    if (std.process.getEnvVarOwned(std.heap.page_allocator, "ZIGTOOL_ENDPOINT")) |value| {
        config.endpoint = value;
        sources.endpoint = .env;
    } else |_| {}

    if (std.process.getEnvVarOwned(std.heap.page_allocator, "ZIGTOOL_TOKEN")) |value| {
        config.token = value;
        sources.token = .env;
    } else |_| {}

    if (std.process.getEnvVarOwned(std.heap.page_allocator, "ZIGTOOL_OUTPUT")) |value| {
        if (std.mem.eql(u8, value, "json")) {
            config.output = .json;
            sources.output = .env;
        } else if (std.mem.eql(u8, value, "markdown")) {
            config.output = .markdown;
            sources.output = .env;
        } else if (std.mem.eql(u8, value, "text")) {
            config.output = .text;
            sources.output = .env;
        }
    } else |_| {}
}
```

O exemplo usa `page_allocator` para focar no desenho. Em uma CLI real, passe o allocator da aplicação e libere as strings alocadas no shutdown. Se a ferramenta roda muitas vezes dentro do mesmo processo, esse cuidado deixa de ser detalhe.

## Arquivo de configuração: simples ganha

Para CLIs pequenas, um formato `key=value` costuma bastar:

```ini
endpoint=https://api.exemplo.local
cache_dir=.zigtool/cache
output=json
timeout_ms=15000
```

Não coloque token no arquivo versionado do projeto. Se precisar documentar, use placeholder:

```ini
# Defina ZIGTOOL_TOKEN no ambiente ou no secret manager.
endpoint=https://api.exemplo.local
```

Um parser mínimo pode ignorar comentários e linhas vazias:

```zig
fn applyConfigLine(config: *Config, sources: *FieldSource, line: []const u8, source: Source) !void {
    const trimmed = std.mem.trim(u8, line, " \t\r\n");
    if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) return;

    const eq = std.mem.indexOfScalar(u8, trimmed, '=') orelse return error.InvalidConfigLine;
    const key = std.mem.trim(u8, trimmed[0..eq], " \t");
    const value = std.mem.trim(u8, trimmed[eq + 1 ..], " \t");

    if (std.mem.eql(u8, key, "endpoint")) {
        config.endpoint = value;
        sources.endpoint = source;
    } else if (std.mem.eql(u8, key, "cache_dir")) {
        config.cache_dir = value;
        sources.cache_dir = source;
    } else if (std.mem.eql(u8, key, "timeout_ms")) {
        config.timeout_ms = try std.fmt.parseInt(u32, value, 10);
        sources.timeout_ms = source;
    } else if (std.mem.eql(u8, key, "output")) {
        if (std.mem.eql(u8, value, "json")) config.output = .json
        else if (std.mem.eql(u8, value, "markdown")) config.output = .markdown
        else if (std.mem.eql(u8, value, "text")) config.output = .text
        else return error.InvalidOutputFormat;
        sources.output = source;
    } else {
        return error.UnknownConfigKey;
    }
}
```

Em produção, copie `value` para memória própria se o buffer da linha será reutilizado. O exemplo mostra a lógica de precedência, mas a vida útil das strings precisa ser tratada com o mesmo rigor que qualquer outro slice em Zig.

## Flags vencem tudo

Depois de aplicar defaults, arquivo de usuário, arquivo do projeto e env vars, processe as flags. Isso permite override explícito:

```zig
fn applyArgs(config: *Config, sources: *FieldSource, args: []const []const u8) !void {
    var i: usize = 0;
    while (i < args.len) : (i += 1) {
        const arg = args[i];

        if (std.mem.eql(u8, arg, "--endpoint")) {
            i += 1;
            if (i >= args.len) return error.MissingEndpoint;
            config.endpoint = args[i];
            sources.endpoint = .flag;
        } else if (std.mem.eql(u8, arg, "--json")) {
            config.output = .json;
            sources.output = .flag;
        } else if (std.mem.eql(u8, arg, "--timeout-ms")) {
            i += 1;
            if (i >= args.len) return error.MissingTimeout;
            config.timeout_ms = try std.fmt.parseInt(u32, args[i], 10);
            sources.timeout_ms = .flag;
        } else if (std.mem.eql(u8, arg, "--verbose")) {
            config.verbose = true;
            sources.verbose = .flag;
        }
    }
}
```

Se a CLI já tem muitos subcomandos, separe parsing de comando e parsing de configuração. `zigtool deploy --timeout-ms 5000` não deveria aceitar as mesmas flags posicionais que `zigtool config set output json` sem intenção clara.

## Valide antes de executar

A validação deve responder três perguntas:

1. o valor tem formato aceitável?
2. o valor é seguro para esta ação?
3. a combinação de valores faz sentido?

```zig
fn validateConfig(config: Config) !void {
    if (!std.mem.startsWith(u8, config.endpoint, "https://")) {
        return error.EndpointMustUseHttps;
    }

    if (config.timeout_ms < 100 or config.timeout_ms > 120_000) {
        return error.InvalidTimeout;
    }

    if (config.cache_dir.len == 0) {
        return error.EmptyCacheDir;
    }
}
```

Para ações destrutivas, não esconda confirmação dentro de config global. Prefira uma flag explícita no comando atual, como `--confirm-delete`, porque intenção perigosa não deve ficar gravada em arquivo.

## Segredos: leia, use, não exponha

Token de API, senha, chave privada e webhook secreto devem vir de secret manager ou variável de ambiente. Se a CLI precisa persistir credencial local, use o cofre do sistema quando possível. Arquivo em texto puro só deve ser exceção documentada e com permissões restritas.

Cuidados práticos:

- não imprima token em erro, debug, `config explain` ou telemetria;
- mostre apenas `set` / `not set` ou os últimos quatro caracteres se for realmente necessário;
- não aceite segredo por flag quando o shell history é risco;
- não grave env vars em arquivos de log;
- trate `.env` como conveniência local, não como contrato de produção.

Uma função de explicação pode mascarar:

```zig
fn printConfigExplain(config: Config, sources: FieldSource, writer: anytype) !void {
    try writer.print("endpoint: {s} ({})\n", .{ config.endpoint, sources.endpoint });
    try writer.print("cache_dir: {s} ({})\n", .{ config.cache_dir, sources.cache_dir });
    try writer.print("timeout_ms: {d} ({})\n", .{ config.timeout_ms, sources.timeout_ms });
    try writer.print("token: {s} ({})\n", .{ if (config.token == null) "not set" else "set", sources.token });
}
```

## Projeto local vs usuário global

Uma boa regra:

- configuração que todos no projeto compartilham pode ficar em `.zigtool/config.ini`;
- preferências pessoais ficam no diretório XDG do usuário;
- segredos ficam no ambiente ou cofre;
- overrides pontuais ficam nas flags.

Isso evita commits acidentais de token e reduz conflito de configuração. Se o repositório precisa de exemplo, publique `.zigtool/config.example.ini`, não o arquivo real.

Para cache e estado, use outro caminho. Configuração responde “como rodar”; cache responde “o que já aconteceu”. Misturar os dois cria arquivos difíceis de revisar e limpar.

## Teste a precedência

Configuração merece testes porque bugs nessa camada são silenciosos. Monte testes com entradas pequenas:

```zig
test "flag vence env e default" {
    var config = try defaultConfig(std.testing.allocator);
    var sources = FieldSource{};

    config.endpoint = "https://env.exemplo.local";
    sources.endpoint = .env;

    try applyArgs(&config, &sources, &.{ "--endpoint", "https://flag.exemplo.local" });

    try std.testing.expectEqualStrings("https://flag.exemplo.local", config.endpoint);
    try std.testing.expectEqual(Source.flag, sources.endpoint);
}
```

Também teste erro de timeout inválido, formato de output desconhecido, arquivo com chave errada e ausência de token quando uma ação exige autenticação.

## Checklist para uma CLI configurável

Antes de considerar a configuração pronta, confira:

- existe uma ordem de precedência documentada;
- `--help` mostra flags e env vars importantes;
- `config explain` ou modo debug mostra fontes sem revelar segredos;
- valores perigosos não ficam persistidos por acidente;
- arquivo de projeto não exige caminho absoluto da máquina de uma pessoa;
- defaults são seguros;
- validação roda antes de executar efeitos externos;
- testes cobrem precedência e erros comuns.

Configuração boa é chata. Ela não aparece quando tudo dá certo e ajuda rápido quando algo dá errado. Em Zig, isso combina com a filosofia da linguagem: menos magia, mais contrato explícito. Se você já tem uma CLI básica, a próxima melhoria não é adicionar mais flags aleatórias; é transformar configuração em uma camada pequena, testada e previsível.

## Próximos passos

Se a CLI ainda está no começo, leia [criar aplicação de linha de comando em Zig](/artigos/zig-cli-aplicacao-linha-comando/) e a receita de [argumentos CLI](/receitas/zig-argumentos-cli/). Para ferramentas que precisam persistir histórico ou cache, veja [Zig e SQLite para ferramentas locais](/artigos/zig-sqlite-ferramentas-locais/). Para distribuição, combine isso com [GitHub Actions para releases multiplataforma](/artigos/zig-github-actions-release-multiplataforma/) ou [GitLab CI para Zig](/artigos/zig-gitlab-ci-pipeline-build-test-release/).

