# O status de saída de um pipeline SSH pode esconder um comando que falhou?

Um comando remoto pode falhar, um formatador pode imprimir uma saída convincente e um agente ainda anunciar sucesso. Isso não é um mistério do SSH. É a semântica normal do shell atravessando um limite de rede sem evidências suficientes.

A solução não é apenas adicionar `set -o pipefail` a todos os scripts. O `pipefail` muda um único resultado agregado. Um agente que executa uma ação SSH importante precisa do status de cada etapa do pipeline, de uma regra definida para saídas diferentes de zero esperadas e de um código de saída remoto final que não possa ser confundido com sucesso. Capture o vetor imediatamente, dê um nome a ele e faça o wrapper decidir o que significa sucesso.

## Um comando final verde pode esconder um primeiro comando vermelho

Por padrão, um pipeline de shell informa o status de saída de seu último comando. Isso torna esta linha perigosa em tarefas de implantação, migração, backup e reparo:

```bash
build_manifest | sign_manifest | tee /var/tmp/manifest.json
```

Suponha que `build_manifest` falhe porque não consegue ler um arquivo obrigatório. `sign_manifest` pode receber uma entrada inutilizável e também falhar, ou produzir um resultado vazio. `tee` ainda pode criar um arquivo, gravar zero bytes e terminar com status zero. O shell informa zero para o pipeline inteiro. Um chamador que verifica apenas `$?` vê sucesso.

O GNU Bash Reference Manual afirma isso claramente: um pipeline usa o status de saída do último comando, a menos que `pipefail` esteja habilitado. O Bash espera todos os comandos em um pipeline síncrono, mas esperar não é o mesmo que preservar os resultados de cada comando.

Uma pessoa diante de um terminal interativo às vezes percebe os dados ausentes ou a mensagem de erro. Um agente costuma ter uma visão mais limitada. Ele pode receber um transcript truncado, um resumo formatado ou apenas o resultado final do comando. Se o script informa zero, o agente tem motivos para dizer que a ação foi concluída, mesmo quando não fez o que foi solicitado.

A distinção que as equipes costumam misturar é simples:

- O status de saída de um pipeline é um valor de decisão.
- Os status de seus comandos são as evidências por trás dessa decisão.

Você precisa dos dois. O valor de decisão controla se o comando remoto retorna sucesso. As evidências informam a um revisor, a um log ou a um agente supervisor onde ocorreu o problema.

Isso é mais importante quando a primeira etapa altera o mundo. Considere uma exportação remota que lê dados de produção, comprime, criptografa e envia o resultado. O cliente de upload pode terminar com zero depois de enviar um fluxo vazio. O transcript pode conter palavras tranquilizadoras como «concluído», porque um programa posterior concluiu sua tarefa específica. Esse resultado não pode se transformar em uma afirmação falsa de que a exportação foi bem-sucedida.

## O SSH retorna o que o shell remoto decide retornar

O OpenSSH não inspeciona os comandos dentro de um pipeline do shell remoto. Ele retorna o status do comando remoto ou 255 quando o próprio SSH encontra um erro.

Esse comportamento é correto e útil. O SSH não tem como saber se este texto remoto é um pipeline, uma função do shell, um script ou um aplicativo que usa códigos de saída à sua maneira:

```bash
ssh deploy@host 'generate | transform | tee result.txt'
```

O shell de login remoto interpreta esse comando. Se a semântica do pipeline informar o status do `tee` final, o SSH retornará esse status à máquina local. O chamador local não tem como reconstruir os resultados anteriores depois que o shell remoto os descartou.

Colocar `set -o pipefail` no shell local não corrige um pipeline executado remotamente. Este comando muda apenas as regras de status do pipeline local:

```bash
set -o pipefail
ssh deploy@host 'generate | transform | tee result.txt'
```

O shell remoto continua sendo o responsável por `generate | transform | tee result.txt`. Ele precisa de seu próprio shell explícito e de seu próprio tratamento de falhas.

Há uma segunda armadilha. Este comando local cria outro pipeline depois que o SSH retorna:

```bash
ssh deploy@host 'remote command' 2>&1 | tee session.log
```

