Script em Bash: Um Mergulho Profundo em Códigos de Saída e Status

Entenda códigos de saída do Bash, inspecione $? com segurança, defina status com exit e construa um fluxo de controle confiável.

Script em Bash: Um Mergulho Profundo em Códigos de Saída e Status

Os códigos de saída do Bash são como os comandos informam ao seu script o que aconteceu. 0 significa sucesso, e um status diferente de zero indica que o comando falhou ou produziu um resultado que seu script precisa tratar.

Este guia mostra como ler $?, definir status com exit e usar códigos de saída para construir um fluxo de controle mais seguro na automação com Bash.

Entendendo os Códigos de Saída

Todo comando, função ou script executado no Bash retorna um código de saída ao finalizar. Este é um valor inteiro que sinaliza o resultado da execução. Por convenção:

  • 0 (Zero): Indica sucesso. O comando foi concluído sem erros.
  • Diferente de zero (Qualquer outro inteiro): Indica falha ou um erro. Valores diferentes de zero podem, às vezes, significar tipos específicos de erros.

Esta simples convenção de 0 vs. diferente de zero é fundamental para como o Bash opera e como você pode construir lógica condicional em seus scripts.

Recuperando o Último Código de Saída: $?

O Bash fornece um parâmetro especial, $?, que contém o código de saída do comando em primeiro plano executado mais recentemente. Você pode verificar seu valor imediatamente após qualquer comando para determinar seu resultado.

# Exemplo 1: Comando bem-sucedido
ls /tmp
echo "Código de saída para 'ls /tmp': $?"

# Exemplo 2: Comando falhou (diretório inexistente)
ls /diretorio_inexistente
echo "Código de saída para 'ls /diretorio_inexistente': $?"

# Exemplo 3: Grep encontrando uma correspondência (sucesso)
grep "root" /etc/passwd
echo "Código de saída para 'grep root /etc/passwd': $?"

# Exemplo 4: Grep não encontrando correspondência (falha, mas esperada)
grep "usuario_inexistente" /etc/passwd
echo "Código de saída para 'grep usuario_inexistente /etc/passwd': $?"

Saída (pode variar ligeiramente dependendo do seu sistema e do conteúdo de /etc/passwd):

ls /tmp
# ... (lista de arquivos em /tmp)
Código de saída para 'ls /tmp': 0
ls /diretorio_inexistente
ls: cannot access '/diretorio_inexistente': No such file or directory
Código de saída para 'ls /diretorio_inexistente': 2
grep "root" /etc/passwd
root:x:0:0:root:/root:/bin/bash
Código de saída para 'grep root /etc/passwd': 0
grep "usuario_inexistente" /etc/passwd
Código de saída para 'grep usuario_inexistente /etc/passwd': 1

Observe que grep retorna 0 para uma correspondência e 1 para nenhuma correspondência. Ambos são resultados válidos no contexto do grep, mas para lógica condicional, 0 significa o encontro bem-sucedido do padrão.

Definindo Códigos de Saída Explicitamente com exit

Ao escrever seus próprios scripts ou funções, você pode definir explicitamente seu código de saída usando o comando exit seguido por um valor inteiro. Isso é crucial para comunicar o resultado do script para processos chamadores, scripts pai ou pipelines de CI/CD.

#!/bin/bash

# script_sucesso.sh
echo "Este script será encerrado com sucesso (0)"
exit 0
#!/bin/bash

# script_falha.sh
echo "Este script será encerrado com falha (1)"
exit 1
# Teste os scripts
./script_sucesso.sh
echo "Status de script_sucesso.sh: $?"

./script_falha.sh
echo "Status de script_falha.sh: $?"

Saída:

Este script será encerrado com sucesso (0)
Status de script_sucesso.sh: 0
Este script será encerrado com falha (1)
Status de script_falha.sh: 1

Dica: Se exit for chamado sem argumento, o status de saída do script será o status de saída do último comando executado antes de exit ser chamado.

Aproveitando os Códigos de Saída para o Fluxo de Controle

Os códigos de saída são a espinha dorsal da execução condicional no Bash, permitindo criar scripts dinâmicos e responsivos.

Declarações Condicionais (if/else)

A declaração if no Bash avalia o código de saída de um comando. Se o comando sair com 0 (sucesso), o bloco if é executado. Caso contrário, o bloco else (se presente) é executado.

#!/bin/bash

