O processamento em lote é uma das principais aplicações de shell scripts no dia a dia de um administrador de sistemas ou desenvolvedor. Em vez de executar comandos manualmente para cada arquivo ou dado, criamos scripts que processam grandes volumes de forma automatizada, repetitiva e consistente. Nesta aula, vamos explorar desde a iteração básica sobre arquivos até técnicas avançadas de paralelismo e geração de relatórios.

Dominar essas técnicas permite que você escreva scripts mais eficientes, capazes de lidar com milhares de arquivos, transformar dados em lote e extrair informações úteis de forma rápida. Vamos começar com os fundamentos e evoluir para padrões mais sofisticados, sempre com exemplos práticos e aplicáveis.

Iterando arquivos

A forma mais comum de processar múltiplos arquivos em shell é usando loops for ou o comando find combinado com while read. A escolha depende do cenário: quando você conhece os nomes dos arquivos ou pode usar glob patterns, o for é simples; quando precisa percorrer árvores de diretórios ou filtrar por atributos, find é mais poderoso.

Um erro comum é iterar sobre a saída de ls, o que quebra com nomes de arquivos contendo espaços ou caracteres especiais. A abordagem robusta é usar globbing ou find -print0 com read -d ''. Vamos ver exemplos práticos.

#!/bin/bash
# Iteração simples com glob
for file in *.txt; do
    echo "Processando $file"
    # ... comandos ...
done

# Iteração segura com find e while read
find . -name "*.log" -print0 | while IFS= read -r -d '' file; do
    echo "Arquivo: $file"
    # ... comandos ...
done

# Usando array para armazenar e depois processar
files=(*.txt)
for file in "${files[@]}"; do
    echo "Tamanho de $file: $(stat -c%s "$file") bytes"
done

No primeiro exemplo, o glob é expandido pelo shell para uma lista de arquivos .txt no diretório atual. Se não houver nenhum, o loop executa com a string literal *.txt — cuidado com isso. Para evitar, use nullglob ou verifique a existência. O segundo exemplo é mais seguro para nomes com espaços, pois o delimitador nulo separa corretamente os nomes. O terceiro mostra como armazenar os arquivos em um array, permitindo manipulações como fatiamento ou contagem.

Quando você precisa de mais controle sobre a iteração, como percorrer recursivamente ou filtrar por data de modificação, o find é indispensável. Ele aceita opções como -type f para arquivos regulares, -size +1M para arquivos maiores que 1MB, e -exec para executar comandos diretamente. No entanto, para processamentos complexos, o padrão while read é mais legível.

Transformações

Transformações em lote envolvem modificar o conteúdo, nome ou formato de arquivos. Ferramentas como sed, awk, tr e rename são poderosas para isso. Em um script, você pode aplicar essas transformações a muitos arquivos de uma vez, mantendo um registro do que foi feito.

Por exemplo, você pode querer substituir todas as ocorrências de uma string em vários arquivos de configuração, converter quebras de linha de CRLF para LF, ou renomear arquivos seguindo um padrão. Vamos ver alguns exemplos.

#!/bin/bash
# Substituir "localhost" por "127.0.0.1" em todos os .conf
for file in *.conf; do
    sed -i 's/localhost/127.0.0.1/g' "$file"
done

# Converter CRLF para LF em todos os .txt
for file in *.txt; do
    tr -d '\r' < "$file" > "${file}.tmp"
    mv "${file}.tmp" "$file"
done

# Renomear arquivos com prefixo "backup_"
for file in *.log; do
    mv "$file" "backup_${file}"
done

O sed -i edita o arquivo in-place, mas é preciso ter cuidado com backups. Você pode usar sed -i.bak para criar um backup automático. O tr é ótimo para remover caracteres, e a redireção para um arquivo temporário é uma prática comum para evitar corromper o original. No renomeio, usamos expansão de parâmetros para adicionar o prefixo.

Para transformações mais complexas, como extrair dados de arquivos CSV e gerar novos formatos, o awk é a ferramenta ideal. Ele processa linha por linha e permite manipular campos. Por exemplo, podemos gerar um resumo de tamanhos de arquivos.

#!/bin/bash
# Gerar um arquivo com tamanho e nome de cada .jpg
for file in *.jpg; do
    size=$(stat -c%s "$file")
    echo "$size $file" >> tamanhos.txt
done

# Usar awk para calcular a soma dos tamanhos
awk '{sum += $1} END {print "Tamanho total: " sum " bytes"}' tamanhos.txt

Paralelismo com xargs

Processar arquivos em sequência pode ser lento. O comando xargs permite executar comandos em paralelo, aproveitando múltiplos núcleos da CPU. Combinado com find ou ls, é uma forma eficiente de processar grandes volumes.

O xargs lê itens da entrada padrão e executa um comando, dividindo os argumentos em lotes. A opção -P define o número de processos simultâneos. É importante usar -0 quando a entrada vem de find -print0 para lidar com espaços.

#!/bin/bash
# Comprimir todos os .txt em paralelo (4 processos)
find . -name "*.txt" -print0 | xargs -0 -P4 -I{} gzip {}

# Executar um script em cada arquivo, com no máximo 8 processos
find . -name "*.log" -print0 | xargs -0 -P8 -I{} ./processa_log.sh {}

