Depurar scripts é uma habilidade fundamental para qualquer desenvolvedor que trabalhe com shell script. Erros de sintaxe, variáveis não definidas, expansões inesperadas e lógica incorreta são problemas comuns que podem consumir muito tempo se não forem abordados com as ferramentas certas. Nesta aula, você aprenderá as principais técnicas de depuração no Bash: desde a execução passo a passo com bash -x, passando pelo controle granular com set -x e set +x, até a implementação de logging e o uso do analisador estático ShellCheck. Ao final, você será capaz de identificar e corrigir bugs de forma mais rápida e confiável.

bash -x

O modo mais simples e direto para depurar um script Bash é executá-lo com a opção -x (ou --xtrace). Quando você roda bash -x script.sh, o shell exibe cada comando antes de executá-lo, precedido por um sinal de mais (+). Isso mostra exatamente o que está sendo executado, incluindo expansões de variáveis, substituições de comandos e redirecionamentos. É como ter um rastro de execução que revela o fluxo real do script.

Por exemplo, considere o script teste.sh:

#!/bin/bash
nome="João"
echo "Olá, $nome"

Ao executar bash -x teste.sh, a saída será algo como:

+ nome='João'
+ echo 'Olá, João'
Olá, João

Perceba que as variáveis já aparecem expandidas. Isso ajuda a identificar problemas como espaços inesperados, caracteres especiais ou variáveis não definidas. O modo -x é especialmente útil para scripts longos, pois você pode ver exatamente onde o erro ocorre. Uma dica prática: combine -x com -v (verbose) para também ver as linhas do script antes da execução (bash -xv script.sh).

set -x e set +x

Enquanto bash -x ativa o trace para todo o script, às vezes queremos depurar apenas uma parte específica. Para isso, usamos set -x para iniciar o trace e set +x para interrompê-lo. Esses comandos podem ser colocados em qualquer lugar do script, permitindo um controle fino sobre quais trechos serão rastreados.

Exemplo:

#!/bin/bash
echo "Início do script"
set -x  # ativa trace a partir daqui
for i in 1 2 3; do
    echo "Número: $i"
done
set +x  # desativa trace
echo "Fim do script"

Ao executar, apenas o loop será exibido com +. Isso reduz o ruído e foca no trecho problemático. Você pode ativar e desativar várias vezes ao longo do script. Outra opção é usar set -o xtrace e set +o xtrace, que são equivalentes. Lembre-se de que set -x é herdado por funções e subshells, a menos que seja desativado dentro deles.

Logging

Em scripts mais complexos ou que rodam em produção, o trace pode gerar muita saída e não é prático para análise posterior. Uma alternativa é implementar logging manual, ou seja, adicionar mensagens informativas em pontos estratégicos do script, geralmente com timestamps e níveis de severidade (INFO, WARN, ERROR). Isso permite que você acompanhe o que aconteceu sem precisar executar o script interativamente.

Exemplo simples de função de log:

#!/bin/bash
LOG_FILE="script.log"
log() {
    local level="$1"
    shift
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $*" | tee -a "$LOG_FILE"
}

log "INFO" "Iniciando script"
# ... comandos ...
if [ ! -d "/tmp/dados" ]; then
    log "ERROR" "Diretório /tmp/dados não existe"
    exit 1
fi
log "INFO" "Script concluído com sucesso"

Você pode enriquecer o log com variáveis de ambiente, códigos de retorno e até mesmo redirecionar stderr para o log. Uma boa prática é usar exec para redirecionar toda a saída do script para um arquivo de log, mas cuidado para não perder a saída interativa. O logging é essencial para scripts que rodam em cron jobs ou em segundo plano.

shellcheck (visão geral)

ShellCheck é uma ferramenta de análise estática para scripts shell. Ela não executa o script, mas examina o código fonte em busca de erros comuns, más práticas e possíveis bugs. É como um linter para Bash. ShellCheck pode ser usado online (shellcheck.net), instalado via gerenciador de pacotes (ex: sudo apt install shellcheck) ou integrado em editores como VSCode, Vim e Emacs.

Exemplo de uso:

shellcheck script.sh

ShellCheck emite avisos e erros com códigos como SC2002 (uso inútil de cat) ou SC2086 (expansão de variável sem aspas). Por exemplo, para o script:

#!/bin/bash
nome=$1
if [ $nome = "João" ]
then
    echo "Olá"
fi