Agora existem dois pipelines diferentes:

1. O shell remoto pode conter um pipeline dentro de `remote command`.
2. O shell local contém `ssh | tee session.log`.

Um `tee` local bem-sucedido pode esconder uma falha de transporte do SSH ou uma saída diferente de zero do wrapper remoto. Você precisa inspecionar o pipeline remoto no host e o pipeline local ao redor do SSH. Tratar a linha como um único comando opaco é como resultados falsamente verdes continuam passando por revisões.

## Pipefail detecta uma falha, mas não a explica

`set -o pipefail` muda o resultado agregado do Bash. Com ele habilitado, o Bash retorna o status do comando mais à direita que terminou com valor diferente de zero, ou zero se todos os comandos tiveram sucesso.

Para muitos scripts, isso é uma melhoria real:

```bash
set -o pipefail
produce_data | validate_data | publish_data
printf 'pipeline status: %s\n' "$?"
```

Se `produce_data` terminar com 17 e os comandos seguintes terminarem com zero, o pipeline retorna 17. Se `validate_data` terminar com 4 e `publish_data` com zero, o pipeline retorna 4. O processo chamador recebe uma falha em vez de uma mentira.

Mas o `pipefail` perde detalhes quando mais de uma etapa falha. Suponha que os status sejam `17 4 0`. O resultado do pipeline é 4, porque 4 veio do último comando que falhou. Isso informa que ocorreu uma falha, mas não prova se o validador causou a falha do produtor, reagiu a ela ou falhou de forma independente.

Por isso, `pipefail` é uma proteção, não um formato de relatório. Use-o quando quiser que um pipeline falhe como uma unidade. Use `PIPESTATUS` quando precisar responder depois a estas perguntas:

- Qual etapa retornou um código diferente de zero?
- Uma etapa posterior foi executada e teve sucesso depois que uma etapa anterior falhou?
- O processo recebeu um sinal em vez de retornar seu próprio erro?
- Um código diferente de zero é esperado para este comando específico?

Não tente encobrir essa lacuna com `|| true`:

```bash
produce_data | validate_data | publish_data || true
```

Esse padrão é popular porque mantém o script em execução. Ele também apaga o único sinal que o chamador tinha. Se uma etapa puder legitimamente retornar um valor diferente de zero, registre o status permitido para essa etapa depois de capturar o vetor real. Não ignore a falha do pipeline inteiro.

## PIPESTATUS desaparece se você esperar até mesmo um comando

O Bash expõe o código de saída de cada etapa no array `PIPESTATUS`. O array é frágil por definição: ele descreve o pipeline em foreground executado mais recentemente, e o comando seguinte pode substituí-lo.

Isto parece razoável, mas está errado:

```bash
source_data | normalize | upload
pipeline_rc=$?
printf 'pipeline result: %s\n' "$pipeline_rc"
statuses=("${PIPESTATUS[@]}")
```

Quando o Bash chega à última atribuição, a atribuição `pipeline_rc=$?` e o `printf` já foram executados. `PIPESTATUS` não descreve mais `source_data | normalize | upload`.

Copie o array primeiro, antes de fazer qualquer outra coisa:

```bash
source_data | normalize | upload
statuses=("${PIPESTATUS[@]}")
```

Depois, inspecione-o sem depender do código agregado do pipeline:

```bash
printf 'source_data=%s normalize=%s upload=%s\n' \
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}"
```

Essa também é a razão pela qual um `set -e` usado sem cuidado pode piorar o diagnóstico. Com `pipefail` ativo, um pipeline com falha pode fazer o Bash sair antes que a linha seguinte copie `PIPESTATUS`. O tratamento de erros do shell tem várias exceções que dependem do contexto, e scripts que dependem apenas de `set -e` costumam produzir menos evidências justamente quando um comando falha.

Quando os status de um pipeline são importantes, desative `errexit` nas poucas linhas necessárias para executá-lo e capturá-lo. Depois, tome uma decisão explícita. Isso exige mais código do que uma opção mágica do shell, mas é um código que você consegue ler durante um incidente.

## Execute o programa remoto sob o shell necessário

