---
title: "Processos Filhos em Zig: stdout, stderr, Exit Code e Timeout"
url: "https://ziglang.com.br/artigos/zig-processos-filhos-stdout-stderr-timeout/"
markdown_url: "https://ziglang.com.br/artigos/zig-processos-filhos-stdout-stderr-timeout.MD"
description: "Guia prático para executar comandos com std.process.Child em Zig, capturar stdout e stderr, validar exit code, evitar deadlock em pipes e aplicar timeout."
date: "2026-08-30"
author: ""
---

# Processos Filhos em Zig: stdout, stderr, Exit Code e Timeout

Guia prático para executar comandos com std.process.Child em Zig, capturar stdout e stderr, validar exit code, evitar deadlock em pipes e aplicar timeout.


Para **executar um comando externo em Zig**, use `std.process.Child.run` quando o trabalho for curto e a saída puder ser capturada em memória. Passe cada argumento separadamente em `argv`, libere `stdout` e `stderr` com o mesmo allocator e valide o campo `term`: conseguir iniciar o processo não significa que o comando terminou com sucesso.

Quando você precisa acompanhar logs enquanto o programa roda, enviar dados por `stdin`, impor timeout, cancelar ou controlar sinais, configure um `std.process.Child`, chame `spawn` e finalize com `wait`. O cuidado mais importante é drenar `stdout` e `stderr` sem deadlock: se ambos forem pipes e um deles encher enquanto o pai lê apenas o outro, filho e pai podem ficar bloqueados para sempre.

Este guia cobre os dois níveis de API, códigos de saída, limites de memória, pipes, timeout, segurança e um checklist para CLIs, ferramentas internas e automações. Como a biblioteca padrão do Zig continua evoluindo antes do 1.0, confirme nomes de campos e assinaturas na versão fixada pelo projeto.

## Resposta rápida

| Necessidade | Abordagem recomendada |
|---|---|
| Rodar comando curto e capturar tudo | `std.process.Child.run` |
| Mostrar saída ao usuário em tempo real | herdar `stdout` e `stderr` ou fazer streaming |
| Enviar uma entrada pequena | pipe em `stdin`, escrever e fechar |
| Capturar muita saída | arquivo temporário, streaming ou limite explícito |
| Capturar stdout e stderr volumosos | leitura concorrente ou event loop |
| Executar pipeline de shell | prefira montar processos; use shell só quando necessário |
| Aplicar timeout | `spawn` + relógio + encerramento + `wait` |
| Executar input do usuário | argumentos separados, nunca concatenação em `sh -c` |
| Rodar em produção | limite de saída, timeout, log estruturado e cleanup |

## Exemplo básico com Child.run

