Como faço para analisar a saída do comando find quando nomes de arquivos possuem espaços neles?

11

Usando um loop como

for i in 'find . -name \*.txt' 

irá quebrar se alguns nomes de arquivos tiverem espaços neles.

Qual técnica posso usar para evitar esse problema?

    
por Scott C Wilson 08.04.2012 / 00:59

5 respostas

11

O ideal é que você não faça isso de maneira alguma, porque analisar nomes de arquivos adequadamente em um script de shell é sempre difícil (conserte-os para espaços, você ainda terá problemas com outros caracteres incorporados, em particular com nova linha). Isso é listado como a primeira entrada na página BashPitfalls.

Dito isto, há uma maneira de quase fazer o que você quer:

oIFS=$IFS
IFS=$'\n'

find . -name '*.txt' | while read -r i; do
  # use "$i" with whatever you're doing
done

IFS=$oIFS

Lembre-se também de citar $i ao usá-lo, para evitar outras coisas ao interpretar os espaços mais tarde. Lembre-se também de definir $IFS de volta depois de usá-lo, porque não fazer isso causará erros desconcertantes depois.

Isso tem uma outra ressalva anexada: o que acontece dentro do loop while pode ocorrer em um subshell, dependendo do shell exato que você está usando, portanto as configurações variáveis podem não persistir. A versão for do loop evita isso, mas pelo preço que, mesmo se você aplicar a solução $IFS para evitar problemas com espaços, você terá problemas se o find retornar muitos arquivos.

Em algum momento, a correção correta para tudo isso é fazê-lo em uma linguagem como Perl ou Python, em vez de shell.

    
por 08.04.2012 / 01:14
11

Use find -print0 e canalize para xargs -0 , ou escreva seu próprio programa C e canalize-o para seu pequeno programa em C. É para isso que -print0 e -0 foram inventados.

Os scripts de shell não são a melhor maneira de lidar com nomes de arquivos com espaços: você pode fazer isso, mas fica desajeitado.

    
por 08.04.2012 / 03:17
2

Você pode definir o "separador de campo interno" ( IFS ) como algo diferente de espaço para a divisão do argumento de loop, por exemplo,

ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
    IFS=${ORIGIFS}
    #do stuff
done
IFS=${ORIGIFS}

Eu reponho o IFS após seu uso, principalmente porque parece legal, eu acho. Eu não vi nenhum problema em tê-lo definido para nova linha, mas acho que isso é "mais limpo".

Outro método, dependendo do que você deseja fazer com a saída de find , é usar diretamente -exec com o comando find ou usar -print0 e canalizá-lo para xargs -0 . No primeiro caso, find cuida do escape do nome do arquivo. No caso -print0 , find imprime sua saída com um separador nulo e, em seguida, xargs divide. Como nenhum nome de arquivo pode conter esse caractere (o que eu sei), isso também é seguro. Isso é mais útil em casos simples; e geralmente não é um ótimo substituto para um loop for completo.

    
por 08.04.2012 / 01:13
1

Usando find -print0 com xargs -0

Usar find -print0 combinado com xargs -0 é completamente robusto em relação aos nomes de arquivos legais e é um dos métodos mais extensíveis disponíveis. Por exemplo, digamos que você queria uma listagem de todos os arquivos PDF no diretório atual. Você poderia escrever

$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo

Isso encontrará cada PDF (via -iname '*.pdf' ) no diretório atual ( . ) e qualquer subdiretório, e passará cada um deles como um argumento para o comando echo . Como especificamos a opção -n 1 , xargs passará apenas um argumento por vez para echo . Se tivéssemos omitido essa opção, xargs teria passado o maior número possível para echo . (Você pode echo short input | xargs --show-limits para ver quantos bytes são permitidos em uma linha de comando).

O que o xargs faz exatamente?

Podemos ver claramente o efeito que xargs tem em 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 perfeitamente com espaços e novas linhas,

$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh

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
por 22.08.2018 / 21:34
1

Eu não concordo com os bash bashers, porque bash , junto com o conjunto de ferramentas * nix, é bastante habilidoso no tratamento de arquivos (incluindo aqueles cujos nomes possuem espaço em branco incorporado).

Na verdade, find lhe dá um bom controle sobre a escolha de quais arquivos processar ... No lado bash, você realmente só precisa perceber que você deve fazer strings em bash words ; normalmente usando "aspas duplas" ou algum outro mecanismo como o uso do IFS, ou% s {}

Observe que na maioria das situações você não precisa definir e redefinir o IFS; basta usar o IFS localmente, conforme mostrado nos exemplos abaixo. Todos os três manipulam os espaços em branco bem. Além disso, você não precisa de uma estrutura de loop "padrão", porque 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//'  

'encontrar also allows you to pass multiple filenames as args to you script ..(if it suits your need: use + instead \;')

    
por 08.04.2012 / 12:25

Tags