Iterando sobre arquivos com segurança
Esta aula aborda como iterar sobre arquivos em shell script de forma segura, evitando problemas com nomes que contêm espaços, quebras de linha ou caracteres especiais. Você aprenderá a diferença entre globbing e parsing de ls, como usar find com -print0 e while read, além de boas práticas para scripts robustos.
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"
doneEsse 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"
doneEsse 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 nullglobCom 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 rmSe 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 rmDa 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"
doneA 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"
doneNote 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
nullgloboufailglob: Para evitar iterar sobre padrões literais quando não há correspondências. - Para buscas complexas, use
find -print0: Combine comwhile 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
- Bash Manual: Filename Expansion
- Bash Manual: Conditional Expressions
- POSIX find utility
- Bash FAQ: How can I find and safely handle file names containing newlines, spaces or both?
- ShellCheck: Static analysis tool for shell scripts
- Filenames and Pathnames in Shell: How to do it correctly
Exercícios
Escreva um script que liste todos os arquivos
.txtno 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 nullglobO 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 defindé 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)Escreva um script que conte o número de linhas de todos os arquivos
.logem/var/log(subdiretórios inclusive). Usefind -print0ewhile.✓ 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"Explique por que o uso de
lsem loops é desencorajado e dê um exemplo de como um nome de arquivo com espaço pode quebrar o script.✓ Resposta:lsexibe a saída formatada para humanos, e o shell a divide por espaços, quebras de linha e tabs. Se um arquivo se chamameu arquivo.txt, o loopfor file in $(ls)produzirá duas iterações:meuearquivo.txt. Além disso,lspode escapar caracteres especiais dependendo das opções, tornando o comportamento imprevisível.Crie um script que renomeie todos os arquivos
.jpgno diretório atual paraprefixo_original.jpg, ondeprefixoé 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