O caminho mais simples é executar o programa, esperar e receber a saída acumulada:

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

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

    const result = try std.process.Child.run(.{
        .allocator = allocator,
        .argv = &.{ "git", "rev-parse", "--short", "HEAD" },
        .max_output_bytes = 64 * 1024,
    });
    defer allocator.free(result.stdout);
    defer allocator.free(result.stderr);

    switch (result.term) {
        .Exited => |code| {
            if (code != 0) {
                std.log.err("git falhou code={d} stderr={s}", .{
                    code,
                    std.mem.trim(u8, result.stderr, " \r\n"),
                });
                return error.ComandoFalhou;
            }
        },
        else => {
            std.log.err("git terminou de forma anormal: {}", .{result.term});
            return error.ComandoInterrompido;
        },
    }

    const commit = std.mem.trim(u8, result.stdout, " \r\n");
    std.debug.print("commit atual: {s}\n", .{commit});
}
```

`Child.run` é uma boa escolha porque concentra o ciclo `spawn` + captura + `wait`. O limite de saída evita que uma ferramenta com bug, um comando muito barulhento ou uma entrada maliciosa consuma memória sem controle. O nome exato da opção de limite pode variar entre releases; procure a definição de `RunResult` e das opções de `run` na stdlib instalada.

Três recursos precisam de ownership explícito:

1. `result.stdout` foi alocado e deve ser liberado;
2. `result.stderr` também foi alocado, mesmo quando está vazio;
3. o allocator precisa continuar válido até os dois `free`.

Esse padrão combina bem com uma [CLI profissional em Zig](/artigos/zig-cli-aplicacao-linha-comando/) e com [ferramentas internas para DevOps](/artigos/zig-ferramentas-internas-devops/).

## Spawn não é sucesso

Há duas famílias de falha diferentes.

A primeira ocorre **antes de o programa começar**:

- executável não encontrado no `PATH`;
- permissão negada;
- diretório de trabalho inválido;
- falta de memória ou descritores;
- formato de executável incompatível.

Essas falhas aparecem como erro de `run` ou `spawn`.

A segunda ocorre **depois que o filho começou**:

- encerrou com código 1, 2 ou outro valor;
- foi morto por sinal;
- recebeu uma exceção do sistema;
- foi interrompido pelo mecanismo de timeout;
- escreveu uma mensagem de erro e saiu normalmente com código não zero.

Por isso, este código é incompleto:

```zig
_ = try std.process.Child.run(.{
    .allocator = allocator,
    .argv = &.{ "git", "status" },
});
```

O `try` prova apenas que a operação da API foi concluída. O contrato do comando está em `term`. Em Unix, o caso normal costuma ser `.Exited => code`; outros targets podem representar término anormal de outra forma.

Também não trate `stderr` como sinônimo de falha. Muitas ferramentas escrevem progresso ou warnings em `stderr` e saem com zero. A regra deve vir da documentação do executável:

- exit code decide sucesso ou falha;
- `stdout` contém o resultado consumível;
- `stderr` contém diagnóstico humano;
- códigos diferentes de zero podem ter significados específicos.

`grep`, por exemplo, tradicionalmente usa `1` para “nenhuma correspondência”, não para erro operacional. Uma integração correta traduz o contrato da ferramenta em um enum ou error set próprio.

## argv direto é melhor do que shell

`std.process.Child` não interpreta uma linha de shell. Cada elemento de `argv` é um argumento separado:

```zig
.argv = &.{
    "git",
    "show",
    "--format=%H",
    "--",
    caminho_recebido,
},
```

Isso é uma vantagem. Um caminho como `relatorios/agosto final.txt` continua sendo um único argumento. Caracteres como `;`, `&`, `|`, `$` e espaços não viram operadores do shell.

Evite:

```zig
// Perigoso se arquivo vier de usuário, API ou configuração externa.
.argv = &.{ "sh", "-c", comando_concatenado },
```

Uma entrada como `arquivo.txt; curl exemplo` muda o programa executado. Mesmo quando não existe atacante, quoting inconsistente quebra com espaços, aspas, caracteres Unicode e diferenças entre Bash, PowerShell e `cmd.exe`.

Se você precisa do equivalente a:

```bash
compactador arquivo.log | uploader --stdin
```

há duas alternativas:

1. criar os dois filhos e conectar a saída de um à entrada do outro;
2. chamar um shell explicitamente, somente para uma expressão fixa e controlada.

A primeira opção é mais trabalhosa, porém oferece exit code individual, portabilidade consciente e menor superfície de command injection.

## Controle manual com spawn e wait

Use o ciclo manual quando precisar configurar o processo antes da execução:

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

pub fn runStreaming(allocator: std.mem.Allocator) !void {
    var child = std.process.Child.init(
        &.{ "zig", "build", "test" },
        allocator,
    );

    child.stdin_behavior = .Ignore;
    child.stdout_behavior = .Inherit;
    child.stderr_behavior = .Inherit;

    try child.spawn();
    const term = try child.wait();

    switch (term) {
        .Exited => |code| if (code != 0) return error.TestesFalharam,
        else => return error.ProcessoInterrompido,
    }
}
```

Herdar os fluxos é a opção mais simples para uma CLI interativa: o usuário vê cores, progresso e prompts diretamente. Também evita armazenar logs gigantes no heap.

Escolha o comportamento por fluxo:

- **Inherit**: filho usa o terminal ou redirecionamento do pai;
- **Ignore**: conecta a uma fonte ou destino descartável, conforme a plataforma;
- **Pipe**: pai recebe um handle para ler ou escrever;
- **Close** ou equivalente: disponível conforme a versão e o fluxo.

