Loop sobre arquivos com glob e find

1. Introdução ao globbing e ao comando find

Globbing é o mecanismo de expansão de padrões de nomes de arquivos que o shell realiza automaticamente. Os padrões mais comuns incluem * (qualquer sequência de caracteres), ? (um caractere qualquer) e [] (conjunto de caracteres). Quando você escreve *.txt no terminal, o Bash expande isso para uma lista de todos os arquivos terminados em .txt no diretório atual.

O comando find, por outro lado, é uma ferramenta poderosa que percorre a árvore de diretórios aplicando filtros complexos. Enquanto o globbing é simples e direto para casos básicos, o find oferece flexibilidade para buscas recursivas, por tipo de arquivo, tamanho, data de modificação e muito mais.

A principal diferença prática: o globbing expande padrões no próprio shell antes da execução do comando, enquanto o find gera uma lista de arquivos dinamicamente durante a execução. Isso tem implicações importantes para tratamento de nomes com espaços e caracteres especiais.

2. Loop com glob básico

A forma mais simples de iterar sobre arquivos com glob é usando o loop for:

for file in *.txt; do
    echo "Processando: $file"
done

Um problema comum: se não houver arquivos .txt, o glob não expande e o loop tenta processar o literal *.txt. Para evitar isso, ative a opção nullglob:

shopt -s nullglob
for file in *.txt; do
    echo "Processando: $file"
done
shopt -u nullglob

Com Bash 4 ou superior, você pode usar glob recursivo com ** para percorrer subdiretórios:

shopt -s globstar
for file in **/*.txt; do
    echo "Arquivo: $file"
done

3. Loop com glob avançado

Você pode combinar múltiplos padrões usando chaves:

for file in *.{jpg,png,gif}; do
    echo "Imagem: $file"
done

Para padrões mais complexos, ative o extglob:

shopt -s extglob
# Arquivos que começam com "backup" ou terminam com ".tmp"
for file in +(backup*) +(*.tmp); do
    echo "$file"
done

Padrões com extglob:
- ?(pattern) — zero ou uma ocorrência
- *(pattern) — zero ou mais ocorrências
- +(pattern) — uma ou mais ocorrências
- @(pattern) — exatamente uma ocorrência
- !(pattern) — qualquer coisa exceto o padrão

Para filtrar apenas diretórios com glob:

for dir in */; do
    echo "Diretório: $dir"
done

E para arquivos ocultos (que começam com ponto):

for file in .*; do
    echo "Oculto: $file"
done

4. Loop com find e while read

A abordagem mais segura para processar arquivos com find usa -print0 e while read:

find . -type f -name "*.log" -print0 | while IFS= read -r -d '' file; do
    echo "Processando: $file"
    wc -l "$file"
done

Por que evitar for file in $(find ...)? Considere um arquivo chamado meu arquivo.txt — o comando find retornaria ./meu arquivo.txt, e o loop for quebraria isso em duas iterações: ./meu e arquivo.txt. O mesmo ocorre com quebras de linha nos nomes.

O -print0 usa o caractere nulo como delimitador, que é o único caractere que não pode aparecer em nomes de arquivos no Linux. O read -d '' lê até encontrar esse delimitador, preservando nomes completos.

Exemplo com filtros do find:

find /var/log -type f -name "*.log" -size +1M -print0 | while IFS= read -r -d '' file; do
    echo "Arquivo grande: $file ($(du -h "$file" | cut -f1))"
done

5. Usando find com -exec e xargs

O find pode executar comandos diretamente com -exec:

# Executa uma vez para cada arquivo (lento, mas seguro)
find . -name "*.tmp" -exec rm {} \;

# Executa uma vez com todos os arquivos como argumentos (mais rápido)
find . -name "*.tmp" -exec rm {} +

A diferença: \; executa o comando para cada arquivo individualmente, enquanto + agrupa vários arquivos em uma única chamada de comando, similar ao xargs.

Combinando find e xargs de forma segura:

find . -type f -name "*.bak" -print0 | xargs -0 rm -f

A opção -0 do xargs corresponde ao -print0 do find, garantindo que nomes com espaços sejam tratados corretamente.

Performance: para operações simples como remoção ou cópia, -exec {} + e xargs -0 têm desempenho similar. Para comandos que precisam de processamento individual, while read ou -exec {} \; são mais apropriados.

6. Lidando com nomes de arquivos problemáticos

Nomes de arquivos podem conter espaços, quebras de linha, tabs e outros caracteres especiais. Veja um exemplo prático de como lidar com isso:

# Criando arquivos com nomes problemáticos para teste
touch "arquivo com espacos.txt"
touch "arquivo
com
quebras.txt"
touch "arquivo'com'aspas.txt"

# Processamento seguro com find e while read
find . -type f -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    # Usar aspas duplas em torno de $file é essencial
    cp "$file" "/backup/$(basename "$file")"
    echo "Copiado: $file"
done

Regras de ouro:
1. Sempre use aspas duplas ao expandir variáveis com nomes de arquivos
2. Use -print0 com find e -0 com xargs
3. Defina IFS= para evitar que o read divida por espaços ou tabs
4. Use -d '' no read para usar o delimitador nulo

7. Exemplos práticos e boas práticas

Renomear arquivos em lote com glob:

# Adicionar prefixo "backup_" a todos os arquivos .conf
shopt -s nullglob
for file in *.conf; do
    mv "$file" "backup_$file"
done

Buscar e processar arquivos recursivamente com find:

# Encontrar todos os arquivos .py com mais de 100 linhas e contar linhas
find . -type f -name "*.py" -size +100c -print0 | while IFS= read -r -d '' file; do
    lines=$(wc -l < "$file")
    echo "$file: $lines linhas"
done

Dicas de performance:

  • Para diretórios com poucos arquivos (centenas), o globbing é mais rápido
  • Para diretórios com milhares de arquivos, o find é mais eficiente
  • Evite for file in $(find ...) a todo custo — além de inseguro, é lento
  • Use find -exec {} + ou xargs -0 para operações em lote
  • Para scripts portáteis, prefira find em vez de globstar (que é específico do Bash)

Exemplo completo: backup seletivo

#!/bin/bash

shopt -s nullglob
backup_dir="/tmp/backup_$(date +%Y%m%d)"
mkdir -p "$backup_dir"

# Usando glob para arquivos locais
for file in *.doc *.pdf; do
    cp "$file" "$backup_dir/"
done

# Usando find para busca recursiva de logs recentes
find /var/log -type f -name "*.log" -mtime -7 -print0 | \
    while IFS= read -r -d '' file; do
        cp "$file" "$backup_dir/"
    done

echo "Backup concluído em $backup_dir"

A escolha entre glob e find depende do contexto: para operações simples em diretórios rasos, o glob é suficiente. Para buscas complexas ou recursivas com segurança contra nomes problemáticos, o find com -print0 é a abordagem correta.

Referências