`PIPESTATUS` é um array do Bash. Ele não faz parte da sintaxe POSIX `sh`, e `pipefail` não é obrigatório no shell POSIX. Um comando remoto chamado pelo SSH pode ser executado em um shell de login que você não escolheu. Em um host pode ser Bash; em outro, `dash`, `zsh` ou um shell restrito.

Não envie sintaxe Bash para um shell remoto não especificado esperando que a máquina coincida com você. Inicie o Bash explicitamente:

```bash
ssh deploy@host 'bash -s' <<'REMOTE_SCRIPT'
printf 'alpha\n' | grep 'beta' | tee /var/tmp/example.out
statuses=("${PIPESTATUS[@]}")
printf 'stages=%s,%s,%s\n' \
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}" >&2
REMOTE_SCRIPT
```

O delimitador do heredoc entre aspas é importante. `<<'REMOTE_SCRIPT'` impede que o shell local expanda variáveis, substituições de comandos e barras invertidas antes de enviar o script. O processo Bash remoto recebe o texto que você escreveu.

No macOS, o Bash do sistema é antigo, mas oferece suporte a arrays indexados, `PIPESTATUS` e `set -o pipefail`. Isso não significa que `/bin/sh` seja Bash. Um script com `#!/bin/bash` ajuda apenas quando você executa o arquivo diretamente. Se você passar um comando de uma linha para `ssh host '...'`, o shell de login remoto ainda o interpretará, a menos que você inicie o Bash explicitamente.

Para uma automação mantida ao longo do tempo, coloque o wrapper remoto em um script versionado e invoque seu caminho absoluto. Para uma ação curta executada por um agente, `bash -s` com um heredoc entre aspas costuma ser mais fácil de auditar, porque o programa remoto completo aparece na solicitação de ação local.

## Um wrapper deve nomear as etapas e retornar um resultado honesto

Um wrapper remoto útil faz quatro coisas. Executa o pipeline, copia imediatamente o vetor de status, emite um registro legível por máquina e termina com um valor diferente de zero quando uma etapa obrigatória falha.

Este exemplo usa uma transferência de dados em três etapas. Substitua os comandos, mas mantenha o fluxo de controle. Ele não depende deliberadamente de `set -e` para decidir o que acontece depois do pipeline.

```bash
#!/usr/bin/env bash
set -uo pipefail

run_export() {
  local -a status
  local stage
  local -a names=("collect" "compress" "send")

  set +e
  collect_records | gzip -c | send_archive --destination daily
  status=("${PIPESTATUS[@]}")
  set -e

  if ((${#status[@]} != ${#names[@]})); then
    printf 'agent_pipeline_error pipeline=export reason=status_count expected=%s got=%s\n' \
      "${#names[@]}" "${#status[@]}" >&2
    return 70
  fi

  for stage in "${!names[@]}"; do
    printf 'agent_pipeline_status pipeline=export stage=%s code=%s\n' \
      "${names[$stage]}" "${status[$stage]}" >&2
  done

  for stage in "${!status[@]}"; do
    if (( status[stage] != 0 )); then
      printf 'agent_pipeline_result pipeline=export outcome=failed\n' >&2
      return "${status[$stage]}"
    fi
  done

  printf 'agent_pipeline_result pipeline=export outcome=ok\n' >&2
  return 0
}

run_export
```

Uma coleta que falha com um compressor e um remetente bem-sucedidos produz uma saída deste tipo:

```text
agent_pipeline_status pipeline=export stage=collect code=23
agent_pipeline_status pipeline=export stage=compress code=0
agent_pipeline_status pipeline=export stage=send code=0
agent_pipeline_result pipeline=export outcome=failed
```

O wrapper termina com 23. O SSH retorna 23 ao processo local. O agente pode informar que a exportação falhou em `collect`, mesmo que `send_archive` tenha exibido uma mensagem de conclusão para um fluxo vazio.

O código exato retornado é menos importante do que a disciplina por trás dele. Neste wrapper, o primeiro status diferente de zero na ordem do pipeline vence. O `pipefail` do Bash seleciona o status diferente de zero mais à direita. Qualquer uma das políticas pode funcionar, desde que você a declare e teste. Para operações, prefiro a primeira etapa que falhou, porque ela costuma apontar mais perto da causa inicial. Mantenha o vetor completo de status no registro da ação para que ninguém precise deduzir o que aconteceu a partir de um único número.

