para vs encontrar en Bash

28

Al recorrer los archivos hay dos formas:

  1. usa un forbucle:

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

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

Suponiendo que estos dos bucles encontrarán la misma lista de archivos, ¿cuáles son las diferencias en esas dos opciones en rendimiento y manejo?

rubo77
fuente
1
¿Por qué? findno abre los archivos que encuentra. Lo único que puedo ver mordiéndote aquí con respecto a una gran cantidad de archivos es ARG_MAX .
kojiro
1
Vea las respuestas y comentarios que le dicen que read fdestrozará los nombres de los archivos a medida que los lee (por ejemplo, nombres con espacios en blanco iniciales). También find * -pruneparece ser una forma muy complicada de decir simplemente ls -1sí.
Ian D. Allen
44
No asuma que los dos bucles encontrarán el mismo conjunto de archivos; en la mayoría de los casos, no lo harán. Además, eso debería ser find ., no find *.
alexis
1
@terdon Sí, analizar ls -les una mala idea. Pero analizar ls -1(eso 1no es un l) no es peor que analizar find * -prune. Ambos fallan en archivos con nuevas líneas en los nombres.
Ian D. Allen
55
Sospecho que cada uno de nosotros pasó más tiempo leyendo esta pregunta y sus respuestas que la diferencia total en el rendimiento durante la vida del guión en cuestión.
mpez0

Respuestas:

9

1)

El primero:

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

falla por archivos llamados -n, -ey variantes como -neney con algunas implementaciones de bash, con los nombres de archivo que contiene las barras invertidas.

El segundo:

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

falla para aún más casos (llamados archivos !, -H, -name, (, nombres de archivo que comienzan o terminan con espacios en blanco o caracteres de nueva línea ...)

Es el shell que se expande *, findno hace más que imprimir los archivos que recibe como argumentos. También podría haber utilizado en su printf '%s\n'lugar lo que printfestá incorporado también evitaría el error potencial de demasiados argumentos .

2)

La expansión de *está ordenada, puede hacerlo un poco más rápido si no necesita la clasificación. En zsh:

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

o simplemente:

printf '%s\n' *(oN)

bashno tiene equivalente por lo que puedo decir, por lo que tendrías que recurrir find.

3)

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

(arriba usando una -print0extensión no estándar GNU / BSD ).

Eso todavía implica generar un comando de búsqueda y usar un while readbucle lento , por lo que probablemente será más lento que usar el forbucle a menos que la lista de archivos sea enorme.

4)

Además, a diferencia de la expansión de comodín de shell, findhará una lstatllamada al sistema en cada archivo, por lo que es poco probable que la no clasificación compense eso.

Con GNU / BSD find, eso puede evitarse utilizando su -maxdepthextensión que activará una optimización guardando lstat:

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

Debido a que findcomienza a generar nombres de archivos tan pronto como los encuentra (excepto para el búfer de salida stdio), donde puede ser más rápido es si lo que hace en el bucle lleva mucho tiempo y la lista de nombres de archivo es más que un búfer stdio (4 / 8 kB). En ese caso, el procesamiento dentro del ciclo comenzará antes de que findhaya terminado de encontrar todos los archivos. En los sistemas GNU y FreeBSD, puede usar stdbufpara hacer que eso suceda antes (deshabilitando el almacenamiento en búfer stdio).

5)

La forma POSIX / estándar / portátil de ejecutar comandos para cada archivo findes usar el -execpredicado:

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

Sin echoembargo, en el caso de que, sea menos eficiente que hacer el bucle en el shell, ya que el shell tendrá una versión incorporada de echowhile find, tendrá que generar un nuevo proceso y ejecutarlo /bin/echopara cada archivo.

Si necesita ejecutar varios comandos, puede hacer:

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

Pero ten cuidado porque cmd2solo se ejecuta si cmd1tiene éxito.

6)

Una forma canónica de ejecutar comandos complejos para cada archivo es llamar a un shell con -exec ... {} +:

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

Esa vez, volvemos a ser eficientes, echoya que estamos usando shuno incorporado y la -exec +versión genera la menor cantidad shposible.

7)

En mis pruebas en un directorio con 200,000 archivos con nombres cortos en ext4, el zsh(párrafo 2) es, con mucho, el más rápido, seguido del primer for i in *bucle simple (aunque, como de costumbre, bashes mucho más lento que otros shells para eso).

Stéphane Chazelas
fuente
¿Qué hace el !comando find?
rubo77
@ rubo77, !es por negación. ! -name . -prune more...hará -prune(y more...como -prunesiempre devuelve verdadero) para cada archivo pero .. Por lo tanto, lo hará more...en todos los archivos ., pero excluirá .y no descenderá a subdirectorios de .. Entonces es el equivalente estándar de GNU -mindepth 1 -maxdepth 1.
Stéphane Chazelas
18

Intenté esto en un directorio con 2259 entradas y utilicé el timecomando.

La salida de time for f in *; do echo "$f"; done(¡menos los archivos!) Es:

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

La salida de time find * -prune | while read f; do echo "$f"; done(¡menos los archivos!) Es:

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

