kb.erickguedes.com
Bash e Shell Script: Automação Unix/Linux

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.