Os identificadores exatos dos enums mudaram entre versões de Zig. Fixe a versão no CI e consulte o código-fonte da stdlib; não copie cegamente um snippet criado para outra release.

## Como enviar dados pelo stdin

Para alimentar um comando que aceita entrada padrão:

1. configure `stdin` como pipe;
2. chame `spawn`;
3. escreva todos os bytes;
4. feche o pipe para enviar EOF;
5. drene as saídas;
6. chame `wait`.

O fechamento é essencial. Muitos programas continuam esperando mais dados enquanto não recebem EOF.

Pseudocódigo próximo da API real:

```zig
var child = std.process.Child.init(
    &.{ "sha256sum", "-" },
    allocator,
);
child.stdin_behavior = .Pipe;
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;

try child.spawn();

try child.stdin.?.writeAll(payload);
child.stdin.?.close();
child.stdin = null;

// Drene stdout e stderr de forma segura antes/de forma coordenada com wait.
```

Não envie senha, token ou chave na linha de comando. Argumentos podem aparecer em listagens de processos, logs de CI e telemetria. Prefira `stdin`, arquivo com permissão restrita, descritor herdado ou mecanismo de secrets da plataforma.

## O deadlock clássico de stdout e stderr

Considere um filho que escreve 10 MiB em `stderr` antes de escrever a resposta em `stdout`. O pai faz:

1. lê `stdout` até EOF;
2. só depois lê `stderr`;
3. chama `wait`.

O pipe de `stderr` possui capacidade limitada. Quando enche, o filho bloqueia tentando escrever. Como ele está bloqueado, não fecha `stdout`. O pai espera EOF em `stdout`; o filho espera espaço em `stderr`. Deadlock.

As soluções são:

### Usar Child.run com limite

É a melhor opção para comandos curtos e saída moderada, pois a implementação da stdlib coordena a captura.

### Herdar um dos fluxos

Capture apenas o dado estruturado em `stdout` e deixe `stderr` aparecer no terminal:

```text
stdout -> pipe para o pai
stderr -> terminal herdado
```

### Redirecionar para arquivo

Para compilação, backup ou migração com logs grandes, redirecione `stderr` para um arquivo e mantenha somente um resumo na memória. Isso também preserva diagnóstico após uma falha.

### Drenar em paralelo

Crie uma thread para cada pipe ou use um event loop com `poll`, `epoll`, `kqueue` ou mecanismo equivalente. Cada consumidor aplica limite e continua drenando mesmo depois de atingir o limite armazenado — descartar o excedente é diferente de parar de ler.

Não resolva apenas aumentando o buffer. A quantidade produzida pode ser maior e o problema volta.

## Limites de memória e saída hostil

Todo comando externo deve ser tratado como uma dependência falível. Mesmo um executável confiável pode receber uma entrada que gere saída enorme.

Defina por chamada:

- máximo de bytes de `stdout`;
- máximo de bytes de `stderr`;
- política ao exceder: falhar, truncar ou salvar em arquivo;
- encoding esperado;
- prazo máximo;
- tamanho máximo da entrada;
- diretório de trabalho permitido.

Não pressuponha que a saída é UTF-8. Comandos podem gerar bytes binários, encoding do sistema ou nomes de arquivo inválidos como texto. Se a saída será logada ou incorporada em JSON, valide ou escape antes.

Para parsing estruturado, prefira uma opção estável do programa, como `--json`, `--porcelain` ou `-z`, em vez de analisar mensagens destinadas a humanos. A disciplina é a mesma do guia de [parsing e serialização em Zig](/artigos/zig-processamento-dados-parsing-serializacao/).

## Timeout: o ciclo completo

Um timeout correto não é apenas “dormir e chamar kill”. O fluxo é:

1. iniciar o filho;
2. registrar um deadline monotônico;
3. drenar os pipes enquanto monitora o prazo;
4. quando vencer, solicitar encerramento;
5. aguardar uma pequena janela de graça, se aplicável;
6. forçar a interrupção se o processo ignorar o pedido;
7. chamar `wait` para fazer o reap;
8. fechar handles e liberar buffers;
9. retornar um erro que diferencie timeout de exit code comum.

