Modo Estrito do Bash: set -euo pipefail Explicado

Modo Estrito do Bash: set -euo pipefail Explicado

O Que É o Modo Estrito do Bash?

Conclusão: Coloque set -euo pipefail no início de um script para capturar erros, variáveis não definidas e falhas em pipes imediatamente. O objetivo é revelar bugs ocultos cedo.

Por padrão, o bash é permissivo demais. Um comando pode falhar, uma variável pode estar indefinida, ou um pipe pode quebrar no meio, e o script continua executando. O resultado é o acidente clássico: "falhou no meio, mas executou até o final e deixou dados corrompidos para trás."

O modo estrito do bash é o idioma bem conhecido de habilitar três opções no início de um script para mudar o bash para o comportamento "parar em caso de falha".

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

As quatro partes abordadas aqui

  • set -e (errexit): sair imediatamente em caso de falha de comando
  • set -u (nounset): tratar referências a variáveis não definidas como erros
  • set -o pipefail: capturar falhas dentro de um pipeline
  • IFS=$'\n\t': prevenir acidentes de divisão de palavras (opcional, mas recomendado)

Premissas (ambiente alvo)

  • Shell: bash (#!/usr/bin/env bash)
  • pipefail é uma extensão do bash. Não funciona em POSIX sh (dash, etc.)
  • Destinado a scripts, não shells interativos

Por Que o Bash Padrão É Perigoso?

Conclusão: O bash padrão continua após uma falha de comando e trata variáveis digitadas errado como strings vazias. Falhas são ignoradas enquanto o processamento continua, e é aí que os acidentes acontecem.

Observe o script abaixo. Ele entra em um diretório de backup e exclui arquivos antigos, um padrão muito comum.

#!/usr/bin/env bash
cd "$BACKUP_DIR"
rm -rf ./*

Se você esquecer de definir BACKUP_DIR, o bash padrão executa cd "" (que não faz nada e permanece no diretório atual), e rm -rf ./* executa no diretório em que você já está. Como cd "" tem sucesso (não faz nada) sem gerar um erro, você apaga um lugar que nunca pretendia.

Com o modo estrito, esse acidente é bloqueado duplamente.

  • set -u para o script na referência a $BACKUP_DIR não definida
  • Se $BACKUP_DIR aponta para um caminho inexistente, set -e sai imediatamente quando cd falha (uma string vazia cd "" tem sucesso, então o caso de string vazia é tratado por set -u)

O Que set -e (errexit) Faz, e Onde Falha?

Conclusão: set -e sai do script no momento em que um comando retorna diferente de zero. Mas existem muitos contextos onde ele não dispara (condições if, && e mais), então não dependa excessivamente dele.

set -e encerra o script assim que um comando retorna um status de saída diferente de zero.

set -e
cp important.conf /etc/myapp/   # para aqui se falhar
systemctl restart myapp          # executa apenas se o anterior teve sucesso

Onde errexit NÃO se aplica

set -e tem contextos documentados onde não tem efeito. Não conhecê-los leva ao acidente "deveria ter parado, mas não parou".

  • A condição de if cmd; then ... (a falha é usada para o teste)
  • O lado esquerdo de cmd && ... / cmd || ... (qualquer coisa exceto o último comando)
  • Um comando negado com !
  • Todo comando em um pipeline exceto o último (coberto pelo pipefail)
set -e
# NAO para mesmo se grep falhar (e uma condicao)
if grep -q pattern file.txt; then
    echo "found"
fi

# Para tolerar intencionalmente uma falha, torne explicito com || true
risky_command || true

Não dependa de set -e sozinho. Verifique explicitamente o status de saída de etapas críticas. Trate set -e como uma rede de segurança para falhas que você acidentalmente negligenciou.

Os Papéis de set -u e pipefail

Conclusão: set -u transforma referências a variáveis não definidas em erros para capturar erros de digitação. set -o pipefail captura falhas no meio do pipe para que combinações como grep | sort relatem sucesso ou falha corretamente.

set -u (nounset)

Referenciar uma variável indefinida se torna um erro e para o script, permitindo que você identifique erros de digitação em nomes de variáveis instantaneamente.

set -u
name="penguin"
echo "$nmae"   # erro de digitacao -> sai com "unbound variable"

Quando você intencionalmente quer "usar um padrão se não definida", torne explícito com expansão de parâmetros.

# Usar um padrao sem erro quando nao definida
echo "${OPTIONAL_VAR:-default}"

# Mesmo para parametros posicionais (uma protecao quando $1 esta ausente)
target="${1:-/tmp}"

set -o pipefail

Por padrão, o status de saída de um pipeline é o do último comando. Mesmo se um comando anterior falhar, o pipe inteiro é reportado como sucesso quando o último tem sucesso.

# Sem pipefail: $? e 0 se grep tem sucesso, mesmo quando curl falha
curl -s https://example.com/data | grep "key"

set -o pipefail
# Com pipefail: a falha do curl se propaga como uma falha do pipe
curl -s https://example.com/data | grep "key"

pipefail retorna o status de saída do comando mais à direita que falhou no pipeline, ou 0 se todos tiverem sucesso. É especialmente valioso ao encadear busca e filtragem de dados.

Por Que Configurar o IFS É Recomendado?

Conclusão: IFS=$'\n\t' limita os separadores de divisão de palavras apenas a nova linha e tabulação, prevenindo divisões não intencionadas em espaços em nomes de arquivos e caminhos.

IFS (Internal Field Separator) é o conjunto de caracteres que o bash usa para dividir strings em palavras. O padrão é espaço, tabulação e nova linha. O espaço é o que causa acidentes com nomes de arquivos que contêm espaços.

# IFS padrao: "my file.txt" se divide em duas palavras no espaco
for f in $(ls); do
    echo "$f"
done

# IFS=$'\n\t': dividir apenas em nova linha e tabulacao
IFS=$'\n\t'

O "modo estrito não oficial do bash" de Aaron Maxwell recomenda essa configuração de IFS junto com set -euo pipefail. Remover o espaço dos separadores faz a divisão em for-loops e expansões se comportar de forma mais intuitiva.

Alterar IFS tem efeitos colaterais. Se algum código depende de divisão baseada em espaço (como ler arrays com read -a), restaure IFS localmente para aquela seção ou pule a configuração ali.

Um Template Prático de Modo Estrito

Conclusão: Mantenha um boilerplate que combina o shebang, modo estrito e uma trap de erro para que você sempre possa iniciar um script seguro com esforco mínimo.

Um template que você pode usar como está. Mostrar a linha que falhou com trap torna a depuração dramaticamente mais fácil.

#!/usr/bin/env bash
#
# Esqueleto de script seguro
#
set -euo pipefail
IFS=$'\n\t'

# Imprimir o numero da linha em caso de erro
trap 'echo "Error on line $LINENO" >&2' ERR

main() {
    local target="${1:-/tmp}"
    echo "Target: $target"
    # logica principal vai aqui
}

main "$@"

Copiar e colar: forma mínima

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Por que usar `#!/usr/bin/env bash`?

Escrever #!/bin/bash diretamente quebra em sistemas onde o bash está instalado fora de /bin (por exemplo /usr/local/bin). Usar env busca o bash no PATH, melhorando a portabilidade. Como pipefail é específico do bash, também é importante nomear o bash explicitamente em vez de #!/bin/sh.

Ressalvas do Modo Estrito em um Relance

Conclusão: O modo estrito não é uma solução mágica. Use-o sabendo os contextos onde set -e não se aplica, os efeitos colaterais do IFS e o risco de aplicá-lo retroativamente em scripts existentes.

Item Ressalva
set -e Não tem efeito em condições if, lados esquerdos de && e mais
set -u Torne padrões explícitos com ${VAR:-default}
pipefail Apenas bash. Não disponível em POSIX sh
IFS=$'\n\t' Cuidado com efeitos colaterais se o código depende de divisão por espaço
Retroativo Adicioná-lo a um script antigo revela todos os bugs previamente ocultos

O que não fazer

  • Usar pipefail em um script #!/bin/sh
  • Referenciar $1 sem verificação sob set -u
  • Adicionar modo estrito a um script grande existente sem testar

Próximas Leituras