Nesta aula, vamos desenvolver um projeto prático e completo: um sistema de backup em Shell Script para Linux. Este sistema será capaz de realizar backups de um ou mais diretórios, comprimir os arquivos, manter um histórico com rotação (eliminando backups antigos), gerar logs detalhados e ser agendável via cron. Ao final, você terá um script profissional que pode ser adaptado para uso real em servidores.

O script será construído de forma modular, com configurações centralizadas, funções bem definidas e tratamento de erros. Vamos explorar boas práticas de programação shell, como uso de variáveis, laços, condições, e integração com comandos padrão do Linux (tar, find, mkdir, etc.).

Configuração

O primeiro passo é definir a configuração do backup. Em scripts de produção, é comum separar as configurações do código. Podemos fazer isso usando um arquivo externo (por exemplo, backup.conf) ou definindo variáveis no início do script. Para este projeto, optaremos por um arquivo de configuração externo, que facilita a manutenção sem editar o script em si.

As configurações principais incluem: diretórios de origem (o que será copiado), diretório de destino (onde os backups serão armazenados), prefixo do nome dos arquivos de backup, número de backups a manter (rotação), e opções de compressão. Também podemos incluir configurações de log, como caminho do arquivo de log e nível de verbosidade.

Vamos criar o arquivo backup.conf com as seguintes variáveis:

# backup.conf - Configurações do sistema de backup

# Diretórios de origem (separados por espaço)
ORIGEM="/home/usuario/documentos /etc/nginx"

# Diretório de destino
DESTINO="/mnt/backups"

# Prefixo para os arquivos de backup
PREFIXO="backup"

# Número de backups a manter (rotação)
ROTACAO=7

# Método de compressão: tar.gz, tar.bz2, tar.xz
COMPRESSOR="tar.gz"

# Arquivo de log
LOG_FILE="/var/log/backup.log"

No script principal, vamos carregar essas variáveis usando source (ou .). Isso permite que o script seja reutilizável e fácil de configurar. Também vamos definir variáveis internas, como data e hora atual, e criar a estrutura de diretórios necessária.

#!/bin/bash

# Carrega as configurações
source "$(dirname "$0")/backup.conf"

# Gera timestamp
DATA=$(date +%Y%m%d_%H%M%S)
NOME_BACKUP="${PREFIXO}_${DATA}.${COMPRESSOR}"

# Cria diretório de destino, se não existir
mkdir -p "$DESTINO" || { echo "Erro ao criar diretório de destino"; exit 1; }

Compressão e rotação

A compressão é feita com o comando tar, que pode criar arquivos compactados nos formatos .tar.gz, .tar.bz2, etc. Vamos usar um case para selecionar o parâmetro correto do tar de acordo com o valor de COMPRESSOR.

Para a rotação, precisamos manter apenas os últimos N backups. Podemos listar os arquivos de backup no diretório de destino, ordená-los por data e excluir os mais antigos além do limite. O comando ls com ordenação e head/tail são úteis, mas para segurança, usaremos find com -name e -mtime ou simplesmente listar e contar.

Implementaremos uma função que: (1) executa o tar com as opções adequadas; (2) verifica se o backup foi criado com sucesso; (3) chama a função de rotação.

# Função para realizar o backup
fazer_backup() {
    echo "Iniciando backup em $(date)"
    
    # Seleciona opções do tar conforme o compressor
    case "$COMPRESSOR" in
        tar.gz)  TAR_OPTS="-czf" ;;
        tar.bz2) TAR_OPTS="-cjf" ;;
        tar.xz)  TAR_OPTS="-cJf" ;;
        *)       echo "Compressor inválido: $COMPRESSOR"; exit 1 ;;
    esac
    
    # Executa o tar
    tar $TAR_OPTS "$DESTINO/$NOME_BACKUP" $ORIGEM
    if [ $? -eq 0 ]; then
        echo "Backup criado: $DESTINO/$NOME_BACKUP"
        rotacionar_backups
    else
        echo "Erro ao criar backup"
        exit 1
    fi
}

# Função para rotacionar backups antigos
rotacionar_backups() {
    echo "Verificando rotação (máximo de $ROTACAO backups)"
    # Lista os backups do tipo prefixo_* e ordena por nome (que contém data)
    # O nome começa com prefixo_ e a data é no formato YYYYMMDD_HHMMSS, então ordenação alfabética funciona.
    BACKUPS=$(ls -1 "$DESTINO"/${PREFIXO}_* 2>/dev/null | sort)
    CONTAGEM=$(echo "$BACKUPS" | wc -l)
    
    # Se houver mais backups que o permitido, remove os mais antigos
    if [ "$CONTAGEM" -gt "$ROTACAO" ]; then
        EXCESSO=$((CONTAGEM - ROTACAO))
        for ARQ in $(echo "$BACKUPS" | head -n "$EXCESSO"); do
            rm -f "$ARQ"
            echo "Removendo backup antigo: $ARQ"
        done
    fi
}

