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 $iao usá-lo, para evitar que outras coisas interpretem os espaços posteriormente. Lembre-se também de $IFSvoltar atrás depois de usá-lo, pois isso não causará erros desconcertantes posteriormente.
Isso tem uma outra ressalva: o que acontece dentro do whileloop pode ocorrer em um subshell, dependendo do shell exato que você está usando, portanto, as configurações variáveis podem não persistir. A forversão em loop evita isso, mas a um preço que, mesmo que você aplique a $IFSsolução para evitar problemas com espaços, você terá problemas se findretornar 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 -print0e xargs -0escreva-o ou escreva seu próprio programa C e encaminhe-o ao seu pequeno programa C. É para isso -print0e -0foi 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 IFSapó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 -execcom o findcomando ou usá -print0-lo xargs -0. No primeiro caso, findcuida do nome do arquivo que está escapando. No -print0caso, findimprime sua saída com um separador nulo e depois xargsdivide-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 forloop completo .
find -print0comxargs -0O uso find -print0combinado 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 echocomando. Como especificamos a -n 1opção, xargspassamos apenas um argumento de cada vez para echo. Se tivéssemos omitido essa opção, xargsteria passado o maior número possível echo. (Você pode echo short input | xargs --show-limitsver quantos bytes são permitidos em uma linha de comando.)
xargsfaz exatamente?Podemos ver claramente o efeito que xargstem sobre a sua entrada - e o efeito de -nem 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 bashbashers, 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, findoferece 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 -print0exargs -0.