Bem-vindo à aula 94 do nosso curso de Shell Script! Hoje vamos colocar em prática muitos dos conceitos que você aprendeu ao longo das aulas anteriores, construindo um organizador de arquivos completo. Este projeto vai além de um simples script: ele incorpora classificação por tipo, movimentação segura, registro de atividades (logging) e um modo de simulação (dry-run) que permite testar o comportamento sem alterar o sistema.

Ao final desta aula, você terá um script reutilizável que pode ser adaptado para organizar pastas de downloads, documentos ou qualquer diretório bagunçado. Além disso, entenderá como estruturar um script profissional, com funções, tratamento de erros e opções de linha de comando. Vamos começar!

Classificação por tipo

O primeiro passo para organizar arquivos é identificar o tipo de cada um. Em sistemas Linux, a extensão do arquivo é uma forma comum de classificação, embora não seja a única. Para este projeto, vamos utilizar a extensão para determinar a categoria (por exemplo, imagens, documentos, vídeos, etc.). Isso é simples e eficaz para a maioria dos casos.

Vamos criar um script que define um mapeamento de extensões para categorias. Usaremos um array associativo no Bash (disponível na versão 4.0 ou superior) para facilitar a consulta. Se a extensão não estiver no mapa, classificaremos como 'outros'. O script também deve tratar arquivos sem extensão, que serão classificados como 'sem_extensao'.

Aqui está um trecho de código que implementa a classificação:

#!/bin/bash

# Mapeamento de extensões para categorias
declare -A categorias=(
    [jpg]="imagens"
    [jpeg]="imagens"
    [png]="imagens"
    [gif]="imagens"
    [svg]="imagens"
    [pdf]="documentos"
    [doc]="documentos"
    [docx]="documentos"
    [txt]="documentos"
    [odt]="documentos"
    [mp3]="audio"
    [wav]="audio"
    [flac]="audio"
    [mp4]="videos"
    [avi]="videos"
    [mkv]="videos"
    [mov]="videos"
    [zip]="compactados"
    [tar]="compactados"
    [gz]="compactados"
    [rar]="compactados"
)

# Função para obter a extensão do arquivo
obter_extensao() {
    local arquivo="$1"
    local nome="$(basename "$arquivo")"
    if [[ "$nome" == *.* ]]; then
        echo "${nome##*.}"
    else
        echo ""
    fi
}

# Função para classificar o arquivo em uma categoria
classificar_arquivo() {
    local arquivo="$1"
    local ext="$(obter_extensao "$arquivo")"
    if [[ -z "$ext" ]]; then
        echo "sem_extensao"
    else
        ext="${ext,,}"  # converte para minúsculas
        if [[ -n "${categorias[$ext]}" ]]; then
            echo "${categorias[$ext]}"
        else
            echo "outros"
        fi
    fi
}

Essa abordagem permite que o script seja facilmente estendido: basta adicionar novas entradas no array categorias. Também é importante notar que usamos ${ext,,} para converter a extensão para minúsculas, garantindo que arquivos como .JPG sejam tratados da mesma forma que .jpg.

Movimentação segura

Mover arquivos pode ser arriscado: você pode sobrescrever um arquivo existente ou mover para um destino errado. Para garantir a segurança, devemos verificar se o destino existe, se já existe um arquivo com o mesmo nome e, se necessário, gerar um nome único para evitar colisões.

No nosso script, vamos implementar uma função mover_arquivo que recebe o arquivo de origem e o diretório de destino. Ela criará o diretório se não existir (usando mkdir -p) e, se houver conflito de nomes, adicionará um sufixo numérico (como arquivo_1.txt) até encontrar um nome livre. Também usaremos mv -n (no-clobber) como uma camada extra de proteção, mas como já tratamos os nomes, isso é redundante.

Vamos ver o código:

# Função para mover arquivo com segurança
mover_arquivo() {
    local origem="$1"
    local destino_dir="$2"
    local nome_arquivo="$(basename "$origem")"
    local destino="$destino_dir/$nome_arquivo"

    # Cria o diretório de destino, se necessário
    mkdir -p "$destino_dir" || { echo "Erro: não foi possível criar $destino_dir" >&2; return 1; }

    # Se o arquivo já existe no destino, gera um nome único
    if [[ -e "$destino" ]]; then
        local base="${nome_arquivo%.*}"
        local ext="${nome_arquivo##*.}"
        if [[ "$base" == "$nome_arquivo" ]]; then
            # Sem extensão
            base="$nome_arquivo"
            ext=""
        fi
        local contador=1
        while [[ -e "$destino_dir/${base}_${contador}.${ext}" ]]; do
            ((contador++))
        done
        destino="$destino_dir/${base}_${contador}.${ext}"
    fi

    # Move o arquivo
    mv "$origem" "$destino" || { echo "Erro ao mover $origem" >&2; return 1; }
    echo "$destino"  # retorna o caminho final (útil para log)
}

