Modo Estrito do Bash: set -euo pipefail Explicado

Modo Estrito do Bash: set -euo pipefail Explicado

O Que E o Modo Estrito do Bash?

Conclusao: Coloque set -euo pipefail no inicio de um script para capturar erros, variaveis nao definidas e falhas em pipes imediatamente. O objetivo e revelar bugs ocultos cedo.

Por padrao, o bash e permissivo demais. Um comando pode falhar, uma variavel pode estar indefinida, ou um pipe pode quebrar no meio, e o script continua executando. O resultado e o acidente classico: "falhou no meio, mas executou ate o final e deixou dados corrompidos para tras."

O modo estrito do bash e o idioma bem conhecido de habilitar tres opcoes no inicio 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 referencias a variaveis nao definidas como erros
  • set -o pipefail: capturar falhas dentro de um pipeline
  • IFS=$'\n\t': prevenir acidentes de divisao de palavras (opcional, mas recomendado)

Premissas (ambiente alvo)

  • Shell: bash (#!/usr/bin/env bash)
  • pipefail e uma extensao do bash. Nao funciona em POSIX sh (dash, etc.)
  • Destinado a scripts, nao shells interativos

Por Que o Bash Padrao E Perigoso?

Conclusao: O bash padrao continua apos uma falha de comando e trata variaveis digitadas errado como strings vazias. Falhas sao ignoradas enquanto o processamento continua, e e ai que os acidentes acontecem.

Observe o script abaixo. Ele entra em um diretorio de backup e exclui arquivos antigos, um padrao muito comum.

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

Se voce esquecer de definir BACKUP_DIR, o bash padrao executa cd "" (que nao faz nada e permanece no diretorio atual), e rm -rf ./* executa no diretorio em que voce ja esta. Como cd "" tem sucesso (nao faz nada) sem gerar um erro, voce apaga um lugar que nunca pretendia.

Com o modo estrito, esse acidente e bloqueado duplamente.

  • set -u para o script na referencia a $BACKUP_DIR nao definida
  • Se $BACKUP_DIR aponta para um caminho inexistente, set -e sai imediatamente quando cd falha (uma string vazia cd "" tem sucesso, entao o caso de string vazia e tratado por set -u)

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

Conclusao: set -e sai do script no momento em que um comando retorna diferente de zero. Mas existem muitos contextos onde ele nao dispara (condicoes if, && e mais), entao nao dependa excessivamente dele.

set -e encerra o script assim que um comando retorna um status de saida 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 NAO se aplica

set -e tem contextos documentados onde nao tem efeito. Nao conhece-los leva ao acidente "deveria ter parado, mas nao parou".

  • A condicao de if cmd; then ... (a falha e usada para o teste)
  • O lado esquerdo de cmd && ... / cmd || ... (qualquer coisa exceto o ultimo comando)
  • Um comando negado com !
  • Todo comando em um pipeline exceto o ultimo (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

Nao dependa de set -e sozinho. Verifique explicitamente o status de saida de etapas criticas. Trate set -e como uma rede de seguranca para falhas que voce acidentalmente negligenciou.

Os Papeis de set -u e pipefail

Conclusao: set -u transforma referencias a variaveis nao definidas em erros para capturar erros de digitacao. set -o pipefail captura falhas no meio do pipe para que combinacoes como grep | sort relatem sucesso ou falha corretamente.

set -u (nounset)

Referenciar uma variavel indefinida se torna um erro e para o script, permitindo que voce identifique erros de digitacao em nomes de variaveis instantaneamente.

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

Quando voce intencionalmente quer "usar um padrao se nao definida", torne explicito com expansao de parametros.

# 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 padrao, o status de saida de um pipeline e o do ultimo comando. Mesmo se um comando anterior falhar, o pipe inteiro e reportado como sucesso quando o ultimo 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 saida do comando mais a direita que falhou no pipeline, ou 0 se todos tiverem sucesso. E especialmente valioso ao encadear busca e filtragem de dados.

Por Que Configurar o IFS E Recomendado?

Conclusao: IFS=$'\n\t' limita os separadores de divisao de palavras apenas a nova linha e tabulacao, prevenindo divisoes nao intencionadas em espacos em nomes de arquivos e caminhos.

IFS (Internal Field Separator) e o conjunto de caracteres que o bash usa para dividir strings em palavras. O padrao e espaco, tabulacao e nova linha. O espaco e o que causa acidentes com nomes de arquivos que contem espacos.

# 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 nao oficial do bash" de Aaron Maxwell recomenda essa configuracao de IFS junto com set -euo pipefail. Remover o espaco dos separadores faz a divisao em for-loops e expansoes se comportar de forma mais intuitiva.

Alterar IFS tem efeitos colaterais. Se algum codigo depende de divisao baseada em espaco (como ler arrays com read -a), restaure IFS localmente para aquela secao ou pule a configuracao ali.

Um Template Pratico de Modo Estrito

Conclusao: Mantenha um boilerplate que combina o shebang, modo estrito e uma trap de erro para que voce sempre possa iniciar um script seguro com esforco minimo.

Um template que voce pode usar como esta. Mostrar a linha que falhou com trap torna a depuracao dramaticamente mais facil.

#!/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 minima

#!/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 esta instalado fora de /bin (por exemplo /usr/local/bin). Usar env busca o bash no PATH, melhorando a portabilidade. Como pipefail e especifico do bash, tambem e importante nomear o bash explicitamente em vez de #!/bin/sh.

Ressalvas do Modo Estrito em um Relance

Conclusao: O modo estrito nao e uma solucao magica. Use-o sabendo os contextos onde set -e nao se aplica, os efeitos colaterais do IFS e o risco de aplica-lo retroativamente em scripts existentes.

Item Ressalva
set -e Nao tem efeito em condicoes if, lados esquerdos de && e mais
set -u Torne padroes explicitos com ${VAR:-default}
pipefail Apenas bash. Nao disponivel em POSIX sh
IFS=$'\n\t' Cuidado com efeitos colaterais se o codigo depende de divisao por espaco
Retroativo Adiciona-lo a um script antigo revela todos os bugs previamente ocultos

O que nao fazer

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

Proximas Leituras