Usando um loop como
for i in `find . -name \*.txt`
será interrompido se alguns nomes de arquivos tiverem espaços.
Que técnica eu posso usar para evitar esse problema?
Usando um loop como
for i in `find . -name \*.txt`
será interrompido se alguns nomes de arquivos tiverem espaços.
Que técnica eu posso usar para evitar esse problema?
Respostas:
O ideal é que você não faça dessa maneira, porque analisar nomes de arquivos corretamente em um shell script é sempre difícil (corrija-o por espaços, você ainda terá problemas com outros caracteres incorporados, em particular a nova linha). Isso é listado como a primeira entrada na página BashPitfalls.
Dito isto, há uma maneira de quase fazer o que você deseja:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Lembre-se de citar também $i
ao usá-lo, para evitar que outras coisas interpretem os espaços posteriormente. Lembre-se também de $IFS
voltar atrás depois de usá-lo, pois isso não causará erros desconcertantes posteriormente.
Isso tem uma outra ressalva: o que acontece dentro do while
loop pode ocorrer em um subshell, dependendo do shell exato que você está usando, portanto, as configurações variáveis podem não persistir. A for
versão em loop evita isso, mas a um preço que, mesmo que você aplique a $IFS
solução para evitar problemas com espaços, você terá problemas se find
retornar muitos arquivos.
Em algum momento, a correção correta para tudo isso passa a ser feita em uma linguagem como Perl ou Python, em vez de shell.
Use-o find -print0
e xargs -0
escreva-o ou escreva seu próprio programa C e encaminhe-o ao seu pequeno programa C. É para isso -print0
e -0
foi inventado.
Os scripts de shell não são a melhor maneira de lidar com nomes de arquivos com espaços: você pode fazê-lo, mas fica complicado.
Você pode definir o "separador de campo interno" ( IFS
) como algo diferente de espaço para a divisão do argumento do loop, por exemplo
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Eu redefinir IFS
após o seu uso em find, principalmente porque parece bom, eu acho. Não vi nenhum problema em configurá-lo para nova linha, mas acho que isso é "mais limpo".
Outro método, dependendo do que você deseja fazer com a saída find
, é usar diretamente -exec
com o find
comando ou usá -print0
-lo xargs -0
. No primeiro caso, find
cuida do nome do arquivo que está escapando. No -print0
caso, find
imprime sua saída com um separador nulo e depois xargs
divide-o. Como nenhum nome de arquivo pode conter esse caractere (o que eu sei), isso também é sempre seguro. Isso é útil principalmente em casos simples; e geralmente não é um ótimo substituto para um for
loop completo .
find -print0
comxargs -0
O uso find -print0
combinado com xargs -0
é totalmente robusto contra nomes de arquivos legais e é um dos métodos mais extensíveis disponíveis. Por exemplo, suponha que você queira uma lista de todos os arquivos PDF no diretório atual. Você poderia escrever
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Ele encontrará todos os PDFs (via -iname '*.pdf'
) no diretório atual ( .
) e em qualquer subdiretório e passará cada um deles como argumento para o echo
comando. Como especificamos a -n 1
opção, xargs
passamos apenas um argumento de cada vez para echo
. Se tivéssemos omitido essa opção, xargs
teria passado o maior número possível echo
. (Você pode echo short input | xargs --show-limits
ver quantos bytes são permitidos em uma linha de comando.)
xargs
faz exatamente?Podemos ver claramente o efeito que xargs
tem sobre a sua entrada - e o efeito de -n
em particular - usando um script que ecoa seus argumentos de uma maneira mais precisa do que echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Observe que ele lida com espaços e novas linhas perfeitamente bem,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
o que seria especialmente problemático com a seguinte solução comum:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Notas
Não concordo com os bash
bashers, porque bash
, junto com o conjunto de ferramentas * nix, é bastante hábil em lidar com arquivos (incluindo aqueles cujos nomes incorporaram espaços em branco).
Na verdade, find
oferece um controle minucioso sobre a escolha dos arquivos a serem processados ... No lado do bash, você realmente só precisa perceber que deve fazer as coisas bash words
; normalmente usando "aspas duplas" ou algum outro mecanismo como o IFS ou{}
Observe que na maioria / muitas situações você não precisa definir e redefinir o IFS; basta usar o IFS localmente, como mostrado nos exemplos abaixo. Todos os três lidam bem com o espaço em branco. Além disso, você não precisa de uma estrutura de loop "padrão", porque o find \;
é efetivamente um loop; basta colocar sua lógica de loop em uma função bash (se você não estiver chamando uma ferramenta padrão).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
E mais dois exemplos
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'encontre also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+ instead
\; `)
find -print0
exargs -0
.