Ejecuté cada comando varias veces, para eliminar errores de caché. Esto sugiere mantenerlo bash(para i in ...) es más rápido que usar findy canalizar la salida (a bash)

Solo por completo, dejé caer la tubería find, ya que en su ejemplo, es totalmente redundante. La salida de just find * -prunees:

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

Además, time echo *(la salida no está separada por una nueva línea, por desgracia):

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

En este punto, sospecho que la razón echo *es más rápida es que no está generando tantas líneas nuevas, por lo que la salida no se desplaza tanto. Probemos ...

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

rendimientos:

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

mientras que time find * -prune > /dev/nullproduce:

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

y time for f in *; do echo "$f"; done > /dev/nullrendimientos:

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

y finalmente: time echo * > /dev/nullrendimientos:

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

Parte de la variación puede explicarse por factores aleatorios, pero parece claro:

  • la salida es lenta
  • la tubería cuesta un poco
  • for f in *; do ...es más lento que find * -prune, por sí solo, pero para las construcciones anteriores que involucran tuberías, es más rápido.

Además, como un aparte, ambos enfoques parecen manejar nombres con espacios muy bien.

EDITAR:

Horarios 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

Entonces, conclusión adicional:

  • find * -prunees más lento que find . -maxdepth 1: en el primero, el shell procesa un globo y luego construye una línea de comando (grande) para find. NB: find . -prunevuelve solo ..

Más pruebas time find . -maxdepth 1 -exec echo {} \; >/dev/null:

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

Conclusión:

  • La forma más lenta de hacerlo hasta ahora. Como se señaló en los comentarios de la respuesta donde se sugirió este enfoque, cada argumento genera una concha.
Phil
fuente
¿Qué tubería es redundante? ¿Puedes mostrar la línea que usaste sin tubería?
rubo77
2
@ rubo77 find * -prune | while read f; do echo "$f"; donetiene la tubería redundante: todo lo que la tubería está haciendo es emitir exactamente lo que findproduce por sí mismo. Sin una tubería, sería simplemente find * -prune La tubería solo es redundante específicamente porque la cosa en el otro lado de la tubería simplemente copia stdin en stdout (en su mayor parte). Es un no-op costoso. Si desea hacer cosas con la salida de find, aparte de escupirlo nuevamente, eso es diferente.
Phil
Tal vez el principal consumo de tiempo es el *. Como BitsOfNix declaró: Todavía sugiero no usar *y .para su findlugar.
rubo77
@ rubo77 parece de esa manera. Supongo que pasé por alto eso. He agregado resultados para mi sistema. Supongo que find . -prunees más rápido porque findleerá una entrada de directorio al pie de la letra, mientras que el shell hará lo mismo, posiblemente coincidiendo con el glob (podría optimizar para *), luego construyendo la línea de comando grande para find.
Phil
1
find . -pruneimprime solo .en mi sistema. Casi no funciona en absoluto. No es lo mismo find * -pruneque muestra todos los nombres en el directorio actual. Un simple read fva a destrozar los nombres de archivos con espacios iniciales.
Ian D. Allen
10

Definitivamente iría con find, aunque cambiaría tu find por solo esto:

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

En cuanto al rendimiento, findes mucho más rápido dependiendo de sus necesidades, por supuesto. Lo que tiene actualmente con forél solo mostrará los archivos / directorios en el directorio actual, pero no el contenido de los directorios. Si usa find, también mostrará el contenido de los subdirectorios.

Digo hallazgo es mejor ya que con su forla *tendrá que ser ampliado primera y tengo miedo de que si usted tiene un directorio con una enorme cantidad de archivos que puede dar el error de lista de argumentos demasiado tiempo . Lo mismo vale parafind *

Como ejemplo, en uno de los sistemas que uso actualmente hay un par de directorios con más de 2 millones de archivos (<100k cada uno):

find *
-bash: /usr/bin/find: Argument list too long
BitsOfNix
fuente
Agregué -prunepara hacer los dos ejemplos más parecidos. y prefiero la tubería con while, por lo que es más fácil aplicar más comandos en el bucle
rubo77
cambiar el límite difícil no es una solución adecuada de mi POV. Especialmente cuando se habla de más de 2 millones de archivos. Sin la digresión de la Pregunta, para casos simples, el directorio de un nivel es más rápido, pero si cambia la estructura de su archivo / directorio será más difícil de migrar. Mientras que con find y su gran cantidad de opciones puedes estar mejor preparado. Aún así, sugiero no usar * y. para encontrar en su lugar. Sería más portátil que * donde es posible que no pueda controlar el límite rígido ...
BitsOfNix
44
Eso generará un proceso de eco por archivo (mientras que en el shell for loop, es el eco incorporado que se utilizará sin bifurcar un proceso adicional), y descenderá a los directorios, por lo que será mucho más lento . También tenga en cuenta que incluirá archivos de puntos.
Stéphane Chazelas
Tienes razón, agregué el maxdepth 1 para que se mantenga solo en el nivel actual.
BitsOfNix
7
find * -prune | while read f; do 
    echo "$f"
done