Os nomes das etapas não são decoração. `0=23,1=0,2=0` obriga alguém a reabrir o script. `collect=23,compress=0,send=0` permite que um supervisor encaminhe a falha, acrescente contexto ou decida se uma nova tentativa é segura.

## O registro local pode criar um segundo sucesso falso

Os operadores querem um transcript local. Os agentes também. A maneira ingênua de obtê-lo é esta:

```bash
ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log
```

Se o SSH retornar 23, mas o `tee` local gravar o transcript e retornar zero, o pipeline local retornará zero por padrão. Você corrigiu a mentira remota e introduziu uma mentira local.

Capture também os status locais:

```bash
set +e
ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log
local_status=("${PIPESTATUS[@]}")
set -e

ssh_rc=${local_status[0]}
tee_rc=${local_status[1]}
printf 'ssh=%s tee=%s\n' "$ssh_rc" "$tee_rc" >&2

if (( ssh_rc != 0 )); then
  exit "$ssh_rc"
fi
if (( tee_rc != 0 )); then
  exit "$tee_rc"
fi
```

Não habilite o `pipefail` local e pare por aí. Ele fornece um resultado agregado diferente de zero se `ssh` ou `tee` falhar, o que é melhor do que o comportamento padrão. Mas não informa ao agente se a ação remota falhou, se a conexão de rede falhou ou se o registro local falhou. Esses casos levam a decisões diferentes.

Um status SSH de 255 exige tratamento especial. O OpenSSH reserva esse valor para um erro no caminho do cliente SSH, não para o resultado de um comando remoto. Um wrapper deve informá-lo como uma falha de transporte ou de execução do SSH, não afirmar que uma etapa remota nomeada retornou 255.

Há outro motivo prático para manter os resultados locais e remotos separados. Um transcript pode conter vários registros de pipelines remotos, avisos do shell de login e um diagnóstico do SSH. Se um agente analisar texto livre procurando o último número que aparece, cedo ou tarde escolherá o número errado. Use registros reconhecíveis e vincule o resultado final da ação ao status real de saída do processo.

## SIGPIPE precisa de uma exceção escrita, não de um perdão geral

`pipefail` revela uma falha que muitos scripts ignoravam: SIGPIPE. No Bash, um processo terminado pelo sinal `N` recebe o status `128 + N`; SIGPIPE normalmente aparece como 141.

Um caso clássico e intencional é:

```bash
generate_many_lines | head -n 10
```

`head` lê dez linhas e termina com sucesso. O gerador pode continuar escrevendo, receber SIGPIPE porque não há mais leitor e terminar com 141. Com `pipefail`, o pipeline pode parecer ter falhado, embora a amostra de dez linhas solicitada tenha sido produzida.

Isso não torna 141 inofensivo em todos os pipelines. Um cliente de rede, compressor ou produtor de dados pode receber SIGPIPE porque um consumidor downstream inesperado caiu ou rejeitou a entrada. Se você marcar todos os status 141 como sucesso, esconderá uma transferência interrompida.

A regra correta é específica: permita um status derivado de sinal apenas para uma etapa cuja interrupção antecipada faça parte do contrato pretendido do comando. Coloque essa exceção junto da etapa, não em uma configuração global do shell.

Por exemplo, um wrapper para uma prévia intencional pode aceitar `generate_many_lines=141` somente quando `head=0`:

```bash
if (( status[0] == 141 && status[1] == 0 )); then
  printf 'agent_pipeline_result pipeline=preview outcome=ok reason=expected_sigpipe\n' >&2
  return 0
fi
```

Qualquer outro resultado diferente de zero continua sendo uma falha. Essa pequena dose de especificidade evita uma reação exagerada comum: alguém habilita `pipefail`, vê um 141 barulhento uma vez e depois o desabilita em todo o ambiente de automação.

## Um agente precisa de evidências separadas da saída do comando

