¿Cuál es la forma más eficiente de resources para contar cuántos files hay en un directory?

CentOS 5.9

El otro día me encontré con un problema donde un directory tenía muchos files. Para contarlo, ejecuté ls -l /foo/foo2/ | wc -l ls -l /foo/foo2/ | wc -l

Resulta que había más de 1 millón de files en un único directory (larga historia: la causa raíz está siendo reparada).

Mi pregunta es: ¿hay una manera más rápida de hacer el recuento? ¿Cuál sería la forma más eficiente de get el conteo?

Respuesta corta:

 \ls -afq | wc -l 

(Esto incluye . Y .. , así que reste 2.)


Cuando enumera los files en un directory, pueden ocurrir tres cosas comunes:

  1. Enumerando los nombres de file en el directory. Esto es ineludible: no hay forma de contar los files en un directory sin enumerarlos.
  2. Ordenando los nombres de file. Los comodines de Shell y el command ls hacen eso.
  3. Llamando a stat para recuperar metadatos sobre cada input de directory, como si es un directory.

El # 3 es el más caro con diferencia, ya que requiere cargar un ínodo para cada file. En comparación, todos los nombres de file necesarios para el # 1 se almacenan de forma compacta en unos pocos bloques. # 2 desperdicia algo de time de CPU, pero a menudo no es un factor decisivo.

Si no hay nuevas líneas en los nombres de files, un simple ls -A | wc -l ls -A | wc -l te dice cuántos files hay en el directory. Tenga en count que si tiene un alias para ls , esto puede desencadenar una llamada a stat (por ejemplo ls --color o ls -F necesita saber el tipo de file, que requiere una llamada a stat ), así que desde la línea de command, llame al command ls -A | wc -l command ls -A | wc -l o \ls -A | wc -l \ls -A | wc -l para evitar un alias.

Si hay nuevas líneas en el nombre del file, si las líneas nuevas están enumeradas o no, depende de la variante de Unix. GNU coreutils y BusyBox pnetworkingeterminados para mostrar ? para una nueva línea, entonces están a salvo.

Llame a ls -f para listr las inputs sin orderarlas (n. ° 2). Esto se enciende automáticamente -a (al less en los sistemas modernos). La opción -f está en POSIX pero con estado opcional; la mayoría de las implementaciones lo admiten, pero no BusyBox. La opción -q reemplaza los caracteres no imprimibles, incluidas las líneas nuevas, por ? ; es POSIX pero no es compatible con BusyBox, por lo tanto, omítalo si necesita soporte de BusyBox a expensas de contar de más los files cuyo nombre contenga un carácter de nueva línea.

Si el directory no tiene subdirectorys, la mayoría de las versiones de find no invocarán stat en sus inputs (optimization del directory de hoja: un directory que tiene un conteo de enlaces de 2 no puede tener subdirectorys, por lo que find no necesita search los metadatos del inputs a less que una condición como -type requiera). Entonces find . | wc -l find . | wc -l find . | wc -l es una forma portátil y rápida de contar files en un directory siempre que el directory no tenga subdirectorys y que ningún nombre de file contenga una nueva línea.

Si el directory no tiene subdirectorys pero los nombres de los files pueden contener líneas nuevas, pruebe con uno de estos (el segundo debería ser más rápido si es compatible, pero puede que no lo sea).

 find -print0 | tr -dc \\0 | wc -c find -printf a | wc -c 

