Iterar sobre arquivos é uma tarefa comum em scripts shell, mas pode se tornar perigosa quando nomes de arquivos contêm espaços, quebras de linha ou outros caracteres especiais. Nesta aula, você aprenderá técnicas robustas para percorrer arquivos de forma segura, evitando armadilhas comuns que podem quebrar seu script ou causar comportamentos inesperados. Vamos comparar abordagens como globbing, o comando ls e o uso de find com -print0, além de consolidar boas práticas para produção.

Globbing vs ls

O globbing (expansão de caminhos) é uma das formas mais simples e seguras de iterar sobre arquivos no Bash. Quando você usa for file in *.txt, o próprio shell expande o padrão para uma lista de nomes de arquivos, sem a necessidade de um comando externo. Isso é eficiente e evita muitos problemas, pois a expansão é feita internamente pelo shell. No entanto, se não houver arquivos correspondentes, o padrão permanece literal (a menos que a opção nullglob ou failglob esteja ativada).

Já o comando ls é frequentemente usado em scripts, mas é frágil: sua saída é formatada para humanos, não para máquinas. Quando você faz for file in $(ls *.txt), a saída de ls é dividida por espaços e quebras de linha, quebrando nomes que contenham esses caracteres. Além disso, ls pode adicionar caracteres de escape ou cores dependendo das configurações. Portanto, evite ls em loops, a menos que você use opções como --quoting-style=shell (que ainda tem limitações).

# Exemplo seguro com globbing
for file in *.txt; do
    echo "Processando: $file"
done

# Exemplo perigoso com ls (NÃO FAÇA ISSO)
for file in $(ls *.txt); do
    echo "Processando: $file"  # Quebra se nome tiver espaço
done

Espaços em nomes

Nomes de arquivos com espaços são comuns em sistemas modernos. Se você não tomar cuidado, seu script pode tratar cada parte do nome como um arquivo separado. A abordagem mais segura é usar globbing com aspas duplas ao redor da variável: for file in *.txt; do echo "$file"; done. As aspas garantem que o nome inteiro seja tratado como uma única string. Além disso, ao usar o comando find, você pode usar a opção -print0 que separa os nomes com caractere nulo (\0), e então usar while IFS= read -r -d '' para ler cada nome corretamente.

# Exemplo com globbing (seguro com aspas)
for file in *.txt; do
    echo "Nome do arquivo: '$file'"
done

# Exemplo com find -print0 e while
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    echo "Nome do arquivo: '$file'"
done

find -print0 com while

O comando find é extremamente poderoso para localizar arquivos com base em critérios complexos. Quando combinado com -print0, ele imprime cada nome de arquivo seguido de um caractere nulo (\0), que é o único caractere que não pode aparecer em nomes de arquivos no Linux. Para ler essa saída de forma segura, use um loop while com read -d '' (ou read -r -d '' para evitar interpretação de escapes). A variável IFS= garante que espaços e tabs não sejam usados como separadores.

# Exemplo completo: encontrar todos os arquivos .log modificados nos últimos 7 dias
find /var/log -name "*.log" -mtime -7 -print0 | while IFS= read -r -d '' file; do
    echo "Arquivo recente: $file"
    # Faça algo com o arquivo, como compactá-lo
done

Essa técnica é robusta mesmo para nomes com espaços, quebras de linha ou caracteres especiais. Lembre-se de que o loop while é executado em um subshell se você usar pipe (|), o que significa que variáveis definidas dentro do loop não estarão disponíveis fora dele. Para evitar isso, você pode usar a redireção de processo (<) com find ... -print0 <(processamento) ou usar um while com redirecionamento de entrada:

# Evitando subshell: redirecionamento de entrada
while IFS= read -r -d '' file; do
    echo "Arquivo: $file"
done < <(find . -name "*.txt" -print0)

Boas práticas

Ao iterar sobre arquivos, siga estas diretrizes para escrever scripts robustos e seguros:

  • Sempre use aspas duplas ao expandir variáveis que contêm nomes de arquivos: "$file".
  • Prefira globbing sempre que possível, pois é mais rápido e seguro que usar comandos externos.
  • Evite ls em scripts; use find com -print0 ou globbing.
  • Configure opções do shell como nullglob (para que padrões sem correspondência resultem em lista vazia) e failglob (para gerar erro se não houver correspondência). Use shopt -s nullglob no início do script se necessário.
  • Use find com -exec ou -execdir para ações simples, evitando loops desnecessários.
  • Teste seu script com nomes de arquivos que contenham espaços, tabs, quebras de linha e caracteres especiais (como \n, \t, !, $).
  • Considere o uso de mapfile (ou readarray) para ler linhas em um array, se você precisar armazenar os nomes.
# Exemplo com nullglob
shopt -s nullglob
for file in *.nonexistent; do
    echo "Isso nunca será executado"
done
shopt -u nullglob

Referências

Exercícios

  1. Escreva um script que liste todos os arquivos .jpg no diretório atual usando globbing e os renomeie para .jpeg (use mv).

    ✓ Resposta:
    #!/bin/bash
    shopt -s nullglob
    for file in *.jpg; do
        mv -- "$file" "${file%.jpg}.jpeg"
    done
  2. Crie um script que use find -print0 e while para contar quantos arquivos regulares existem no diretório /etc (apenas no primeiro nível).

    ✓ Resposta:
    #!/bin/bash
    count=0
    while IFS= read -r -d '' file; do
        ((count++))
    done < <(find /etc -maxdepth 1 -type f -print0)
    echo "Total de arquivos: $count"
  3. Explique por que o seguinte script é perigoso e corrija-o: for file in $(ls *.txt); do echo $file; done.

    ✓ Resposta:

    Perigos: (1) Uso de ls cuja saída é dividida por espaços/linhas; (2) Ausência de aspas em $file no echo. Corrigido:

    for file in *.txt; do
        echo "$file"
    done
  4. Escreva um script que encontre todos os arquivos com nome contendo pelo menos um espaço no diretório atual e os renomeie substituindo espaços por underscores.

    ✓ Resposta:
    #!/bin/bash
    find . -maxdepth 1 -name "* *" -print0 | while IFS= read -r -d '' file; do
        newname="${file// /_}"
        mv -- "$file" "$newname"
    done
  5. Usando find com -exec, escreva um comando que remova todos os arquivos .tmp no diretório /tmp que tenham mais de 7 dias. Use -print para mostrar quais foram removidos.

    ✓ Resposta:
    find /tmp -name "*.tmp" -type f -mtime +7 -print -exec rm {} \;