Al recorrer los archivos hay dos formas:
usa un
forbucle:for f in *; do echo "$f" doneuso
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?
bash
shell-script
performance
rubo77
fuente
fuente

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 .read fdestrozará los nombres de los archivos a medida que los lee (por ejemplo, nombres con espacios en blanco iniciales). Tambiénfind * -pruneparece ser una forma muy complicada de decir simplementels -1sí.find ., nofind *.ls -les una mala idea. Pero analizarls -1(eso1no es unl) no es peor que analizarfind * -prune. Ambos fallan en archivos con nuevas líneas en los nombres.Respuestas:
1)
El primero:
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:
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 suprintf '%s\n'lugar lo queprintfestá 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. Enzsh:o simplemente:
bashno tiene equivalente por lo que puedo decir, por lo que tendrías que recurrirfind.3)
(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 elforbucle a menos que la lista de archivos sea enorme.4)
Además, a diferencia de la expansión de comodín de shell,
findhará unalstatllamada 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 guardandolstat: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 quefindhaya terminado de encontrar todos los archivos. En los sistemas GNU y FreeBSD, puede usarstdbufpara 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: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 deechowhilefind, tendrá que generar un nuevo proceso y ejecutarlo/bin/echopara cada archivo.Si necesita ejecutar varios comandos, puede hacer:
Pero ten cuidado porque
cmd2solo se ejecuta sicmd1tiene éxito.6)
Una forma canónica de ejecutar comandos complejos para cada archivo es llamar a un shell con
-exec ... {} +:Esa vez, volvemos a ser eficientes,
echoya que estamos usandoshuno incorporado y la-exec +versión genera la menor cantidadshposible.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 primerfor i in *bucle simple (aunque, como de costumbre,bashes mucho más lento que otros shells para eso).fuente
!comando find?!es por negación.! -name . -prune more...hará-prune(ymore...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.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:La salida de
time find * -prune | while read f; do echo "$f"; done(¡menos los archivos!) Es:Ejecuté cada comando varias veces, para eliminar errores de caché. Esto sugiere mantenerlo
bash(para i in ...) es más rápido que usarfindy canalizar la salida (abash)Solo por completo, dejé caer la tubería
find, ya que en su ejemplo, es totalmente redundante. La salida de justfind * -prunees:Además,
time echo *(la salida no está separada por una nueva línea, por desgracia):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 ...rendimientos:
mientras que
time find * -prune > /dev/nullproduce:y
time for f in *; do echo "$f"; done > /dev/nullrendimientos:y finalmente:
time echo * > /dev/nullrendimientos:Parte de la variación puede explicarse por factores aleatorios, pero parece claro:
for f in *; do ...es más lento quefind * -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:find * -prune > /dev/null:Entonces, conclusión adicional:
find * -prunees más lento quefind . -maxdepth 1: en el primero, el shell procesa un globo y luego construye una línea de comando (grande) parafind. NB:find . -prunevuelve solo..Más pruebas
time find . -maxdepth 1 -exec echo {} \; >/dev/null:Conclusión:
fuente
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 quefindproduce por sí mismo. Sin una tubería, sería simplementefind * -pruneLa 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.*. Como BitsOfNix declaró: Todavía sugiero no usar*y.para sufindlugar.find . -prunees más rápido porquefindleerá 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 parafind.find . -pruneimprime solo.en mi sistema. Casi no funciona en absoluto. No es lo mismofind * -pruneque muestra todos los nombres en el directorio actual. Un simpleread fva a destrozar los nombres de archivos con espacios iniciales.Definitivamente iría con find, aunque cambiaría tu find por solo esto:
En cuanto al rendimiento,
findes mucho más rápido dependiendo de sus necesidades, por supuesto. Lo que tiene actualmente conforé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):
fuente
-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 buclees 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:-ropción deread. Esto no es un problema con elforbucle.forbucle.Manejar cualquier nombre de archivo
findes difícil , por lo que debe usar laforopción de bucle siempre que sea posible solo por esa razón. Además, ejecutar un programa externo comofinden general será más lento que ejecutar un comando de bucle interno comofor.fuente
find'-print0nixargs'-0son compatibles con POSIX, y no puede poner comandos arbitrariossh -c ' ... '(las comillas simples no pueden escaparse entre comillas simples), por lo que no es tan simple.¡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
*. Perofind * -prune | while read fadolece de varios defectos que hacen posible que no encuentre todos los archivos que espera:findimplementaciones lo hacen, pero aún así, no debe confiar en eso.find *puede romperse cuando golpeasARG_MAX.for f in *no, porque seARG_MAXaplica aexec, no incorporados.while read fpuede romperse con nombres de archivos que comienzan y terminan con espacios en blanco, que se eliminarán. Podría superar esto conwhile readsu parámetro predeterminadoREPLY, 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: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.
fuente
$ 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 bashfind *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 quefor x in *.Tenga en cuenta que en
find ./* -prune | while read f ..realidad no utiliza la funcionalidad de escaneo defind. Es la sintaxis global./*que realmente atraviesa el directorio y genera nombres. Luego, elfindprograma tendrá que realizar al menos unastatverificació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 ./* ....fuente
Bueno, para empezar,
fores una palabra clave de shell, integrada en Bash, mientras quefindes un ejecutable separado.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.
fuente
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:
fuente
Durante años he estado usando esto: -
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: -
fuente