for vs find em Bash

26

Ao fazer o loop pelos arquivos, existem duas maneiras:

  1. use um for -loop:

    for f in *; do
        echo "$f"
    done
    
  2. use find :

    find * -prune | while read f; do 
        echo "$f"
    done
    

Supondo que esses dois loops encontrarão a mesma lista de arquivos, quais são as diferenças entre essas duas opções no perfomance e no manuseio?

    
por rubo77 22.10.2013 / 08:43

9 respostas

9

1.

O primeiro:

for f in *; do
  echo "$f"
done

falha em arquivos chamados -n , -e e variantes como -nene e com algumas implementações do bash, com nomes de arquivos contendo barras invertidas.

O segundo:

find * -prune | while read f; do 
  echo "$f"
done

falha em ainda mais casos (arquivos chamados ! , -H , -name , ( , nomes de arquivos que começam ou terminam com espaços em branco ou contêm caracteres de nova linha ...)

É o shell que expande * , find não faz nada além de imprimir os arquivos que recebe como argumentos. Você poderia também ter usado printf '%s\n' em vez disso, pois como printf está embutido, também evitaria o erro potencial muitos args .

2.

A expansão de * está classificada, você pode torná-la um pouco mais rápida se não precisar da classificação. Em zsh :

for f (*(oN)) printf '%s\n' $f

ou simplesmente:

printf '%s\n' *(oN)

bash não tem equivalente, pelo que eu posso dizer, então você precisa recorrer a find .

3.

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(acima usando uma extensão não padrão do GNU / BSD -print0 ).

Isso ainda envolve gerar um comando find e usar um loop while read lento, então provavelmente será mais lento do que usar o loop for , a menos que a lista de arquivos seja enorme.

4.

Além disso, ao contrário da expansão com curinga, find fará uma chamada lstat do sistema em cada arquivo, portanto, é improvável que a não classificação compense isso.

Com o GNU / BSD find , isso pode ser evitado usando sua extensão -maxdepth , que acionará uma otimização para salvar o lstat :

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

Como find inicia a saída de nomes de arquivos assim que eles são encontrados (exceto para o buffer de saída stdio), onde pode ser mais rápido é se o que você faz no loop é demorado e a lista de nomes de arquivos é mais do que um buffer stdio (4/8 kB). Nesse caso, o processamento dentro do loop será iniciado antes que find tenha encontrado todos os arquivos. Nos sistemas GNU e FreeBSD, você pode usar stdbuf para fazer com que isso aconteça mais cedo (desabilitando o buffer stdio).

5.

A maneira POSIX / padrão / portátil de executar comandos para cada arquivo com find é usar o predicado -exec :

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

No caso de echo , é menos eficiente do que fazer o loop no shell, pois o shell terá uma versão incorporada de echo , enquanto find precisará gerar um novo processo e executar /bin/echo nele para cada arquivo.

Se você precisar executar vários comandos, faça o seguinte:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

Mas tenha em atenção que cmd2 só é executado se cmd1 for bem sucedido.

6.

Uma maneira canônica de executar comandos complexos para cada arquivo é chamar um shell com -exec ... {} + :

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

Dessa vez, voltamos a ser eficientes com echo , pois estamos usando sh e a versão -exec + gera o mínimo de sh possível.

7.

Em meus testes em um diretório com 200.000 arquivos com nomes curtos no ext4, o zsh one (parágrafo 2.) é de longe o mais rápido, seguido pelo primeiro loop for i in * simples (embora, como de costume, bash é muito mais lento que outros shells para isso).

    
por 22.10.2013 / 12:44
18

Eu tentei isso em um diretório com 2259 entradas e usei o comando time .

A saída de time for f in *; do echo "$f"; done (menos os arquivos!) é:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

A saída de time find * -prune | while read f; do echo "$f"; done (menos os arquivos!) é:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

Eu corri cada comando várias vezes, para eliminar erros de cache. Isso sugere que mantê-lo em bash (para i em ...) é mais rápido do que usar find e canalizar a saída (para bash )

Apenas para completar, deixei cair o pipe de find , já que no seu exemplo, é totalmente redundante. A saída de apenas find * -prune é:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

Além disso, time echo * (a saída não é separada por nova linha, infelizmente):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

Neste ponto, suspeito que o motivo echo * seja mais rápido, pois ele não está gerando tantas linhas novas, então a saída não está rolando muito. Vamos testar ...

time find * -prune | while read f; do echo "$f"; done > /dev/null

rendimentos:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

enquanto time find * -prune > /dev/null produz:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

e time for f in *; do echo "$f"; done > /dev/null de rendimento:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

e finalmente: time echo * > /dev/null produz:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

Parte da variação pode ser explicada por fatores aleatórios, mas parece claro:

  • a saída está lenta
  • a tubulação custa um pouco
  • for f in *; do ... é mais lento que find * -prune , por si só, mas para as construções acima envolvendo tubos, é mais rápido.

Além disso, como um lado, ambas as abordagens parecem manipular nomes com espaços muito bem.

EDITAR:

Horários para find . -maxdepth 1 > /dev/null vs. find * -prune > /dev/null :

time find . -maxdepth 1 > /dev/null :

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null :

real    0m0.031s
user    0m0.020s
sys     0m0.008s

Então, conclusão adicional:

  • find * -prune é mais lento que find . -maxdepth 1 - no primeiro, o shell está processando um glob e, em seguida, construindo uma linha de comando (grande) para find . NB: find . -prune retorna apenas . .

Mais testes: time find . -maxdepth 1 -exec echo {} \; >/dev/null :

real    0m3.389s
user    0m0.040s
sys     0m0.412s

Conclusão:

  • a maneira mais lenta de fazer isso até agora. Como foi apontado nos comentários para a resposta onde esta abordagem foi sugerida, cada argumento gera uma concha.
por 22.10.2013 / 09:42
9

Eu iria definitivamente com o find, embora eu mudasse seu achado apenas para isso:

find . -maxdepth 1 -exec echo {} \;

Em termos de desempenho, find é muito mais rápido, dependendo das suas necessidades, é claro. O que você tem atualmente com for exibirá somente os arquivos / diretórios no diretório atual, mas não o conteúdo dos diretórios. Se você usar, também mostrará o conteúdo dos subdiretórios.

Digo que o find é melhor, pois com o for o * precisará ser expandido primeiro e tenho medo de que, se você tiver um diretório com uma quantidade enorme de arquivos, ele possa dar o argumento de erro lista muito longa . O mesmo vale para find *

Como exemplo, em um dos sistemas que eu uso atualmente, existem alguns diretórios com mais de 2 milhões de arquivos (< 100k cada):

find *
-bash: /usr/bin/find: Argument list too long
    
por 22.10.2013 / 09:00
7
find * -prune | while read f; do 
    echo "$f"
done

é um uso inútil de find - o que você está dizendo é efetivamente "para cada arquivo no diretório ( * ), não encontra nenhum arquivo. Além disso, não é seguro por vários motivos:

  • As barras invertidas nos caminhos são tratadas especialmente sem a opção -r para read . Isso não é um problema com o loop for .
  • Novas linhas em caminhos quebrariam qualquer funcionalidade não trivial dentro do loop. Isso não é um problema com o loop for .

O tratamento de qualquer nome de arquivo com find é difícil , então você deve usar a opção for loop sempre que possível apenas por esse motivo. Além disso, executar um programa externo como find será, em geral, mais lento do que executar um comando de loop interno como for .

    
por 22.10.2013 / 12:27
4

Mas somos otários para questões de desempenho! Este pedido-para-experimento faz pelo menos duas suposições que não o tornam terrivelmente válido.

Suponha que eles encontrem os mesmos arquivos…

Bem, eles encontrarão os mesmos arquivos no início, porque estão interagindo com o mesmo glob, ou seja, * . Mas find * -prune | while read f sofre de várias falhas que tornam possível que não encontre todos os arquivos que você espera:

  1. O achado do POSIX não é garantido para aceitar mais de um argumento de caminho. A maioria das implementações de find , mas ainda assim, você não deve confiar nisso.
  2. find * pode quebrar quando você atinge ARG_MAX . for f in * não, porque ARG_MAX se aplica a exec , não a builtins.
  3. while read f pode quebrar com nomes de arquivos que começam e terminam com espaços em branco, que serão removidos. Você poderia superar isso com while read e seu parâmetro padrão REPLY , mas isso ainda não ajudará quando se tratar de nomes de arquivos com novas linhas neles.

%código%. Ninguém vai fazer isso apenas para ecoar o nome do arquivo. Se você quiser, basta fazer um destes:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

O canal para o loop echo aqui cria um subshell implícito que é fechado quando o loop termina, o que pode ser pouco intuitivo para alguns.

Para responder à pergunta, aqui estão os resultados em um diretório meu que possui 184 arquivos e diretórios.

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s
    
por 22.10.2013 / 13:59
2

find * não funcionará corretamente se * produzir tokens que se pareçam com predicados em vez de caminhos.

Você não pode usar o argumento usual -- para corrigir isso porque -- indica o fim das opções e as opções do find vêm antes dos caminhos.

Para corrigir esse problema, você pode usar find ./* . Mas então não está produzindo exatamente as mesmas strings que for x in * .

Observe que find ./* -prune | while read f .. na verdade não usa a funcionalidade de verificação de find . É a sintaxe de globbing ./* que realmente percorre o diretório e gera nomes. Em seguida, o programa find terá que executar pelo menos uma verificação de stat em cada um desses nomes. Você tem a sobrecarga de iniciar o programa e fazer com que ele acesse esses arquivos e, em seguida, fazer a E / S para ler sua saída.

É difícil imaginar como poderia ser algo menos eficiente do que for x in ./* ... .

    
por 22.10.2013 / 23:51
1

Bem, para iniciantes for é uma palavra-chave do shell, incorporada ao Bash, enquanto find é um executável separado.

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

O loop for só encontrará os arquivos do caractere globstar quando ele se expande, e não recorrerá a nenhum diretório encontrado.

Localizar por outro lado também receberá uma lista expandida pelo globstar, mas ele encontrará recursivamente todos os arquivos e diretórios abaixo dessa lista expandida e canalizará cada um deles até o loop while .

Essas duas abordagens podem ser consideradas perigosas no sentido de não manipularem caminhos ou nomes de arquivos que contenham espaços.

Isso é tudo o que posso pensar em comentar sobre essas duas abordagens.

    
por 22.10.2013 / 08:55
0

Se todos os arquivos retornados pelo find puderem ser processados por um único comando (obviamente não aplicável ao seu exemplo de eco acima), você pode usar xargs:

find * |xargs some-command
    
por 22.10.2013 / 18:59
0

Por anos eu tenho usado isso: -

find . -name 'filename'|xargs grep 'pattern'|more

para procurar determinados arquivos (por exemplo, * .txt) que contenham um padrão que o grep possa procurar e canalizar mais para que ele não saia da tela. Às vezes eu uso o > > pipe para gravar os resultados em outro arquivo que eu possa ver depois.

Aqui está uma amostra do resultado: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <[email protected]>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <[email protected]>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <[email protected]>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <[email protected]>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
    
por 24.10.2013 / 22:12