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
- GNU Bash Manual: The Set Builtin — Documentação oficial do comando
sete todas as suas opções, incluindo-e,-uepipefail - ShellCheck - SC2251 — Ferramenta de análise estática que detecta problemas comuns em scripts Bash, com explicações detalhadas sobre cada flag de segurança
- BashPitfalls - Greg's Wiki — Compilação de armadilhas comuns em Bash, incluindo exemplos de como
set -epode falhar em cenários complexos - Unofficial Bash Strict Mode — Artigo clássico que popularizou o uso de
set -euo pipefaileIFSpara scripts mais seguros - Advanced Bash-Scripting Guide: Exit and Exit Status — Guia avançado sobre códigos de saída, tratamento de erros e boas práticas em scripts Bash
- Bash Reference Manual: Pipelines — Documentação oficial sobre o comportamento de pipelines e a opção
pipefail - Shell Scripting Tutorial: Error Handling — Tutorial prático sobre tratamento de erros em shell scripts, com exemplos de
set -eetrap