ARQUIVO="/caminho/para/meu/arquivo_importante.txt"

if [ -f "$ARQUIVO" ]; then # O comando de teste `[` sai com 0 se o arquivo existir
    echo "Arquivo '$ARQUIVO' existe. Prosseguindo com o processamento..."
    # Adicione a lógica de processamento do arquivo aqui
    # Exemplo: cat "$ARQUIVO"
    exit 0
else
    echo "Erro: Arquivo '$ARQUIVO' não existe."
    echo "Abortando script."
    exit 1
fi

Operadores Lógicos (&&, ||)

O Bash fornece poderosos operadores lógicos de curto-circuito que dependem dos códigos de saída:

  • comando1 && comando2: comando2 é executado apenas se comando1 sair com 0 (sucesso).
  • comando1 || comando2: comando2 é executado apenas se comando1 sair com um valor diferente de zero (falha).

Estes são extremamente úteis para comandos sequenciais e mecanismos de fallback.

#!/bin/bash

DIR_LOG="/var/log/minha_app"

# Criar diretório apenas se não existir
mkdir -p "$DIR_LOG" && echo "Diretório de log '$DIR_LOG' garantido."

# Tentar iniciar um serviço, se falhar, tentar um comando alternativo
systemctl start meu_servico || { echo "Falha ao iniciar meu_servico. Tentando fallback..."; ./iniciar_fallback.sh; }

# Um comando que deve ter sucesso para o script continuar
copiar_dados_para_local_backup && echo "Backup de dados bem-sucedido." || { echo "Falha no backup de dados!"; exit 1; }

echo "Script concluído com sucesso."
exit 0

set -e: Sair ao Erro

A opção set -e é uma ferramenta poderosa para tornar seus scripts mais robustos. Quando set -e está ativo, o Bash sairá imediatamente do script se qualquer comando sair com um status diferente de zero. Isso evita falhas silenciosas e erros em cascata.

#!/bin/bash
set -e # Sair imediatamente se um comando sair com status diferente de zero

echo "Iniciando script..."

# Este comando será bem-sucedido
ls /tmp

echo "Primeiro comando bem-sucedido."

# Este comando falhará e, por causa do 'set -e', o script sairá aqui
ls /caminho_inexistente

echo "Esta linha nunca será alcançada se o comando anterior falhar."

exit 0 # Esta linha só será alcançada se todos os comandos anteriores forem bem-sucedidos

Saída (se /caminho_inexistente não existir):

Iniciando script...
# ... (saída de ls /tmp)
Primeiro comando bem-sucedido.
ls: cannot access '/caminho_inexistente': No such file or directory

O script termina após o comando ls falhar, e a mensagem "Esta linha nunca será alcançada" não é impressa.

Aviso: set -e tem exceções, e alguns comandos retornam legitimamente diferente de zero para resultados esperados. Por exemplo, grep retorna 1 quando não encontra correspondência. Prefira um if grep -q "padrão" arquivo; then ... fi explícito quando você se importa com o resultado.

Cenários Comuns de Códigos de Saída e Melhores Práticas

Embora 0 para sucesso e diferente de zero para falha seja a regra geral, alguns códigos diferentes de zero têm significados comuns, especialmente para comandos do sistema e built-ins:

  • 0: Sucesso.
  • 1: Erro geral, captura tudo para problemas diversos.
  • 2: Uso incorreto de built-ins do shell ou argumentos de comando incorretos.
  • 126: Comando invocado não pode ser executado (por exemplo, problema de permissão, não é um executável).
  • 127: Comando não encontrado (por exemplo, erro de digitação no nome do comando, não está no PATH).
  • 128 + N: O comando foi terminado pelo sinal N. Por exemplo, 130 (128 + 2) significa que o comando foi terminado por SIGINT (Ctrl+C).

Ao criar seus próprios scripts, mantenha 0 para sucesso. Para falhas, 1 é um padrão seguro para um erro geral. Se seu script lida com múltiplas condições de erro distintas, você pode usar valores mais altos diferentes de zero (por exemplo, 10, 20, 30) para diferenciá-los, mas documente esses códigos personalizados claramente.

