Dicas para escrever scripts Bash mais seguros com set -euo pipefail

1. Introdução à segurança em scripts Bash

Scripts Bash são onipresentes em administração de sistemas, automação de deploys e pipelines de CI/CD. No entanto, sua natureza permissiva frequentemente leva a falhas silenciosas que passam despercebidas até causarem danos em produção. O problema fundamental é que, por padrão, o Bash continua executando um script mesmo após um comando falhar — um comportamento que pode corromper dados, deixar sistemas em estados inconsistentes ou mascarar erros críticos.

O comando set controla flags de execução que modificam esse comportamento. Compreender e utilizar corretamente set -euo pipefail transforma scripts frágeis em ferramentas robustas que param ao primeiro sinal de problema. Este artigo explora cada flag em detalhes, com exemplos práticos e armadilhas comuns.

2. Entendendo set -e: abortar em erros

A flag -e (ou errexit) instrui o Bash a interromper imediatamente a execução do script quando qualquer comando retornar um código de saída diferente de zero. Considere o exemplo abaixo sem -e:

#!/bin/bash
rm arquivo_inexistente.txt
echo "Script continua após falha"

A saída será:

rm: cannot remove 'arquivo_inexistente.txt': No such file or directory
Script continua após falha

Agora com set -e:

#!/bin/bash
set -e
rm arquivo_inexistente.txt
echo "Esta linha nunca será executada"

O script aborta imediatamente após o rm falhar, impedindo qualquer ação subsequente baseada em estado inválido.

Exceções importantes: Comandos em condicionais (if, while, until) ou operadores lógicos (&&, ||) não disparam o -e, pois seu código de saída é intencionalmente testado:

#!/bin/bash
set -e
if grep -q "padrao" arquivo.txt; then
    echo "Padrão encontrado"
fi
# O script continua normalmente, mesmo se grep retornar 1

3. Dominando set -u: variáveis não definidas como erro

O flag -u (ou nounset) trata referências a variáveis não definidas como erros fatais. Sem ele, variáveis vazias podem causar comportamentos imprevisíveis:

#!/bin/bash
# Sem set -u
echo "Diretório: $DIRETORIO_NAO_DEFINIDO"
rm -rf "$DIRETORIO_NAO_DEFINIDO"/*
# rm -rf /*  (se a variável estiver vazia, desastre!)

Com set -u:

#!/bin/bash
set -u
echo "Diretório: $DIRETORIO_NAO_DEFINIDO"
# Script aborta aqui com: ./script.sh: line 2: DIRETORIO_NAO_DEFINIDO: unbound variable

Para cenários onde valores padrão são aceitáveis, utilize a expansão ${var:-default}:

#!/bin/bash
set -u
NOME=${USUARIO:-"visitante"}
echo "Olá, $NOME"

Essa sintaxe fornece um fallback sem desabilitar a proteção para outras variáveis.

4. Aplicando set -o pipefail: falhas em pipelines

Por padrão, o Bash retorna o código de saída do último comando de um pipeline, ignorando falhas intermediárias:

#!/bin/bash
set -e
comando_inexistente | grep "qualquer" | wc -l
echo "Pipeline 'completou' com sucesso"

Mesmo com -e, o script continua porque o último comando (wc -l) retornou 0. A flag pipefail corrige isso:

#!/bin/bash
set -eo pipefail
comando_inexistente | grep "qualquer" | wc -l
# Script aborta aqui, pois comando_inexistente retornou 127

pipefail faz o pipeline retornar o código de saída do primeiro comando que falhar (da esquerda para a direita). Exemplo prático:

#!/bin/bash
set -euo pipefail

# Pipeline seguro: qualquer falha interrompe o script
curl -s https://api.exemplo.com/dados | jq '.users[] | .name' | sort > usuarios.txt
echo "Dados processados com sucesso"

5. Combinação set -euo pipefail: a tríade da segurança

Usar os três flags juntos cria uma camada robusta de proteção. O padrão recomendado é declarar logo após o shebang:

#!/bin/bash
set -euo pipefail

Script comparativo — sem proteção:

#!/bin/bash
# Script sem set -euo pipefail
ARQUIVO="dados.txt"
rm $ARQUIVO
cat $ARQUIVO | grep "erro" > saida.txt
echo "Processamento concluído"

Mesmo se rm falhar ou $ARQUIVO não existir, o script executa até o fim, possivelmente corrompendo dados.

Com proteção:

#!/bin/bash
set -euo pipefail
ARQUIVO="dados.txt"
rm "$ARQUIVO"
cat "$ARQUIVO" | grep "erro" > saida.txt
echo "Processamento concluído"

Qualquer falha — arquivo inexistente, variável vazia, comando em pipeline com erro — interrompe imediatamente o script, forçando o desenvolvedor a tratar explicitamente cada ponto de falha.

6. Armadilhas comuns e como evitá-las

Comandos que retornam códigos não-zero esperados: grep sem resultados retorna 1. Com set -e, isso aborta o script:

#!/bin/bash
set -euo pipefail
grep "texto_ausente" arquivo.txt
# Script aborta aqui

Solução: use condicionais ou || true:

#!/bin/bash
set -euo pipefail
if grep "texto_ausente" arquivo.txt; then
    echo "Encontrado"
fi
# Ou: grep "texto_ausente" arquivo.txt || true

Uso de || true: Permite ignorar falhas intencionais:

#!/bin/bash
set -euo pipefail
rm -f /tmp/lock.tmp || true  # Ignora se arquivo não existe

Problemas com source: Scripts que usam source para carregar bibliotecas podem herdar ou sobrescrever flags:

#!/bin/bash
set -euo pipefail
source ./biblioteca.sh  # Se biblioteca.sh tiver set +e, desativa proteção

Sempre verifique scripts incluídos para garantir que não desabilitam flags de segurança.

7. Técnicas avançadas de depuração e logging

Ativando set -x para rastreamento: Exibe cada comando antes de executá-lo:

#!/bin/bash
set -euo pipefail
set -x  # Modo debug

ARQUIVO="config.txt"
cp "$ARQUIVO" /backup/

Saída:

+ ARQUIVO=config.txt
+ cp config.txt /backup/

Usando trap com ERR: Captura erros para logging:

#!/bin/bash
set -euo pipefail

trap 'echo "ERRO: Linha $LINENO - Comando falhou com código $?" >&2' ERR

comando_que_falha
echo "Isso não executa"

Estratégias de logging informativo:

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/meu_script.log"
exec 2>> "$LOG_FILE"  # Redireciona stderr para log

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}

log "Iniciando processamento"
# ... comandos ...
log "Processamento concluído"

8. Conclusão e checklist de boas práticas

set -euo pipefail não é uma bala de prata, mas é a base mínima para scripts Bash confiáveis. Os benefícios são claros:

  • Detecção precoce de falhas: scripts param antes de causar danos
  • Comportamento previsível: variáveis não definidas são erros explícitos
  • Pipelines confiáveis: falhas intermediárias não são mascaradas

Checklist para revisão de scripts:
- [ ] set -euo pipefail declarado após o shebang
- [ ] Variáveis sempre entre aspas duplas ("$VAR")
- [ ] Uso de ${VAR:-default} para valores opcionais
- [ ] Tratamento explícito de comandos que retornam códigos não-zero esperados
- [ ] trap configurado para logging de erros
- [ ] Scripts incluídos via source também possuem proteções

Recomendações para CI/CD: Integre verificações de estilo com shellcheck e execute testes que forçam cenários de erro para validar o comportamento de set -euo pipefail.

Referências