Projeto: organizador de arquivos
LidoNesta aula, você construirá um script Bash para organizar arquivos automaticamente, classificando-os por tipo, movendo-os com segurança, registrando ações em log e oferecendo um modo de simulação (dry-run). O projeto integra conceitos como loops, condicionais, tratamento de erros e boas práticas, resultando em uma ferramenta útil e robusta.
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
findcom profundidade maior). - Adicione suporte a expressões regulares para classificação mais avançada (ex.: detectar arquivos por conteúdo).
- Use
set -eouset -o pipefailpara 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
- Bash Reference Manual
- Bash Arrays
- GNU Coreutils: mv
- GNU Findutils: find
- Shell Parameter Expansion
- Advanced Bash-Scripting Guide
- ShellCheck - analisador estático
Exercícios
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ávelDRY_RUNe, nas funções de mover, verificamos essa variável para apenas registrar a ação (com prefixo [DRY-RUN]) em vez de executar omv.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çãomover_arquivo.Explique como o script classifica arquivos sem extensão.
✓ Resposta: Na funçãoclassificar_arquivo, se a extensão estiver vazia (não há ponto no nome), retornamos a categoriasem_extensao.Qual comando é usado para percorrer os arquivos do diretório de forma segura com nomes contendo espaços? Por que isso é necessário?
✓ Resposta: Usamosfind "$DIR_ALVO" -maxdepth 1 -type f -print0e depoiswhile IFS= read -r -d '' arquivo. Isso lida corretamente com nomes que contêm espaços ou caracteres especiais, pois o-print0separa os nomes com caractere nulo, e oread -d ''lê até o nulo.Como você adicionaria uma nova categoria, por exemplo, para arquivos de código-fonte (.c, .py, .js)?
✓ Resposta: Basta adicionar entradas no array associativocategorias, por exemplo:[c]="codigo",[py]="codigo",[js]="codigo". O script criará automaticamente a pastacodigoe moverá os arquivos correspondentes.