Logging

Registrar as ações do script é fundamental para auditoria e depuração. Vamos criar uma função de log que escreve mensagens com timestamp e nível (INFO, ERRO, AVISO) no arquivo definido em LOG_FILE. Também podemos espelhar as mensagens na saída padrão, se desejado.

Usaremos tee para salvar no arquivo e exibir no terminal, ou simplesmente redirecionar. Para robustez, vamos garantir que o diretório do log exista e que o arquivo possa ser gravado.

# Função de log
log() {
    local NIVEL="$1"
    shift
    local MENSAGEM="$@"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$TIMESTAMP] [$NIVEL] $MENSAGEM" | tee -a "$LOG_FILE"
}

# Exemplo de uso
log "INFO" "Iniciando backup"
log "ERRO" "Falha ao criar diretório"

É importante redirecionar erros do script para o log. Podemos usar exec 2>> "$LOG_FILE" para fazer com que todos os erros do shell sejam anexados ao log, mas isso pode poluir o log com mensagens do sistema. Melhor é usar nossa função de log para mensagens controladas e redirecionar apenas a saída de comandos específicos quando necessário.

No script final, substituiremos os echo por chamadas à função log.

Agendamento

Para executar o backup automaticamente, usamos o cron. O cron é um agendador de tarefas do Linux. Vamos criar uma entrada no crontab para executar o script diariamente em um horário específico, por exemplo, às 2h da manhã.

Para editar o crontab do usuário: crontab -e. Adicione a linha:

0 2 * * * /caminho/para/backup.sh

Certifique-se de que o script tenha permissão de execução (chmod +x backup.sh) e que o interpretador bash esteja no shebang. Também é importante que o script seja executado com o ambiente adequado; podemos definir variáveis de ambiente no próprio script ou usar source de um arquivo de perfil.

Vamos criar um script principal que chama as funções e adiciona o agendamento como parte do projeto (opcional). Podemos incluir uma função que instala a entrada no crontab, mas isso deve ser feito com cuidado para não duplicar entradas.

# Função para instalar o cron (opcional)
instalar_cron() {
    local SCRIPT="$(realpath "$0")"
    local LINHA="0 2 * * * $SCRIPT"
    ( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab -
    log "INFO" "Cron instalado: $LINHA"
}

# Chamada principal
main() {
    log "INFO" "Iniciando script de backup"
    fazer_backup
    log "INFO" "Backup concluído"
}

# Executa a função principal
main "$@"

Boas práticas e observações finais

Ao desenvolver scripts de backup, considere as seguintes boas práticas:

  • Use set -euo pipefail para sair em caso de erro, tratar variáveis não definidas e falhas em pipes. Isso torna o script mais previsível.
  • Valide as configurações: verifique se os diretórios de origem existem e são legíveis.
  • Teste o script manualmente antes de agendar.
  • Considere usar rsync para backups incrementais, mas o tar é suficiente para backups completos.
  • Proteja o arquivo de configuração se contiver senhas (não é o caso aqui).
  • Mantenha os logs centralizados e faça rotação de logs também, para não encher o disco.
  • Use timestamps no nome dos arquivos para evitar sobrescrita.
  • Teste a restauração periodicamente para garantir que os backups são válidos.

Agora, vamos montar o script completo. O código final integra todas as partes e pode ser usado como base para um sistema real.

#!/bin/bash

# backup.sh - Sistema de backup completo

set -euo pipefail

# Carrega configurações
SCRIPT_DIR="$(dirname "$(realpath "$0")")"
source "$SCRIPT_DIR/backup.conf"

# Função de log
log() {
    local NIVEL="$1"
    shift
    local MENSAGEM="$@"
    local TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$TIMESTAMP] [$NIVEL] $MENSAGEM" | tee -a "$LOG_FILE"
}

# Função para criar diretório de destino
criar_destino() {
    mkdir -p "$DESTINO" || { log "ERRO" "Falha ao criar diretório de destino"; exit 1; }
}

# Função para realizar o backup
fazer_backup() {
    log "INFO" "Iniciando backup..."
    
    local DATA=$(date +%Y%m%d_%H%M%S)
    local NOME_BACKUP="${PREFIXO}_${DATA}.${COMPRESSOR}"
    local CAMINHO_BACKUP="$DESTINO/$NOME_BACKUP"
    
    case "$COMPRESSOR" in
        tar.gz)  TAR_OPTS="-czf" ;;
        tar.bz2) TAR_OPTS="-cjf" ;;
        tar.xz)  TAR_OPTS="-cJf" ;;
        *)       log "ERRO" "Compressor inválido: $COMPRESSOR"; exit 1 ;;
    esac
    
    if tar $TAR_OPTS "$CAMINHO_BACKUP" $ORIGEM 2>>"$LOG_FILE"; then
        log "INFO" "Backup criado: $CAMINHO_BACKUP"
    else
        log "ERRO" "Falha ao criar backup"
        exit 1
    fi
    
    rotacionar_backups
}

