para vs encontrar no Bash

28

Ao percorrer os arquivos, há duas maneiras:

  1. use um forloop:

    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 nessas duas opções em desempenho e manipulação?

rubo77
fonte
11
Por quê? findnão abre os arquivos que encontra. A única coisa que posso ver mordendo você aqui em relação a um grande número de arquivos é ARG_MAX .
kojiro
11
Veja as respostas e comentários que informam que read fos nomes dos arquivos serão alterados à medida que forem lidos (por exemplo, nomes com espaços em branco à esquerda). Também find * -pruneparece ser uma maneira muito complicada de dizer simplesmente ls -1sim?
22613 Ian D. Allen
4
Não assuma que os dois loops encontrarão o mesmo conjunto de arquivos; na maioria dos casos, eles não vão. Além disso, isso deveria ser find ., não find *.
22413 Alexis
11
@terdon Sim, analisar ls -lé uma má ideia. Mas analisar ls -1(isso 1não é um l) não é pior do que analisar find * -prune. Ambos falham em arquivos com novas linhas nos nomes.
Ian D. Allen
5
Suspeito que cada um de nós tenha passado mais tempo lendo esta pergunta e respostas do que a diferença total de desempenho ao longo da vida do script em questão.
mpez0

Respostas:

9

1

O primeiro:

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

falha para arquivos chamados -n, -ee variantes como -nenee com algumas implementações festança, com nomes de arquivos que contém barras invertidas.

O segundo:

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

falha para 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 se expande *, findnão faz nada além de imprimir os arquivos que recebe como argumentos. Em printf '%s\n'vez disso, você também poderia ter usado o que, como printfestá embutido, evitaria o erro potencial de muitos argumentos .

2)

A expansão de *é 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)

bashnão tem equivalente, tanto quanto eu posso dizer, então você precisaria recorrer find.

3)

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

(acima, usando uma -print0extensão não padrão do GNU / BSD ).

Isso ainda envolve gerar um comando find e usar um while readloop lento ; portanto, provavelmente será mais lento do que usar o forloop, a menos que a lista de arquivos seja enorme.

4)

Além disso, ao contrário da expansão de curinga do shell, findfará uma lstatchamada 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 -maxdepthextensão que acionará uma otimização, salvando o lstat:

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

Como findinicia a saída de nomes de arquivos assim que os encontra (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 que um buffer stdio (4 / 8 kB). Nesse caso, o processamento no loop será iniciado antes de findconcluir a localização de todos os arquivos. Nos sistemas GNU e FreeBSD, você pode stdbuffazer isso acontecer mais cedo (desativando o buffer do stdio).

5)

A maneira POSIX / standard / portátil de executar comandos para cada arquivo findé usar o -execpredicado:

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

No echoentanto, isso é menos eficiente do que fazer o loop no shell, pois o shell terá uma versão interna do echowhile finde precisará gerar um novo processo e executá /bin/echo-lo para cada arquivo.

Se você precisar executar vários comandos, poderá:

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

Mas cuidado, isso cmd2só é executado se cmd1for 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 {} +

Nesse momento, voltamos a ser eficientes, echopois estamos usando sho incorporado e a -exec +versão gera o shmínimo possível.

7)

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

Stéphane Chazelas
fonte
o que !faz no comando find?
rubo77
@ Rubo77, !é para negação. ! -name . -prune more...fará -prune(e more...como -prunesempre retorna true) para todos os arquivos, exceto .. Portanto, ele funcionará more...em todos os arquivos incluídos ., mas excluirá .e não descerá em subdiretórios de .. Portanto, é o equivalente padrão do GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

Eu tentei isso em um diretório com 2259 entradas e usei o timecomando

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 executei cada comando várias vezes, para eliminar erros de cache. Isso sugere que mantê-lo bash(para i em ...) é mais rápido do que usar finde canalizar a saída (para bash)

Apenas para completar, deixei cair o cano find, já que no seu exemplo, é totalmente redundante. A saída de just 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 *é mais rápido: não está produzindo tantas novas linhas; portanto, a saída não está rolando tanto. 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/nullproduz:

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

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

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