O agente não deve determinar o sucesso lendo prosa. Os comandos imprimem palavras de sucesso antes de falhar, as ferramentas misturam avisos com resultados e um script remoto pode emitir uma última linha depois que uma etapa já deu errado.

Defina um contrato de ação em duas camadas:

1. O código de saída do processo decide se a ação solicitada teve sucesso.
2. Registros estruturados de status explicam cada etapa relevante do pipeline.

Mantenha a saída normal dos comandos disponível para depuração, mas não peça ao agente que deduza o fluxo de controle a partir dela. No wrapper acima, o stderr carrega registros que começam com `agent_pipeline_status` e `agent_pipeline_result`. Um programa chamador pode preservar esse fluxo, analisar apenas esses registros exatos e ainda mostrar o restante a uma pessoa.

Não confie em um marcador apenas porque ele aparece em uma saída de comando não confiável. Se uma etapa do pipeline manipula dados fornecidos por outro usuário ou sistema, esses dados podem conter uma linha parecida com seu registro de status. O padrão mais seguro é fazer o wrapper capturar a saída das etapas e emitir os registros depois que o pipeline terminar. Para tarefas de maior risco, use um arquivo de resultado dedicado com permissões restritivas. Depois, faça o wrapper lê-lo e validá-lo antes de emitir um único registro final.

O relatório do agente deve incluir o código de saída remoto, o código de saída local do SSH e os status das etapas remotas nomeadas, quando disponíveis. Também deve distinguir estes resultados:

- a ação remota foi executada e uma etapa nomeada falhou;
- o wrapper remoto não conseguiu produzir um registro de status completo;
- o SSH não conseguiu estabelecer ou manter o canal da ação;
- a captura local do transcript falhou depois que a ação remota foi concluída.

Esses são fatos operacionalmente diferentes. Uma nova tentativa depois de uma perda de rede pode duplicar uma alteração remota já concluída. Uma nova tentativa depois de uma etapa de validação que falhou pode ser segura. Uma nova tentativa depois de um `tee` local com falha pode ser inútil, porque o trabalho remoto já aconteceu.

O Sallyport pode manter a credencial SSH fora do agente enquanto executa a ação SSH, mas o comando remoto ainda precisa desse contrato honesto de saída e evidências.

## Teste os caminhos de falha antes que um agente os alcance

Um wrapper de shell só merece confiança depois de falhar de maneiras controladas. Testar o caminho feliz comprova o ramo menos interessante.

Crie comandos descartáveis que retornem os status que você quer observar:

```bash
fail_23() { printf 'collector failed\n' >&2; return 23; }
pass_through() { cat; }
succeed() { cat >/dev/null; return 0; }

set +e
fail_23 | pass_through | succeed
status=("${PIPESTATUS[@]}")
set -e
printf 'observed=%s,%s,%s\n' "${status[0]}" "${status[1]}" "${status[2]}"
```

O resultado esperado é `23,0,0`. Depois, execute o mesmo padrão pela invocação SSH exata usada pelo agente. Não pare em um teste no shell local, porque a seleção do shell remoto, as aspas do heredoc, o pipeline local de registro e o comportamento de saída do wrapper ficam fora dessa primeira verificação.

Teste pelo menos estes casos:

- todas as etapas têm sucesso e o wrapper termina com zero;
- uma etapa inicial falha enquanto as etapas seguintes retornam zero;
- uma etapa intermediária falha depois de consumir parte da entrada;
- o SSH não consegue se conectar ou autenticar;
- o `tee` local não consegue gravar o transcript;
- um pipeline deliberado com `head` aciona a regra esperada para SIGPIPE.

Registre o código de saída esperado e os registros de etapas esperados para cada caso. Se um teste informar sucesso depois que uma etapa anterior retornou um valor diferente de zero, o wrapper não cumpriu sua função.

O atalho tentador é fazer o agente inspecionar um transcript depois de cada ação e julgar se a saída «parece correta». Isso falha sob carga, quando as ferramentas mudam suas mensagens e quando a saída é truncada. Os códigos de saída são o canal de controle. Os registros das etapas são o canal de evidências. Mantenha-os separados, preserve ambos através do SSH e não permita que um `tee` final decida se uma ação remota aconteceu.
