Iterar sobre arquivos é uma tarefa comum em shell scripts, mas muitas abordagens ingênuas falham quando os nomes dos arquivos contêm espaços, quebras de linha ou outros caracteres especiais. Nesta aula, vamos explorar técnicas seguras para percorrer arquivos, evitando armadilhas que podem quebrar seu script ou causar comportamento inesperado.

Começaremos entendendo por que usar ls em loops é problemático e como o globbing do shell pode ser uma alternativa mais segura. Em seguida, veremos como lidar com casos extremos usando find com -print0 e while read. Por fim, consolidaremos boas práticas para garantir que seus scripts sejam robustos e portáveis.

Globbing vs ls

Uma das armadilhas mais comuns é usar a saída de ls em um loop for. Por exemplo:

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

Esse código quebra se algum arquivo tiver espaço no nome, pois o shell divide a saída de ls por espaços (ou quebras de linha). Além disso, ls pode exibir saída formatada de forma diferente dependendo do sistema, e caracteres especiais podem ser interpretados pelo shell.

O globbing (expansão de caminhos) é uma alternativa segura. O shell expande padrões como *.txt para uma lista de nomes de arquivos, tratando corretamente espaços e caracteres especiais:

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

Esse loop funciona corretamente mesmo que os nomes contenham espaços. No entanto, se não houver arquivos .txt, o padrão permanece literal (*.txt). Para evitar isso, você pode ativar a opção nullglob ou failglob:

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

Com nullglob, se nenhum arquivo corresponder, o loop não executa (em vez de iterar sobre a string literal).

Espaços em nomes

Nomes de arquivos com espaços são um problema clássico. O globbing resolve isso para loops for, mas outras abordagens, como usar find com xargs padrão, também sofrem com espaços. Por exemplo:

find . -name "*.txt" | xargs rm

Se um arquivo se chamar meu arquivo.txt, o xargs interpretará como dois argumentos separados, causando erros. A solução é usar -print0 com xargs -0:

find . -name "*.txt" -print0 | xargs -0 rm

Da mesma forma, ao usar while read, é crucial definir o separador como nulo e ler com -d '' ou -d $'\0':

find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
  echo "Processando: $file"
done

A flag -r evita que barras invertidas sejam interpretadas, e IFS= preserva espaços à esquerda/direita.

find -print0 com while

A combinação find -print0 com while read -d '' é a maneira mais segura de iterar sobre arquivos quando você precisa de flexibilidade (como filtrar por tipo, data, etc.). Exemplo completo:

#!/bin/bash
# Processa 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 "Compactando: $file"
  gzip "$file"
done

Note que as variáveis são sempre citadas ("$file"). Isso protege contra espaços e caracteres especiais. Além disso, o loop while é executado em um subshell (devido ao pipe), então variáveis definidas dentro do loop não persistem após o pipe. Para evitar isso, use a redireção <(find ...) ou while ... done < <(find ...):

while IFS= read -r -d '' file; do
  echo "Arquivo: $file"
  count=$((count + 1))
done < <(find . -name "*.txt" -print0)
echo "Total: $count"

Essa construção mantém o loop no shell principal, permitindo que variáveis como count sejam acessíveis após o loop.

Boas práticas

Aqui estão algumas recomendações para iterar sobre arquivos com segurança:

  • Sempre cite variáveis: Use "$var" ao expandir nomes de arquivos.
  • Prefira globbing a ls: Para operações simples, for file in *.txt é seguro e eficiente.
  • Use nullglob ou failglob: Para evitar iterar sobre padrões literais quando não há correspondências.
  • Para buscas complexas, use find -print0: Combine com while read -d '' para lidar com qualquer caractere.
  • Evite subshells desnecessários: Use redireção de processo < <(find ...) para preservar variáveis.
  • Teste com nomes problemáticos: Crie arquivos com espaços, quebras de linha e caracteres especiais para validar seu script.

Seguindo essas práticas, seus scripts serão robustos e funcionarão corretamente em qualquer ambiente Unix/Linux.

Referências

Exercícios

  1. Escreva um script que liste todos os arquivos .txt no diretório atual, um por linha, usando globbing. Certifique-se de que o script não produza saída se não houver arquivos .txt.

    ✓ Resposta:
    #!/bin/bash
    shopt -s nullglob
    for file in *.txt; do
      echo "$file"
    done
    shopt -u nullglob
  2. O loop abaixo tem um problema. Identifique e corrija:

    for file in $(find . -name "*.pdf"); do
      echo "$file"
    done

    ✓ Resposta: O problema é que a saída de find é dividida por espaços e quebras de linha, quebrando nomes com espaços. Correção:
    while IFS= read -r -d '' file; do
      echo "$file"
    done < <(find . -name "*.pdf" -print0)
  3. Escreva um script que conte o número de linhas de todos os arquivos .log em /var/log (subdiretórios inclusive). Use find -print0 e while.

    ✓ Resposta:
    #!/bin/bash
    total=0
    while IFS= read -r -d '' file; do
      lines=$(wc -l < "$file")
      total=$((total + lines))
    done < <(find /var/log -name "*.log" -type f -print0)
    echo "Total de linhas: $total"
  4. Explique por que o uso de ls em loops é desencorajado e dê um exemplo de como um nome de arquivo com espaço pode quebrar o script.

    ✓ Resposta: ls exibe a saída formatada para humanos, e o shell a divide por espaços, quebras de linha e tabs. Se um arquivo se chama meu arquivo.txt, o loop for file in $(ls) produzirá duas iterações: meu e arquivo.txt. Além disso, ls pode escapar caracteres especiais dependendo das opções, tornando o comportamento imprevisível.
  5. Crie um script que renomeie todos os arquivos .jpg no diretório atual para prefixo_original.jpg, onde prefixo é fornecido como argumento. Use globbing e trate o caso de nenhum arquivo.

    ✓ Resposta:
    #!/bin/bash
    prefix=$1
    if [[ -z $prefix ]]; then
      echo "Uso: $0 prefixo"
      exit 1
    fi
    shopt -s nullglob
    for file in *.jpg; do
      mv "$file" "${prefix}_${file}"
    done
    shopt -u nullglob