e finalmente: time echo * > /dev/nullproduz:

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

Algumas das variações podem ser explicadas por fatores aleatórios, mas parece claro:

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

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

EDITAR:

Horários para find . -maxdepth 1 > /dev/nullvs. 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 do que find . -maxdepth 1- no primeiro, o shell está processando um glob e construindo uma linha de comando (grande) para find. NB: find . -pruneretorna apenas ..

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

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

Conclusão:

  • maneira mais lenta de fazê-lo até agora. Como foi indicado nos comentários para a resposta em que essa abordagem foi sugerida, cada argumento gera uma casca.
Phil
fonte
Qual canal é redundante? você pode mostrar a linha que usou sem cachimbo?
rubo77
2
O @ rubo77 find * -prune | while read f; do echo "$f"; donepossui o tubo redundante - tudo o que o tubo está fazendo é produzir exatamente o que é produzido findpor si próprio. Sem um pipe, seria simplesmente find * -prune O pipe é redundante apenas especificamente porque a coisa do outro lado do pipe simplesmente copia stdin para stdout (na maior parte). É um no-op caro. Se você quiser fazer coisas com a saída de find, além de cuspir novamente, é diferente.
Phil
Talvez o principal consumo de tempo seja o *. Como o BitsOfNix afirmou: Eu ainda sugiro fortemente não usar *e .em findvez disso.
rubo77
@ rubo77 parece assim. Acho que esqueci isso. Adicionei descobertas para o meu sistema. Eu suponho que find . -pruneé mais rápido, porque findestará lendo uma entrada de diretório literalmente, enquanto o shell estará fazendo o mesmo, potencialmente compatível com a glob (pode ser otimizada para *), e então construindo a grande linha de comando para find.
22413 Phil
11
find . -pruneimprime apenas .no meu sistema. Quase não funciona. Não é o mesmo find * -pruneque mostra todos os nomes no diretório atual. Um simples read firá modificar nomes de arquivos com espaços à esquerda.
Ian D. Allen
10

Eu iria definitivamente com o find, embora eu alterasse o seu achado para exatamente 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 forexibirá apenas os arquivos / diretórios no diretório atual, mas não o conteúdo dos diretórios. Se você usar o find, também mostrará o conteúdo dos subdiretórios.

Eu digo que o find é melhor, pois com o seu foro *arquivo terá que ser expandido primeiro e eu tenho medo de que, se você tiver um diretório com uma quantidade enorme de arquivos, ele possa dar uma lista de argumentos de erro muito longa . O mesmo vale parafind *

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

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
fonte
Eu adicionei -prunepara tornar os dois exemplos mais parecidos. e eu prefiro o tubo com enquanto isso, é mais fácil de aplicar mais comandos no circuito
rubo77
alterar o limite rígido dificilmente é uma solução adequada do meu POV. Especialmente quando se fala em mais de 2 milhões de arquivos. Sem digressão da pergunta, para casos simples, como o diretório de um nível é mais rápido, mas se você alterar sua estrutura de arquivos / diretórios, será mais difícil migrar. Enquanto estiver com o find e sua enorme quantidade de opções, você pode estar melhor preparado. Ainda assim, eu ainda sugiro não usar * e. para encontrar em seu lugar. Seria mais portátil do que * onde você pode não ser capaz de controlar o hardlimit ...
BitsOfNix
4
Isso gerará um processo de eco por arquivo (enquanto estiver no shell for loop, é o eco interno que será usado sem forçar um processo extra) e descerá nos diretórios, por isso será muito mais lento . Observe também que ele incluirá arquivos de ponto.
Stéphane Chazelas
Você está certo, eu adicionei o maxdepth 1 para que ele permaneça apenas no nível atual.
BitsOfNix
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 -ropção read. Este não é um problema com o forloop.
  • Novas linhas nos caminhos quebrariam qualquer funcionalidade não trivial dentro do loop. Este não é um problema com o forloop.

