Depuração e Boas Práticas
Aula 5 de 6
Strict Mode
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
set -e (errexit)
# Sai imediatamente se qualquer comando retornar não-zero
set -e
# Exemplo: script para sem -e
cp /etc/config.conf /backup/
rm /etc/config.conf # executa mesmo se cp falhar!
# Com -e: para no primeiro erro
set -e
cp /etc/config.conf /backup/ # se falhar, script para
set -u (nounset)
# Gera erro ao usar variáveis não definidas
set -u
echo "$USER" # ok
echo "$VAR_NAO_EXISTE" # erro: unbound variable
# Fornecer valor padrão (evita o erro)
echo "${VAR_NAO_EXISTE:-default}"
set -x (xtrace)
# Mostra cada comando antes de executar (debug)
set -x
echo "Debug ativo"
ls -la
set +x # desativar trace
# Usar temporariamente
set -x
comando_problematico # ver exatamente o que executa
set +x
trap — Armadilhas
EXIT
# Executa sempre que o script terminar (qualquer motivo)
cleanup() {
echo "Limpando recursos..."
rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
# Inline
trap 'rm -f "$TEMP_FILE"' EXIT
ERR
# Executa em qualquer erro (com set -e)
erro_handler() {
echo "Erro na linha $1, comando: $2"
echo "Exit code: $?"
}
trap 'erro_handler $LINENO "$BASH_COMMAND"' ERR
SIGINT e sinais
# Capturar Ctrl+C
trap 'echo -e "\nOperação cancelada"; exit 130' SIGINT
# Limpeza ao receber sinal
cleanup() {
echo "Encerrando graciosamente..."
kill $PID 2>/dev/null || true
exit 0
}
trap cleanup SIGTERM SIGINT SIGHUP
Debugging Avançado
bash -x
# Executar script com debug
bash -x script.sh
# Debug parcial
bash -x -c '
set -e
echo "Teste"
ls /naoexiste
echo "Não chega aqui"
'
PS4 — Prompt de Debug
# Personalizar prompt do xtrace
export PS4='+ [${BASH_SOURCE}:${LINENO}] ${FUNCNAME[0]:+${FUNCNAME[0]}():} '
set -x
# + [script.sh:5] main(): echo "debug"
bash -n (noexec)
# Verificar sintaxe sem executar
bash -n script.sh
# Em CI: verificar todos os scripts
for script in $(find . -name "*.sh"); do
bash -n "$script" || echo "Erro de sintaxe: $script"
done
ShellCheck
Instalação
# apt
sudo apt install shellcheck -y
# brew
brew install shellcheck
# Verificar script
shellcheck script.sh
Warnings Comuns
# SC2086: Double quote to prevent globbing
# Errado:
rm -rf $DIR
# Correto:
rm -rf "$DIR"
# SC2002: Useless cat
# Errado:
cat arquivo | grep padrao
# Correto:
grep padrao < arquivo
grep padrao arquivo
# SC2012: Use find instead of ls
# Errado:
for f in $(ls *.txt)
# Correto:
for f in *.txt
# SC2034: Variável não usada
# Remover ou usar a variável
# SC2046: Quote para word splitting
# Errado:
rm $(find . -name "*.tmp")
# Correto:
find . -name "*.tmp" -delete
# SC2164: Use || após cd
# Errado:
cd diretorio
rm -rf *
# Correto:
cd diretorio || exit 1
rm -rf *
ShellCheck em CI
# .github/workflows/shellcheck.yml
- name: ShellCheck
run: shellcheck --severity=warning $(find . -name "*.sh")
Error Handling Patterns
# Operador ||: fallback em caso de erro
comando || true # ignora erro
comando || echo "Falhou" # log e continua
comando || exit 1 # log e para
comando || return $? # retorna erro da função
# ${VAR?}: erro se variável não setada
echo "${NOME:?Variável NOME não definida}"
# Verificar dependências
for cmd in curl jq git; do
if ! command -v "$cmd" &>/dev/null; then
echo "Erro: $cmd não encontrado" >&2
exit 1
fi
done
# Verificar exit code manualmente
if ! comando; then
echo "Falha ao executar comando" >&2
exit 1
fi
Idempotência
# Scripts devem poder executar múltiplas vezes sem efeito colateral
# Ruim: falha se diretório já existe
mkdir /backup
# Bom: ignora se já existe
mkdir -p /backup
# Ruim: adiciona linha repetida
echo "127.0.0.1 localhost" >> /etc/hosts
# Bom: verifica antes de adicionar
grep -q "127.0.0.1 localhost" /etc/hosts || \
echo "127.0.0.1 localhost" >> /etc/hosts
Script Template
cat > template.sh << 'TEMPLATE'
#!/bin/bash
#
# Nome: template.sh
# Descrição: Template de script bash com boas práticas
# Uso: ./template.sh [opções]
set -euo pipefail
IFS=$'\n\t'
# ─── Constantes ───────────────────────────────────────────────
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
readonly VERSION="1.0.0"
# ─── Cores ────────────────────────────────────────────────────
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# ─── Logging ──────────────────────────────────────────────────
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
# ─── Cleanup ──────────────────────────────────────────────────
CLEANUP_DIRS=()
cleanup() {
for dir in "${CLEANUP_DIRS[@]}"; do
[[ -d "$dir" ]] && rm -rf "$dir"
done
}
trap cleanup EXIT
# ─── Erro handler ─────────────────────────────────────────────
error_handler() {
log_error "Erro na linha $1"
exit 1
}
trap 'error_handler $LINENO' ERR
# ─── Help ─────────────────────────────────────────────────────
usage() {
cat << EOF
Uso: $SCRIPT_NAME [opções]
Opções:
-d, --dir DIR Diretório de trabalho
-v, --verbose Modo verboso
-h, --help Mostra esta ajuda
--version Mostra versão
Exemplo:
$SCRIPT_NAME -d /caminho
EOF
exit 0
}
# ─── Parse de argumentos ──────────────────────────────────────
VERBOSE=false
WORK_DIR=""
while [[ $# -gt 0 ]]; do
case "$1" in
-d|--dir)
WORK_DIR="$2"
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-h|--help)
usage
;;
--version)
echo "$VERSION"
exit 0
;;
*)
log_error "Opção desconhecida: $1"
usage
;;
esac
done
# ─── Validações ──────────────────────────────────────────────
[[ -n "$WORK_DIR" ]] || { log_error "--dir é obrigatório"; exit 1; }
[[ -d "$WORK_DIR" ]] || { log_error "Diretório não existe: $WORK_DIR"; exit 1; }
# ─── Função principal ────────────────────────────────────────
main() {
log_info "Iniciando $SCRIPT_NAME v$VERSION"
$VERBOSE && set -x
# ... lógica principal ...
log_info "Concluído com sucesso!"
}
main "$@"
TEMPLATE
chmod +x template.sh
Lab: Debug de um Script Quebrado
# Script com bugs propositais
cat > script-bugado.sh << 'SCRIPT'
#!/bin/bash
# Script com erros para depurar
set -e
CONFIG_FILE="config.txt"
# erro: não verifica existência
cat $CONFIG_FILE
# erro: não usa aspas
DIR=/tmp/minha pasta
ls $DIR
# erro: variável não definida
echo $NAO_EXISTE
# erro: não verifica cd
cd /diretorio/que/nao/existe
rm -rf *
echo "Fim do script"
SCRIPT
echo ""
echo "=== TESTE 1: Execução normal ==="
bash script-bugado.sh 2>&1 || true
echo ""
echo "=== TESTE 2: ShellCheck ==="
shellcheck script-bugado.sh 2>&1 || true
echo ""
echo "=== TESTE 3: Debug com xtrace ==="
bash -x script-bugado.sh 2>&1 || true
echo ""
echo "=== SCRIP CORRIGIDO ==="
cat > script-corrigido.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
CONFIG_FILE="config.txt"
# Verifica existência
[ -f "$CONFIG_FILE" ] || { echo "Arquivo não encontrado"; exit 1; }
cat "$CONFIG_FILE"
# Aspas em nomes com espaços
DIR="/tmp/minha pasta"
ls "$DIR" 2>/dev/null || echo "Diretório não existe"
# Variável com valor padrão ou erro
echo "${NAO_EXISTE:-default_value}"
# Verifica cd antes de prosseguir
cd /diretorio/que/nao/existe 2>/dev/null || echo "Diretório inacessível, pulando"
echo "Fim do script corrigido"
SCRIPT
chmod +x script-corrigido.sh
bash script-corrigido.sh
set -euo pipefail + IFS=$'\n\t' é o strict mode padrão. trap captura sinais e garante cleanup. ShellCheck previne armadilhas comuns. Idempotência torna scripts seguros para repetição.