# Função para rotacionar backups
rotacionar_backups() {
    log "INFO" "Verificando rotação (máximo de $ROTACAO backups)"
    local BACKUPS=( "$DESTINO"/${PREFIXO}_* )
    local CONTAGEM=${#BACKUPS[@]}
    
    if [ "$CONTAGEM" -gt "$ROTACAO" ]; then
        # Ordena por nome (timestamp) e remove os mais antigos
        local EXCESSO=$((CONTAGEM - ROTACAO))
        # Usa ls -t para ordenar por tempo de modificação (mais recentes primeiro)
        local ANTIGOS=$(ls -1t "$DESTINO"/${PREFIXO}_* | tail -n "$EXCESSO")
        for ARQ in $ANTIGOS; do
            rm -f "$ARQ"
            log "INFO" "Removendo backup antigo: $ARQ"
        done
    fi
}

# Função para instalar cron (opcional)
instalar_cron() {
    local SCRIPT="$(realpath "$0")"
    local LINHA="0 2 * * * $SCRIPT"
    ( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab -
    log "INFO" "Cron instalado: $LINHA"
}

# Função principal
main() {
    criar_destino
    fazer_backup
    log "INFO" "Backup concluído com sucesso"
}

# Executa a função principal
main "$@"

Referências

Exercícios

  1. Explique a importância de usar set -euo pipefail em um script de backup e dê um exemplo de como cada opção ajuda a evitar problemas.

    ✓ Resposta: set -e faz o script sair imediatamente se um comando retornar um status de erro, evitando que falhas passem despercebidas. set -u trata o uso de variáveis não definidas como erro, prevenindo erros de digitação. set -o pipefail faz com que o status de saída de um pipeline seja o do último comando que falhou, garantindo que erros em qualquer parte do pipeline sejam detectados. Exemplo: se tar falhar em um pipeline, o script sairá em vez de continuar com um backup corrompido.
  2. No script, a rotação usa ls -1t para listar os backups por tempo de modificação. Explique por que isso é preferível a ordenar por nome, e em que situação a ordenação por nome seria mais adequada.

    ✓ Resposta: ls -1t ordena por data de modificação (mais recentes primeiro), o que é mais confiável se os nomes dos arquivos forem alterados manualmente ou se houver backups de fontes diferentes com timestamps semelhantes. A ordenação por nome é adequada quando os nomes seguem um padrão estritamente cronológico (como YYYYMMDD_HHMMSS) e não há alterações externas, mas pode falhar se houver múltiplos backups no mesmo segundo ou se o relógio do sistema for ajustado.
  3. A função instalar_cron usa grep -v "$SCRIPT" para evitar duplicatas. Explique como essa linha funciona e o que aconteceria se não usássemos o grep.

    ✓ Resposta: A linha ( crontab -l 2>/dev/null | grep -v "$SCRIPT"; echo "$LINHA" ) | crontab - primeiro obtém o crontab atual (ignorando erros), filtra as linhas que contêm o caminho do script (para remover entradas antigas), adiciona a nova linha com o agendamento e envia tudo de volta para o crontab. Sem o grep -v, cada execução adicionaria uma nova entrada, resultando em múltiplos backups agendados e causando execuções duplicadas.
  4. Modifique o script para que ele aceite um argumento de linha de comando --dry-run que apenas simula o backup, mostrando o que seria feito sem realmente criar o arquivo. Implemente a lógica usando uma variável DRY_RUN e condicionais.

    ✓ Resposta:
    #!/bin/bash
    # ... configurações ...
    DRY_RUN=false
    if [[ "$1" == "--dry-run" ]]; then
        DRY_RUN=true
    fi
    
    fazer_backup() {
        # ...
        if $DRY_RUN; then
            log "INFO" "DRY RUN: tar $TAR_OPTS $CAMINHO_BACKUP $ORIGEM"
        else
            if tar $TAR_OPTS "$CAMINHO_BACKUP" $ORIGEM; then
                log "INFO" "Backup criado: $CAMINHO_BACKUP"
            else
                log "ERRO" "Falha ao criar backup"
                exit 1
            fi
        fi
        # ... rotação também deve ser simulada
    }
    
  5. Escreva um trecho de código que valide se os diretórios de origem existem e são legíveis antes de iniciar o backup. Use um laço for e a função log para reportar erros.

    ✓ Resposta:
    validar_origens() {
        for DIR in $ORIGEM; do
            if [ ! -d "$DIR" ]; then
                log "ERRO" "Diretório não existe: $DIR"
                exit 1
            elif [ ! -r "$DIR" ]; then
                log "ERRO" "Diretório sem permissão de leitura: $DIR"
                exit 1
            fi
        done
        log "INFO" "Diretórios de origem válidos"
    }
    # Chame validar_origens antes de fazer_backup