Essa função é segura porque verifica a existência do arquivo antes de mover e cria um nome alternativo se necessário. Note que usamos ${nome_arquivo%.*} para extrair a base e ${nome_arquivo##*.} para a extensão, mas precisamos tratar o caso de arquivos sem extensão. O loop while garante que o novo nome não colida com nenhum existente.

Logging

Um bom script deve registrar o que está fazendo, seja para auditoria ou para depuração. Vamos implementar um sistema de logging simples que grava mensagens em um arquivo de log e também pode exibir na tela (se o modo verbose estiver ativo).

Criaremos uma função log que aceita uma mensagem e um nível (INFO, WARN, ERRO). O log será gravado em um arquivo definido pelo usuário (via opção -l) ou em um padrão. Usaremos timestamps para cada entrada.

# Configurações de log
LOG_FILE="organizador.log"
VERBOSE=false

# Função de log
log() {
    local nivel="$1"
    local mensagem="$2"
    local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
    local linha="[$timestamp] [$nivel] $mensagem"
    echo "$linha" >> "$LOG_FILE"
    if [[ "$VERBOSE" == true ]]; then
        echo "$linha"
    fi
}

No corpo principal, usaremos log para registrar cada ação, como "Movendo arquivo X para Y" ou "Arquivo ignorado". Isso ajuda o usuário a entender o que o script fez, mesmo depois de executado.

Dry-run

O modo dry-run (simulação) é uma prática essencial em scripts que modificam o sistema. Ele permite que o usuário veja o que o script faria sem realmente executar as mudanças. Isso é útil para testar o script em um diretório real sem medo de danos.

Implementaremos uma variável DRY_RUN que, quando ativada (via opção -d ou --dry-run), faz o script apenas exibir as ações que seriam tomadas, sem mover arquivos. A função mover_arquivo deve ser modificada para verificar essa variável e, se estiver ativa, apenas registrar a ação e retornar o destino simulado.

Vamos adaptar a função:

# Função para mover arquivo com segurança (com dry-run)
mover_arquivo() {
    local origem="$1"
    local destino_dir="$2"
    local nome_arquivo="$(basename "$origem")"
    local destino="$destino_dir/$nome_arquivo"

    # Cria o diretório de destino, se necessário (mesmo em dry-run, apenas simula)
    if [[ "$DRY_RUN" == true ]]; then
        log "INFO" "[DRY-RUN] Criaria diretório: $destino_dir"
    else
        mkdir -p "$destino_dir" || { log "ERRO" "Não foi possível criar $destino_dir"; return 1; }
    fi

    # Se o arquivo já existe no destino, gera um nome único
    if [[ -e "$destino" ]]; then
        local base="${nome_arquivo%.*}"
        local ext="${nome_arquivo##*.}"
        if [[ "$base" == "$nome_arquivo" ]]; then
            base="$nome_arquivo"
            ext=""
        fi
        local contador=1
        while [[ -e "$destino_dir/${base}_${contador}.${ext}" ]]; do
            ((contador++))
        done
        destino="$destino_dir/${base}_${contador}.${ext}"
    fi

    if [[ "$DRY_RUN" == true ]]; then
        log "INFO" "[DRY-RUN] Moveria '$origem' para '$destino'"
    else
        mv "$origem" "$destino" || { log "ERRO" "Falha ao mover $origem"; return 1; }
        log "INFO" "Movido '$origem' para '$destino'"
    fi
    echo "$destino"
}

Dessa forma, no modo simulação, o script não altera o sistema, apenas informa o que faria. Isso é extremamente útil para validar a lógica antes de executar de verdade.

Estrutura completa do script

Agora vamos juntar todas as partes em um script completo. Ele terá opções de linha de comando para especificar o diretório a organizar, o diretório de destino (opcional), ativar o dry-run, o modo verbose e o arquivo de log. Também incluiremos um tratamento básico de erros e uma mensagem de uso.

#!/bin/bash

# Script: organizador.sh
# Descrição: Organiza arquivos em um diretório, classificando por tipo e movendo para subpastas.
# Uso: ./organizador.sh [opções] 

# Configurações padrão
DIR_ALVO=""
DIR_DESTINO=""  # Se vazio, usa o próprio diretório alvo
DRY_RUN=false
VERBOSE=false
LOG_FILE="organizador.log"

# Função de ajuda
mostrar_ajuda() {
    cat <<EOF
Uso: $0 [opções] <diretório>

Opções:
  -d, --dry-run       Simula a organização sem mover arquivos
  -v, --verbose       Exibe mensagens detalhadas no terminal
  -l, --log ARQUIVO   Define o arquivo de log (padrão: organizador.log)
  -h, --help          Mostra esta ajuda

Exemplo:
  $0 -d ~/Downloads
EOF
}

# Processamento de argumentos
while [[ $# -gt 0 ]]; do
    case "$1" in
        -d|--dry-run)
            DRY_RUN=true
            shift
            ;;
        -v|--verbose)
            VERBOSE=true
            shift
            ;;
        -l|--log)
            LOG_FILE="$2"
            shift 2
            ;;
        -h|--help)
            mostrar_ajuda
            exit 0
            ;;
        -*)
            echo "Opção inválida: $1" >&2
            mostrar_ajuda
            exit 1
            ;;
        *)
            DIR_ALVO="$1"
            shift
            ;;
    esac