es un uso inútil de find- Lo que está diciendo es efectivamente "para cada archivo en el directorio ( *), no encuentre ningún archivo. Además, no es seguro por varias razones:

  • Las barras invertidas en las rutas se tratan especialmente sin la -ropción de read. Esto no es un problema con el forbucle.
  • Las nuevas líneas en las rutas romperían cualquier funcionalidad no trivial dentro del bucle. Esto no es un problema con el forbucle.

Manejar cualquier nombre de archivo findes difícil , por lo que debe usar la foropción de bucle siempre que sea posible solo por esa razón. Además, ejecutar un programa externo como finden general será más lento que ejecutar un comando de bucle interno como for.

l0b0
fuente
@ I0b0 ¿Qué pasa con find -path './*' -prune o find -path './[^.font>*' -prune (para evitar archivos y directorios ocultos) como una mejor construcción? En forma completa: find -path ' ./* '-prune -print0 | xargs -0 sh -c '...'?
AsymLabs
1
Ni find' -print0ni xargs' -0son compatibles con POSIX, y no puede poner comandos arbitrarios sh -c ' ... '(las comillas simples no pueden escaparse entre comillas simples), por lo que no es tan simple.
l0b0
4

¡Pero somos tontos para las preguntas de rendimiento! Esta solicitud de experimento hace al menos dos suposiciones que la hacen poco válida.

A. Suponga que encuentran los mismos archivos ...

Bueno, ellos van a encontrar los mismos archivos en un primer momento, porque los dos iteración sobre el mismo pegote, a saber *. Pero find * -prune | while read fadolece de varios defectos que hacen posible que no encuentre todos los archivos que espera:

  1. No se garantiza que POSIX find acepte más de un argumento de ruta. La mayoría de las findimplementaciones lo hacen, pero aún así, no debe confiar en eso.
  2. find *puede romperse cuando golpeas ARG_MAX. for f in *no, porque se ARG_MAXaplica a exec, no incorporados.
  3. while read fpuede romperse con nombres de archivos que comienzan y terminan con espacios en blanco, que se eliminarán. Podría superar esto con while readsu parámetro predeterminado REPLY, pero eso aún no lo ayudará cuando se trata de nombres de archivo con nuevas líneas en ellos.

B. echo. Nadie va a hacer esto solo para hacer eco del nombre del archivo. Si quieres eso, solo haz uno de estos:

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

La tubería al whilebucle aquí crea una subshell implícita que se cierra cuando finaliza el bucle, lo que puede ser poco intuitivo para algunos.

Para responder a la pregunta, aquí están los resultados en un directorio mío que tiene 184 archivos y directorios.

$ 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
fuente
No estoy de acuerdo con la afirmación de que el ciclo while genera una subshell - en el peor de los casos, un nuevo hilo: lo siguiente está tratando de mostrar antes y después, disculpas por el mal formato$ 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
Técnicamente, hablo mal: la tubería causa la subshell implícita, no el bucle while. Lo editaré
kojiro
2

find *no funcionará correctamente si *produce tokens que parecen predicados en lugar de rutas.

No puede usar el --argumento habitual para arreglar esto porque --indica el final de las opciones, y las opciones de find van antes que las rutas.

Para solucionar este problema, puede usarlo find ./*en su lugar. Pero entonces no está produciendo exactamente las mismas cadenas que for x in *.

Tenga en cuenta que en find ./* -prune | while read f ..realidad no utiliza la funcionalidad de escaneo de find. Es la sintaxis global ./*que realmente atraviesa el directorio y genera nombres. Luego, el findprograma tendrá que realizar al menos una statverificación en cada uno de esos nombres. Usted tiene la sobrecarga de iniciar el programa y tener acceso a estos archivos, y luego hacer E / S para leer su salida.

Es difícil imaginar cómo podría ser cualquier cosa menos menos eficiente que for x in ./* ....

Kaz
fuente
1

Bueno, para empezar, fores una palabra clave de shell, integrada en Bash, mientras que findes un ejecutable separado.

$ type -a for
for is a shell keyword

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

El forbucle solo encontrará los archivos del personaje globstar cuando se expanda, no se repetirá en ningún directorio que encuentre.

Find, por otro lado, también recibirá una lista expandida por globstar, pero buscará de forma recursiva todos los archivos y directorios debajo de esta lista expandida y canalizará cada uno hacia el whilebucle.

Ambos enfoques podrían considerarse peligrosos en el sentido de que no manejan rutas o nombres de archivos que contienen espacios.

Eso es todo lo que puedo pensar que vale la pena comentar sobre estos 2 enfoques.

slm
fuente
Agregué -prune al comando find, por lo que son más parecidos.
rubo77
0

Si todos los archivos devueltos por find pueden procesarse con un solo comando (obviamente no es aplicable a su ejemplo de eco anterior), puede usar xargs:

find * |xargs some-command
Robar
fuente
0

Durante años he estado usando esto: -

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

para buscar ciertos archivos (por ejemplo, * .txt) que contienen un patrón que grep puede buscar y canalizarlo más para que no se desplace de la pantalla. A veces uso el >> tubo para escribir los resultados en otro archivo que puedo ver más adelante.

Aquí hay una muestra del 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
fuente