O monitor deve usar relógio monotônico, não horário civil. Mudanças de NTP ou relógio do sistema não podem estender ou encurtar o prazo.

Um esqueleto conceitual:

```zig
var child = std.process.Child.init(argv, allocator);
child.stdout_behavior = .Pipe;
child.stderr_behavior = .Pipe;
try child.spawn();

const deadline = monotonicNow() + timeout_ns;
var timed_out = false;

while (!processFinished(&child)) {
    try drainAvailableOutput(&child);

    if (monotonicNow() >= deadline) {
        timed_out = true;
        try requestTermination(&child);
        break;
    }
}

const term = try child.wait();
if (timed_out) return error.ProcessoExpirou;
try validateTerm(term);
```

As funções de consulta e encerramento variam por versão e sistema operacional. O ponto arquitetural é manter captura, relógio, sinal e `wait` no mesmo state machine.

Em Unix, há outra decisão: matar apenas o filho direto ou o **grupo de processos**. Se você iniciar `sh -c` e ele criar `compactador | uploader`, encerrar somente o shell pode deixar netos rodando. Ferramentas de automação robustas criam grupo/sessão própria e aplicam o sinal ao grupo. No Windows, o equivalente operacional costuma envolver Job Objects. Portabilidade exige uma camada específica por plataforma.

## Diretório, ambiente e PATH

Processos filhos herdam estado do pai por padrão, mas automações reproduzíveis devem ser explícitas.

### Diretório de trabalho

Não dependa de “quem chamou a CLI”. Configure o diretório do repositório, workspace ou job. Antes, valide que ele existe e que não aponta para uma área controlada por usuário quando o comando manipula arquivos sensíveis.

### Variáveis de ambiente

Construa um ambiente mínimo quando necessário. Evite repassar por acidente:

- tokens de deploy;
- credenciais de cloud;
- proxies internos;
- flags de debug;
- variáveis que mudam locale e formato de saída.

Para ferramentas que precisam do ambiente completo, remova ao menos secrets que não fazem parte do contrato do filho. Veja também o guia de [configuração via XDG e variáveis de ambiente](/artigos/zig-cli-config-xdg-env/).

### Resolução do executável

`PATH` é conveniente, mas pode ser manipulado. Em serviço privilegiado, agente de CI ou daemon, prefira caminho absoluto ou um `PATH` conhecido. Registre a versão do executável, não apenas o nome, quando a compatibilidade for crítica.

## Cleanup em todos os caminhos

Processos combinam recursos de vários tipos: PID/handle, pipes, buffers, threads leitoras e arquivos de log. Estruture a função com `errdefer` durante a inicialização e `defer` após adquirir ownership estável.

Regras práticas:

- se `spawn` funcionou, algum caminho precisa executar `wait` ou mecanismo equivalente;
- feche o `stdin` depois de enviar a entrada;
- não use um pipe depois de transferir ou fechar seu handle;
- espere threads de drain antes de liberar os buffers compartilhados;
- mate o filho em caminhos de erro somente quando o contrato exigir;
- não confie em `std.process.exit` para cleanup, pois ele pula `defer`.

O padrão de [graceful shutdown com SIGTERM](/artigos/zig-graceful-shutdown-sigterm-sigint/) também se aplica ao pai: se sua CLI receber cancelamento, decida como propagar o sinal ao filho e como evitar processos órfãos.

## Observabilidade sem vazar dados

Para diagnosticar uma execução, registre:

- nome lógico da operação;
- executável ou ferramenta;
- duração;
- exit code ou tipo de término;
- se houve timeout;
- bytes capturados em cada fluxo;
- se a saída foi truncada;
- ID do job ou correlação.

Não registre automaticamente:

- linha de comando completa com credenciais;
- conteúdo integral de `stderr` sem limite;
- variáveis de ambiente;
- payload enviado ao `stdin`;
- caminhos privados sem necessidade.

Uma mensagem útil e segura:

```text
child_process operation=git_fetch duration_ms=1842 exit_code=128 stderr_bytes=317 truncated=false
```

Associe isso a métricas de latência e falha conforme o guia de [observabilidade com logs e Prometheus](/artigos/zig-observabilidade-logs-prometheus/).

## Testes recomendados