done

# Verifica se o diretório alvo foi informado
if [[ -z "$DIR_ALVO" ]]; then
    echo "Erro: informe o diretório a ser organizado." >&2
    mostrar_ajuda
    exit 1
fi

# Se o diretório não existe, sai
if [[ ! -d "$DIR_ALVO" ]]; then
    echo "Erro: diretório $DIR_ALVO não encontrado." >&2
    exit 1
fi

# Se não foi informado diretório de destino, usa o próprio alvo
if [[ -z "$DIR_DESTINO" ]]; then
    DIR_DESTINO="$DIR_ALVO"
fi

# Cria o arquivo de log (ou limpa se já existir? Vamos apenas adicionar)
: > "$LOG_FILE"  # limpa o log a cada execução

# Funções (como definidas anteriormente) ...

# Mapeamento de categorias, funções, etc.

# Função para processar um arquivo
processar_arquivo() {
    local arquivo="$1"
    local categoria="$(classificar_arquivo "$arquivo")"
    local destino_dir="$DIR_DESTINO/$categoria"
    log "INFO" "Processando $arquivo - categoria: $categoria"
    mover_arquivo "$arquivo" "$destino_dir"
}

# Percorre todos os arquivos do diretório (apenas arquivos, não subpastas)
log "INFO" "Iniciando organização do diretório $DIR_ALVO"
while IFS= read -r -d '' arquivo; do
    processar_arquivo "$arquivo"
done < <(find "$DIR_ALVO" -maxdepth 1 -type f -print0)

log "INFO" "Organização concluída."
echo "Organização concluída. Verifique o log em $LOG_FILE"

Note que usamos find -print0 e read -d '' para lidar com nomes de arquivos que contenham espaços ou caracteres especiais. Isso é uma boa prática em scripts Bash.

Boas práticas e observações finais

Este projeto demonstra várias boas práticas de programação em Shell Script: modularização com funções, uso de arrays associativos, tratamento de erros, opções de linha de comando e logging. Algumas dicas para aprimorar ainda mais:

  • Teste sempre com dry-run antes de executar em produção.
  • Mantenha o log para auditoria e depuração.
  • Considere lidar com subdiretórios recursivamente, se necessário (use find com profundidade maior).
  • Adicione suporte a expressões regulares para classificação mais avançada (ex.: detectar arquivos por conteúdo).
  • Use set -e ou set -o pipefail para falhar rapidamente em caso de erro, mas com cuidado para não interromper o loop.

Lembre-se de que este script é um ponto de partida. Você pode adaptá-lo para suas necessidades específicas, como organizar por data de modificação ou por tamanho. O importante é entender os conceitos e saber aplicá-los.

Referências

Exercícios

  1. Qual é a finalidade do modo dry-run em um script de organização? Como ele é implementado no nosso script?

    ✓ Resposta: O modo dry-run permite simular as ações sem modificar o sistema, exibindo o que seria feito. No script, usamos a variável DRY_RUN e, nas funções de mover, verificamos essa variável para apenas registrar a ação (com prefixo [DRY-RUN]) em vez de executar o mv.
  2. Como o script evita sobrescrever arquivos existentes no diretório de destino?

    ✓ Resposta: O script verifica se o destino já existe. Se sim, gera um novo nome adicionando um contador (ex.: arquivo_1.txt) até encontrar um nome livre. Isso é feito na função mover_arquivo.
  3. Explique como o script classifica arquivos sem extensão.

    ✓ Resposta: Na função classificar_arquivo, se a extensão estiver vazia (não há ponto no nome), retornamos a categoria sem_extensao.
  4. Qual comando é usado para percorrer os arquivos do diretório de forma segura com nomes contendo espaços? Por que isso é necessário?

    ✓ Resposta: Usamos find "$DIR_ALVO" -maxdepth 1 -type f -print0 e depois while IFS= read -r -d '' arquivo. Isso lida corretamente com nomes que contêm espaços ou caracteres especiais, pois o -print0 separa os nomes com caractere nulo, e o read -d '' lê até o nulo.
  5. Como você adicionaria uma nova categoria, por exemplo, para arquivos de código-fonte (.c, .py, .js)?

    ✓ Resposta: Basta adicionar entradas no array associativo categorias, por exemplo: [c]="codigo", [py]="codigo", [js]="codigo". O script criará automaticamente a pasta codigo e moverá os arquivos correspondentes.