Melhores Práticas para Scripts Robutos:

  1. Sempre Verifique Comandos Críticos: Não presuma sucesso. Use declarações if ou && para verificar etapas críticas.
  2. Forneça Mensagens de Erro Informativas: Quando um script falhar, imprima mensagens claras para stderr explicando o que deu errado e como potencialmente corrigir. Use >&2 para redirecionar a saída para o erro padrão.
    meu_comando || { echo "Erro: meu_comando falhou. Verifique os logs." >&2; exit 1; }
    
  3. Limpeza em Caso de Falha: Use trap para garantir que arquivos temporários ou recursos sejam limpos mesmo se o script sair prematuramente.
    limpar() {
        echo "Limpando arquivos temporários..."
        rm -f /tmp/meu_arquivo_temp_$$
    }
    trap limpar EXIT
    
  4. Valide Entradas: Verifique argumentos do script ou variáveis de ambiente no início e saia com um erro informativo se forem inválidos.
  5. Registre o Status de Saída: Para automação complexa, registre o status de saída das operações principais para auditoria e depuração.

Exemplo do Mundo Real: Um Trecho de Script de Backup Robusto

Aqui está como você pode combinar esses conceitos em um cenário prático:

#!/bin/bash
set -e # Sair imediatamente se um comando sair com status diferente de zero

ORIGEM_BACKUP="/data/app/config"
DESTINO_BACKUP="/mnt/backup/configs"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ARQUIVO_LOG="/var/log/backup_config_${TIMESTAMP}.log"

# --- Funções ---
log_message() {
    echo "$(date +%Y-%m-%d_%H:%M:%S) - $1" | tee -a "$ARQUIVO_LOG"
}

limpar() {
    log_message "Limpeza iniciada."
    if [ -n "${DIR_TEMP:-}" ] && [ -d "$DIR_TEMP" ]; then
        rm -rf "$DIR_TEMP"
        log_message "Diretório temporário removido: $DIR_TEMP"
    fi
}

# --- Trap para saída e sinais ---
trap 'limpar' EXIT
trap 'log_message "Script interrompido (SIGINT). Saindo."; exit 130' INT
trap 'log_message "Script terminado (SIGTERM). Saindo."; exit 143' TERM

# --- Lógica Principal do Script ---
log_message "Iniciando backup de configuração."

# 1. Verificar se o diretório de origem existe
if [ ! -d "$ORIGEM_BACKUP" ]; then
    log_message "Erro: Origem do backup '$ORIGEM_BACKUP' não existe." >&2
    exit 2 # Código de erro personalizado para origem inválida
fi

# 2. Garantir que o destino do backup exista
mkdir -p "$DESTINO_BACKUP" || {
    log_message "Erro: Falha ao criar/garantir destino do backup '$DESTINO_BACKUP'." >&2
    exit 3 # Código de erro personalizado para problema no destino
}

# 3. Criar um diretório temporário para compressão
DIR_TEMP=$(mktemp -d)
log_message "Diretório temporário criado: $DIR_TEMP"

# 4. Copiar dados para o diretório temporário
cp -r "$ORIGEM_BACKUP" "$DIR_TEMP/" || {
    log_message "Erro: Falha ao copiar dados de '$ORIGEM_BACKUP' para '$DIR_TEMP'." >&2
    exit 4 # Código de erro personalizado para falha na cópia
}
log_message "Dados copiados para local temporário."

# 5. Comprimir os dados
NOME_ARQUIVO="config_backup_${TIMESTAMP}.tar.gz"
tar -czf "$DIR_TEMP/$NOME_ARQUIVO" -C "$DIR_TEMP" "$(basename "$ORIGEM_BACKUP")" || {
    log_message "Erro: Falha ao comprimir dados." >&2
    exit 5 # Código de erro personalizado para falha na compressão
}
log_message "Dados comprimidos em $NOME_ARQUIVO."

# 6. Mover o arquivo para o destino final
mv "$DIR_TEMP/$NOME_ARQUIVO" "$DESTINO_BACKUP/" || {
    log_message "Erro: Falha ao mover arquivo para '$DESTINO_BACKUP'." >&2
    exit 6 # Código de erro personalizado para falha na movimentação
}
log_message "Arquivo movido para '$DESTINO_BACKUP/$NOME_ARQUIVO'."

log_message "Backup concluído com sucesso!"
exit 0

Conclusão

Trate os códigos de saída como parte da interface do seu script. Verifique comandos críticos, retorne status diferentes de zero claros em caso de falha e documente quaisquer códigos personalizados que outro script ou trabalho de CI possa precisar interpretar.