Por otro lado, no use find si el directory tiene subdirectorys: incluso find . -maxdepth 1 find . -maxdepth 1 llama a stat en cada input (al less con GNU find y BusyBox find). Evita clasificar (# 2) pero paga el precio de una búsqueda de inode (# 3) que mata el performance.

En el shell sin herramientas externas, puede ejecutar contar los files en el directory actual con set -- *; echo $# set -- *; echo $# . Esto falla en los files de puntos (files cuyo nombre comienza con . ) E informa 1 en lugar de 0 en un directory vacío. Esta es la forma más rápida de contar files en directorys pequeños porque no requiere iniciar un progtwig externo, pero (excepto en zsh) desperdicia time para directorys más grandes debido al paso de sorting (# 2).

  • En bash, esta es una manera confiable de contar los files en el directory actual:

     shopt -s dotglob nullglob a=(*) echo ${#a[@]} 
  • En ksh93, esta es una manera confiable de contar los files en el directory actual:

     FIGNORE='(.|..)' a=(~(N)*) echo ${#a[@]} 
  • En zsh, esta es una manera confiable de contar los files en el directory actual:

     a=(*(DNoN)) echo $#a 

    Si tiene la opción mark_dirs establecida, asegúrese de desactivarla: a=(*(DNoN^M)) .

  • En cualquier shell POSIX, esta es una manera confiable de contar los files en el directory actual:

     total=0 set -- * if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi set -- .[!.]* if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi set -- ..?* if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi echo "$total" 

Todos estos methods orderan los nombres de los files, excepto el de zsh.

 find /foo/foo2/ -maxdepth 1 | wc -l 

Es considerablemente más rápido en mi máquina que en la local . directory se agrega al conteo.

ls -1U antes de que el conducto gaste solo un poco less de resources, ya que no intenta orderar las inputs del file, simplemente las lee, ya que están orderadas en la carpeta del disco. También produce less producción, lo que significa un poco less de trabajo para wc .

También podría usar ls -f que es más o less un atajo para ls -1aU .

No obstante, no sé si hay una manera eficiente de resources para hacerlo a través de un command sin tuberías.

Otro punto de comparación. Aunque no es un "oneliner" de shell, este progtwig C no hace nada superfluo. Tenga en count que los files ocultos se ignoran para que coincidan con la salida de ls|wc -l ( ls -l|wc -l está desactivado en uno debido al total de bloques en la primera línea de salida).

 #include <stdio.h> #include <stdlib.h> #include <dirent.h> #include <error.h> #include <errno.h> int main(int argc, char *argv[]) { int file_count = 0; DIR * dirp; struct dirent * entry; if (argc < 2) error(EXIT_FAILURE, 0, "missing argument"); if(!(dirp = opendir(argv[1]))) error(EXIT_FAILURE, errno, "could not open '%s'", argv[1]); while ((entry = readdir(dirp)) != NULL) { if (entry->d_name[0] == '.') { /* ignore hidden files */ continue; } file_count++; } closedir(dirp); printf("%d\n", file_count); } 

Podría intentar perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Sería interesante comparar los times con tu tubería de concha.

Una solución solo de bash, que no requiere ningún progtwig externo, pero no se sabe cuánto eficiente:

 list=(*) echo "${#list[@]}" 

A partir de esta respuesta, puedo pensar en esta como una posible solución.

 /* * List directories using getdents() because ls, find and Python libraries * use readdir() which is slower (but uses getdents() underneath. * * Compile with * ]$ gcc getdents.c -o getdents */ #define _GNU_SOURCE #include <dirent.h> /* Defines DT_* constants */ #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/syscall.h> #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) struct linux_dirent { long d_ino; off_t d_off; unsigned short d_reclen; char d_name[]; }; #define BUF_SIZE 1024*1024*5 int main(int argc, char *argv[]) { int fd, nread; char buf[BUF_SIZE]; struct linux_dirent *d; int bpos; char d_type; fd = open(argc > 1 ? argv[1] : ".", O_RDONLY | O_DIRECTORY); if (fd == -1) handle_error("open"); for ( ; ; ) { nread = syscall(SYS_getdents, fd, buf, BUF_SIZE); if (nread == -1) handle_error("getdents"); if (nread == 0) break; for (bpos = 0; bpos < nread;) { d = (struct linux_dirent *) (buf + bpos); d_type = *(buf + bpos + d->d_reclen - 1); if( d->d_ino != 0 && d_type == DT_REG ) { printf("%s\n", (char *)d->d_name ); } bpos += d->d_reclen; } } exit(EXIT_SUCCESS); } 

Copie el progtwig C arriba en el directory en el que los files deben aparecer. Luego ejecuta los commands a continuación.

 gcc getdents.c -o getdents ./getdents | wc -l 

Probablemente, la forma más eficiente de resources no implique invocaciones de processs externos. Así que apostaría a …

 cglb() ( c=0 ; set -- tglb() { [ -e "$2" ] || [ -L "$2" ] && c=$(($c+$#-1)) } for glb in '.?*' \* do tglb $1 ${glb##.*} ${glb#\*} set -- .. done echo $c ) 

Después de solucionar el problema de la respuesta de @Joel, donde se agregó . como un file:

find /foo/foo2 -maxdepth 1 | tail -n +2 | wc -l

tail simplemente elimina la primera línea, lo que significa que . no se count más

os.listdir () en python puede hacer el trabajo por usted. Da una matriz de los contenidos del directory, excluyendo el especial '.' y '…' files. Además, no hay necesidad de preocuparse de los files abt con caracteres especiales como '\ n' en el nombre.

 python -c 'import os;print len(os.listdir("."))' 

siguiente es el time tomado por el command python anterior en comparación con el command 'ls -Af'.

 ~ / test $ time ls -Af | wc -l
 399144

 0m0.300s reales
 usuario 0m0.104s
 sys 0m0.240s
 ~ / test $ time python -c 'import os; print len ​​(os.listdir ("."))'
 399142

 0m0.249s reales
 usuario 0m0.064s
 sys 0m0.180s

ls -1 | wc -l ls -1 | wc -l viene a mi mente inmediatamente. Si ls -1U es más rápido que ls -1 es puramente académico, la diferencia debería ser insignificante, pero para directorys muy grandes.

Sé que esto es viejo, pero siento que hay que mencionar awk aquí. Las sugerencias que incluyen el uso de wc simplemente no son correctas en lo que respecta a la pregunta de OP: "la forma más eficiente de resources". Recientemente tuve un file de logging fuera de control (debido a un mal software) y por lo tanto tropecé con esta publicación. ¡Hubo aproximadamente 232 millones de inputs! Primero probé wc -l y esperé 15 minutos, ni siquiera pude terminar de contar las líneas. La siguiente statement awk me dio un recuento preciso de líneas en 3 minutos en ese file de logging. A lo largo de los años, he aprendido a nunca subestimar la capacidad de awk de simular progtwigs de shell estándar de una manera mucho más eficiente. Espero que ayude a alguien como yo. ¡Feliz hacking!

 awk 'BEGIN{i=0} {i++} END{print i}' /foo/foo2 

Y si necesita sustituir un command como ls para contar files en un directory:

 `#Normal:` awk 'BEGIN{i=0} {i++} END{print i}' <(ls /foo/foo2/) `#Hidden:` awk 'BEGIN{i=0} {i++} END{print (i-2)}' <(ls -f /foo/foo2/) 

Yo pensaría que echo * sería más eficiente que cualquier command 'ls':

 echo * | wc -w