Iterando sobre arquivos com segurança
Aprenda a iterar sobre arquivos em shell script de forma segura, evitando problemas com espaços e caracteres especiais. Esta aula cobre as diferenças entre globbing e ls, como lidar com espaços em nomes, o uso de find -print0 com while, e boas práticas essenciais.
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
lsem scripts; usefindcom-print0ou globbing. - Configure opções do shell como
nullglob(para que padrões sem correspondência resultem em lista vazia) efailglob(para gerar erro se não houver correspondência). Useshopt -s nullglobno início do script se necessário. - Use
findcom-execou-execdirpara 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(oureadarray) 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
- Bash Pattern Matching (globbing) - GNU
- Word Splitting - Bash Manual
- GNU Findutils Manual
- Bash FAQ: How can I find and safely handle file names containing spaces, newlines, etc.?
- Bash Hackers Wiki: Positional Parameters and Loops
- ShellCheck - Static Analysis Tool for Shell Scripts
Exercícios
-
Escreva um script que liste todos os arquivos
.jpgno diretório atual usando globbing e os renomeie para.jpeg(usemv).✓ Resposta:#!/bin/bash shopt -s nullglob for file in *.jpg; do mv -- "$file" "${file%.jpg}.jpeg" done -
Crie um script que use
find -print0ewhilepara 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" -
Explique por que o seguinte script é perigoso e corrija-o:
for file in $(ls *.txt); do echo $file; done.✓ Resposta:Perigos: (1) Uso de
lscuja saída é dividida por espaços/linhas; (2) Ausência de aspas em$fileno echo. Corrigido:for file in *.txt; do echo "$file" done -
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 -
Usando
findcom-exec, escreva um comando que remova todos os arquivos.tmpno diretório/tmpque tenham mais de 7 dias. Use-printpara mostrar quais foram removidos.✓ Resposta:find /tmp -name "*.tmp" -type f -mtime +7 -print -exec rm {} \;