findÉ difícil lidar com qualquer nome de arquivo , portanto, você deve usar a foropção de loop sempre que possível por esse motivo. Além disso, a execução de um programa externo como find, em geral, será mais lenta do que a execução de um comando de loop interno for.

l0b0
fonte
@ I0b0 E quanto a find -path './*' -prune ou find -path './[^. ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
11
Nem find's -print0nem xargs' -0são POSIX compatível, e você não pode colocar comandos arbitrários em sh -c ' ... '(aspas simples não pode ser escapado dentro de aspas simples), por isso não é tão simples.
L0b0 23/10
4

Mas somos otários por questões de desempenho! Essa solicitação de experiência faz pelo menos duas suposições que a tornam terrivelmente válida.

A. Suponha que eles encontrem os mesmos arquivos ...

Bem, eles vão encontrar os mesmos arquivos em primeiro lugar, porque eles são ambos iteração sobre o mesmo glob, a saber *. Mas find * -prune | while read fsofre de várias falhas que tornam possível que não encontre todos os arquivos que você espera:

  1. Não é garantido que a localização do POSIX aceite mais de um argumento de caminho. A maioria das findimplementações faz, mas ainda assim, você não deve confiar nisso.
  2. find *pode quebrar quando você bate ARG_MAX. for f in *, porque ARG_MAXse aplica a exec, não embutidos.
  3. while read fpode romper com os nomes de arquivos que começam e terminam em espaço em branco, que será removido. Você pode superar isso com while readseu parâmetro padrão REPLY, mas isso ainda não ajudará quando se trata de nomes de arquivos com novas linhas.

B. echo. Ninguém fará isso apenas para ecoar o nome do arquivo. Se você quiser isso, basta fazer um destes:

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

O whilecanal para o loop aqui cria um subshell implícito que fecha quando o loop termina, o que pode não ser adequado 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
kojiro
fonte
Eu não concordo com a afirmação ao mesmo tempo desova de loop um subnível - na pior das hipóteses, um novo segmento: o seguinte é tentar mostrar antes e depois, desculpas para a má formatação$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
Phil
Tecnicamente, eu errei: o pipe causa o subshell implícito, não o loop while. Eu vou editar.
Kojiro # 22/13
2

find *não funcionará corretamente se *produzir tokens que parecem predicados e não caminhos.

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

Para corrigir esse problema, você pode usar find ./*. Mas então não está produzindo exatamente as mesmas seqüências de caracteres for x in *.

Observe que find ./* -prune | while read f .., na verdade, não usa a funcionalidade de digitalização de find. É a sintaxe globbing ./*que realmente atravessa o diretório e gera nomes. Em seguida, o findprograma precisará executar pelo menos uma statverificação em cada um desses nomes. Você tem a sobrecarga de iniciar o programa e fazê-lo acessar esses arquivos e, em seguida, executar E / S para ler sua saída.

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

Kaz
fonte
1

Bom para iniciantes foré uma palavra-chave 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 forloop encontrará apenas os arquivos do personagem globstar quando se expandir, e não recursará em nenhum diretório encontrado.

A busca por outro lado também receberá uma lista expandida pela globstar, mas encontrará recursivamente todos os arquivos e diretórios abaixo dessa lista expandida e direcionará cada um deles para o whileloop.

Ambas as abordagens podem ser consideradas perigosas no sentido de que não tratam caminhos ou nomes de arquivos que contêm espaços.

É tudo o que consigo pensar em comentar essas duas abordagens.

slm
fonte
Eu adicionei -une ao comando find, para que eles sejam mais parecidos.
rubo77
0

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

find * |xargs some-command
Roubar
fonte
0

Por anos eu tenho usado isso: -

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

procurar certos arquivos (por exemplo, * .txt) que contenham um padrão que o grep possa procurar e canalizá-lo mais para que ele não role para fora da tela. Às vezes, uso o pipe >> para gravar os resultados em outro arquivo que posso ver mais tarde.

Aqui está uma amostra do resultado: -

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./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
Allen
fonte