Não teste somente `/bin/echo`. Crie um pequeno executável auxiliar para reproduzir cenários:

- sai com código configurável;
- escreve N bytes em stdout;
- escreve N bytes em stderr;
- alterna escrita nos dois fluxos;
- lê até EOF no stdin;
- dorme por um período;
- ignora um sinal em POSIX;
- cria um processo neto;
- produz bytes que não são UTF-8.

Então valide:

1. sucesso com código zero;
2. falha com código não zero;
3. executável inexistente;
4. saída acima do limite;
5. stdout e stderr volumosos ao mesmo tempo;
6. timeout e reap do filho;
7. cancelamento do processo pai;
8. argumentos com espaços e caracteres especiais;
9. ambiente mínimo;
10. ausência de leak no `std.testing.allocator`.

Esse conjunto encontra bugs que um teste feliz jamais mostra, especialmente deadlocks que só aparecem quando o pipe enche.

## Checklist de produção

- [ ] o projeto fixa a versão de Zig usada pela API de `Child`;
- [ ] argumentos são passados separadamente, sem concatenação de shell;
- [ ] `term` é validado, não apenas o retorno de `spawn`;
- [ ] stdout e stderr possuem limite ou destino de streaming;
- [ ] os dois pipes são drenados sem deadlock;
- [ ] stdin é fechado para enviar EOF;
- [ ] todo filho iniciado passa por `wait` ou reap equivalente;
- [ ] timeout usa relógio monotônico;
- [ ] pipelines e processos netos têm política de encerramento;
- [ ] diretório de trabalho é explícito;
- [ ] ambiente herdado não vaza secrets desnecessários;
- [ ] executável vem de caminho ou `PATH` confiável;
- [ ] logs não incluem tokens, payloads ou argv sensível;
- [ ] testes cobrem saída grande, falha, timeout e cancelamento;
- [ ] diferenças entre POSIX e Windows estão isoladas.

## Perguntas frequentes

### Como executar um comando externo em Zig?

Use `std.process.Child.run` para execução curta com captura em memória. Passe allocator e `argv`, libere as duas saídas e confira `term`. Para controle avançado, use `Child.init`, configure os fluxos, chame `spawn` e finalize com `wait`.

### Child.run executa por meio do Bash?

Não. Ele inicia o executável diretamente. Pipes, redirecionamentos, glob e `&&` só existem se você chamar um shell explicitamente.

### Como saber se o comando teve sucesso?

Aceite sucesso apenas quando o tipo de término e o exit code atendem ao contrato do programa. Na maioria dos comandos, isso significa encerramento normal com código zero.

### Como evitar deadlock em stdout e stderr?

Use a captura coordenada de `Child.run`, herde ou redirecione um fluxo, ou drene os dois simultaneamente. Nunca espere EOF de um pipe enquanto ignora outro que pode encher.

### Como implementar timeout?

Faça `spawn`, monitore um deadline monotônico, interrompa o processo quando vencer e sempre chame `wait`. Se houver netos, defina também uma política para o grupo de processos.

### É seguro usar sh -c com texto do usuário?

Não. Construa `argv` diretamente. Se um shell for obrigatório, mantenha o script fixo e restrinja rigorosamente os valores incorporados.

## Conclusão

Executar processos filhos em Zig é simples no caso básico e exige engenharia cuidadosa no caso real. `std.process.Child.run` resolve comandos curtos com captura limitada; `spawn` e `wait` oferecem o controle necessário para streaming, entrada, timeout e cancelamento.

O design seguro tem cinco regras: argumentos separados, validação de exit code, limites de saída, drain simultâneo de `stdout` e `stderr` e cleanup com `wait` em todos os caminhos. Com essas garantias, uma CLI Zig pode orquestrar compiladores, Git, compactadores e ferramentas de infraestrutura sem depender de um shell frágil — e sem deixar deadlocks ou processos órfãos escondidos no pipeline.

Continue com o guia de [automação e scripts em Zig](/artigos/zig-automacao-scripts-substituir-python-bash/), a referência de [`std.process`](/stdlib/std-process/) e as práticas de [error handling](/artigos/zig-error-handling-boas-praticas/) para transformar esse padrão em uma camada reutilizável do seu projeto.