ShellCheck apontaria que a variável $nome deve estar entre aspas duplas para evitar splitting e globbing, e que o = dentro de [ ] deveria ser == (embora = funcione, é ambíguo). Corrigindo:

#!/bin/bash
nome="$1"
if [ "$nome" = "João" ]; then
    echo "Olá"
fi

ShellCheck é uma ferramenta indispensável para escrever scripts robustos e portáveis. Incorpore-a no seu fluxo de trabalho: execute antes de testar e antes de commit. A documentação oficial explica cada aviso em detalhes.

Boas práticas e observações finais

Depurar é uma arte que combina técnica e paciência. Além das ferramentas apresentadas, lembre-se de:

  • Usar echo ou printf para exibir valores de variáveis em pontos críticos.
  • Verificar códigos de retorno com $? após comandos importantes.
  • Utilizar trap para capturar sinais e depurar em caso de erro (ex: trap 'echo "Erro na linha $LINENO"' ERR).
  • Manter o script organizado com funções e indentação consistente.
  • Testar com entradas variadas, incluindo casos extremos (espaços, caracteres especiais, variáveis vazias).

Combinando bash -x, set -x, logging e ShellCheck, você estará preparado para enfrentar qualquer bug. Pratique com scripts reais e veja como a depuração se torna mais eficiente.

Referências

Exercícios

  1. Crie um script que contenha um erro proposital (ex: variável sem aspas que cause splitting). Use bash -x para identificar o erro e corrija-o.

    ✓ Resposta: Exemplo de script com erro:
    #!/bin/bash
    # script: erro.sh
    dir="/tmp/pasta com espaco"
    ls $dir   # erro: sem aspas, $dir será expandido como dois argumentos
    
    Ao executar bash -x erro.sh, vemos:
    + dir='/tmp/pasta com espaco'
    + ls /tmp/pasta com espaco
    ls: cannot access '/tmp/pasta': No such file or directory
    ls: cannot access 'com': No such file or directory
    ls: cannot access 'espaco': No such file or directory
    
    Corrigido: ls "$dir".
  2. Escreva um script que use set -x e set +x para depurar apenas um loop que calcula fatorial. Mostre a saída do trace.

    ✓ Resposta:
    #!/bin/bash
    n=5
    fatorial=1
    echo "Calculando fatorial de $n"
    set -x
    for (( i=1; i<=n; i++ )); do
        fatorial=$((fatorial * i))
    done
    set +x
    echo "Resultado: $fatorial"
    
    Saída do trace:
    + (( i=1 ))
    + (( i<=n ))
    + fatorial=1
    + (( i++ ))
    + (( i<=n ))
    + fatorial=2
    + (( i++ ))
    ...
    
  3. Implemente uma função de logging que escreva em um arquivo e também exiba no terminal, com níveis INFO, WARN e ERROR. Use-a em um script simples.

    ✓ Resposta:
    #!/bin/bash
    LOG_FILE="meu_script.log"
    log() {
        local level="$1"
        shift
        local message="$(date '+%Y-%m-%d %H:%M:%S') [$level] $*"
        echo "$message" | tee -a "$LOG_FILE"
    }
    
    log "INFO" "Iniciando script"
    if [ ! -f "/etc/passwd" ]; then
        log "ERROR" "Arquivo /etc/passwd não encontrado"
    else
        log "INFO" "Arquivo /etc/passwd existe"
    fi
    log "INFO" "Script finalizado"
    
  4. Instale o ShellCheck (ou use a versão online) e analise o script abaixo. Liste pelo menos dois problemas apontados e corrija-os.

    #!/bin/bash
    for i in $(ls *.txt); do
        echo $i
    done
    

    ✓ Resposta: ShellCheck aponta:
    • SC2045: Iterating over 'ls' output is fragile. Use globs. (Melhor: for i in *.txt)
    • SC2086: Double quote to prevent globbing and word splitting. (Melhor: echo "$i")
    Código corrigido:
    #!/bin/bash
    for i in *.txt; do
        echo "$i"
    done
    
  5. Crie um script que usa trap para exibir a linha do erro quando um comando falhar. Teste com um comando que retorna erro.

    ✓ Resposta:
    #!/bin/bash
    trap 'echo "Erro na linha $LINENO"' ERR
    echo "Tentando acessar diretório inexistente"
    cd /diretorio/inexistente
    echo "Isso não será executado"
    
    Ao executar, a saída será:
    Tentando acessar diretório inexistente
    Erro na linha 5