# Exemplo com contagem de linhas
find . -name "*.sh" -print0 | xargs -0 -P4 wc -l

No primeiro exemplo, cada arquivo é compactado individualmente. A opção -I{} substitui {} pelo nome do arquivo. No segundo, chamamos um script personalizado. No terceiro, wc -l é executado em paralelo, e a saída é agregada no final.

É crucial entender os limites: paralelismo não é útil para operações de I/O pesado se o disco for o gargalo, e pode causar sobrecarga se o comando for muito leve. Teste com -P baixo e aumente conforme necessário. Além disso, o xargs pode ser usado com sh -c para comandos mais complexos, mas cuidado com quoting.

Relatórios

Após processar dados em lote, é comum gerar relatórios resumindo os resultados. Shell scripts podem criar relatórios em texto, HTML ou CSV, usando redirecionamento e ferramentas como awk, sort, uniq e paste. Relatórios bem formatados são essenciais para auditoria e monitoramento.

Um relatório típico pode listar arquivos processados, contagens, erros, ou totais. Vamos criar um relatório simples de tamanhos de arquivos em formato CSV e depois um resumo em texto.

#!/bin/bash
# Gerar relatório CSV de todos os arquivos .txt
{
    echo "arquivo,tamanho,linhas"
    for file in *.txt; do
        size=$(stat -c%s "$file")
        lines=$(wc -l < "$file")
        echo "$file,$size,$lines"
    done
} > relatorio.csv

# Gerar relatório em texto com totais
{
    echo "Relatório de arquivos .txt"
    echo "========================="
    total=0
    for file in *.txt; do
        size=$(stat -c%s "$file")
        total=$((total + size))
        echo "$file: $size bytes"
    done
    echo "-------------------------"
    echo "TOTAL: $total bytes"
} > relatorio.txt

# Exemplo com sort e uniq para contar extensões
find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn > extensoes.txt

O primeiro exemplo usa um bloco { } para agrupar comandos e redirecionar toda a saída para um arquivo. O segundo gera um relatório mais legível, calculando o total. O terceiro mostra como usar sort e uniq para contar extensões de arquivos — útil para análise rápida.

Para relatórios mais elaborados, você pode usar printf para formatar colunas, ou gerar HTML com tabelas. O importante é que o script seja reutilizável e fácil de manter. Considere também a inclusão de timestamps e logs de erros.

Boas práticas e observações finais

Ao escrever scripts de processamento em lote, sempre use aspas duplas ao redor das variáveis para preservar espaços em nomes de arquivos. Prefira find -print0 com read -d '' ou xargs -0 para robustez. Teste com um subconjunto pequeno antes de processar tudo.

Para paralelismo, monitore o uso de CPU e memória. Use nice ou ionice para reduzir prioridade se necessário. E nunca se esqueça de verificar se os comandos estão disponíveis no sistema (ex.: command -v).

Referências

Exercícios

  1. Escreva um script que itere sobre todos os arquivos .log no diretório atual e, para cada um, exiba o nome e o número de linhas. Use um loop for simples.

    ✓ Resposta:
    #!/bin/bash
    for file in *.log; do
        lines=$(wc -l < "$file")
        echo "$file: $lines linhas"
    done
    
  2. Crie um script que use find para localizar todos os arquivos .txt no diretório atual e subdiretórios, e substitua a string "foo" por "bar" em cada um, usando sed -i. Lide com nomes contendo espaços.

    ✓ Resposta:
    #!/bin/bash
    find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
        sed -i 's/foo/bar/g' "$file"
    done
    
  3. Utilize xargs para converter todos os arquivos .png do diretório atual para .jpg, usando o comando convert (ImageMagick). Execute com 4 processos em paralelo.

    ✓ Resposta:
    #!/bin/bash
    ls *.png | xargs -P4 -I{} convert {} {}.jpg
    # Para lidar com espaços, use:
    # find . -maxdepth 1 -name "*.png" -print0 | xargs -0 -P4 -I{} convert {} {}.jpg
    
  4. Escreva um script que gere um relatório em CSV com o nome e o tamanho de todos os arquivos .jpg no diretório atual, ordenados por tamanho (maior primeiro). Use sort e awk.

    ✓ Resposta:
    #!/bin/bash
    {
        echo "nome,tamanho"
        for file in *.jpg; do
            size=$(stat -c%s "$file")
            echo "$file,$size"
        done
    } | sort -t, -k2 -nr > relatorio.csv
    # Ou usando awk para ordenar depois:
    # (echo "nome,tamanho"; for file in *.jpg; do stat -c "%n,%s" "$file"; done) | sort -t, -k2 -nr > relatorio.csv
    
  5. Modifique o script de relatório do exercício 4 para incluir também a contagem de arquivos e o tamanho total no final do relatório, em uma seção separada.

    ✓ Resposta:
    #!/bin/bash
    output="relatorio.csv"
    {
        echo "nome,tamanho"
        total=0
        count=0
        for file in *.jpg; do
            size=$(stat -c%s "$file")
            echo "$file,$size"
            total=$((total + size))
            count=$((count + 1))
        done
        echo "---"
        echo "Total de arquivos: $count"
        echo "Tamanho total: $total bytes"
    } > "$output"