web-dev-qa-db-fra.com

Quelle est la façon la plus efficace d'utiliser les ressources pour compter le nombre de fichiers dans un répertoire?

CentOS 5.9

Je suis tombé sur un problème l'autre jour où un répertoire avait beaucoup de fichiers. Pour le compter, j'ai couru ls -l /foo/foo2/ | wc -l

Il s'avère qu'il y avait plus d'un million de fichiers dans un seul répertoire (longue histoire - la cause première est en train d'être corrigée).

Ma question est: existe-t-il un moyen plus rapide de faire le décompte? Quelle serait la façon la plus efficace d'obtenir le décompte?

57
Mike B

Réponse courte:

\ls -afq | wc -l

(Cela inclut . Et .., Donc soustrayez 2.)


Lorsque vous répertoriez les fichiers dans un répertoire, trois choses courantes peuvent se produire:

  1. Énumération des noms de fichiers dans le répertoire. C'est incontournable: il n'y a aucun moyen de compter les fichiers dans un répertoire sans les énumérer.
  2. Tri des noms de fichiers. Les caractères génériques du shell et la commande ls le font.
  3. Appeler stat pour récupérer des métadonnées sur chaque entrée de répertoire, par exemple s'il s'agit d'un répertoire.

Le n ° 3 est de loin le plus cher, car il nécessite le chargement d'un inode pour chaque fichier. En comparaison, tous les noms de fichiers nécessaires pour # 1 sont stockés de manière compacte dans quelques blocs. # 2 gaspille un peu de temps CPU, mais ce n'est souvent pas un facteur décisif.

S'il n'y a pas de nouvelle ligne dans les noms de fichiers, un simple ls -A | wc -l Vous indique le nombre de fichiers dans le répertoire. Attention, si vous avez un alias pour ls, cela peut déclencher un appel à stat (par exemple, ls --color Ou ls -F Doivent connaître le type de fichier, qui nécessite un appel à stat), donc à partir de la ligne de commande, appelez command ls -A | wc -l ou \ls -A | wc -l pour éviter un alias.

S'il y a des retours à la ligne dans le nom de fichier, le fait que les retours à la ligne soient répertoriés ou non dépend de la variante Unix. GNU coreutils et BusyBox affichent par défaut ? Pour une nouvelle ligne, donc ils sont sûrs.

Appelez ls -f Pour lister les entrées sans les trier (# 2). Cela active automatiquement -a (Au moins sur les systèmes modernes). L'option -f Est en POSIX mais avec un statut optionnel; la plupart des implémentations le prennent en charge, mais pas BusyBox. L'option -q Remplace les caractères non imprimables, y compris les retours à la ligne par ?; il s'agit de POSIX mais n'est pas pris en charge par BusyBox, alors omettez-le si vous avez besoin de la prise en charge de BusyBox au détriment du sur-comptage des fichiers dont le nom contient un caractère de nouvelle ligne.

Si le répertoire n'a pas de sous-répertoires, la plupart des versions de find n'appelleront pas stat sur ses entrées (optimisation du répertoire feuille: un répertoire qui a un nombre de liens de 2 ne peut pas avoir de sous-répertoires, donc find n'a pas besoin de rechercher les métadonnées des entrées sauf si une condition telle que -type l'exige). find . | wc -l Est donc un moyen portable et rapide de compter les fichiers dans un répertoire à condition que le répertoire n’ait pas de sous-répertoires et qu’aucun nom de fichier ne contienne de nouvelle ligne.

Si le répertoire n'a pas de sous-répertoires mais que les noms de fichiers peuvent contenir des sauts de ligne, essayez-en un (le second devrait être plus rapide s'il est pris en charge, mais peut-être pas sensiblement).

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

En revanche, n'utilisez pas find si le répertoire a des sous-répertoires: même find . -maxdepth 1 Appelle stat à chaque entrée (au moins avec GNU find et BusyBox). Vous évitez le tri (# 2) mais vous payez le prix d'une recherche d'inode (# 3) qui tue les performances.

Dans le shell sans outils externes, vous pouvez exécuter compter les fichiers dans le répertoire en cours avec set -- *; echo $#. Cela manque les fichiers de points (fichiers dont le nom commence par .) Et signale 1 au lieu de 0 dans un répertoire vide. C'est le moyen le plus rapide de compter les fichiers dans les petits répertoires car il ne nécessite pas de démarrer un programme externe, mais (sauf dans zsh) fait perdre du temps aux répertoires plus grands en raison de l'étape de tri (# 2).

  • En bash, c'est un moyen fiable de compter les fichiers du répertoire courant:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
  • Dans ksh93, c'est un moyen fiable de compter les fichiers dans le répertoire courant:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
  • Dans zsh, c'est un moyen fiable de compter les fichiers dans le répertoire courant:

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

    Si l'option mark_dirs Est définie, assurez-vous de la désactiver: a=(*(DNoN^M)).

  • Dans n'importe quel shell POSIX, c'est un moyen fiable de compter les fichiers dans le répertoire courant:

    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"
    

Toutes ces méthodes trient les noms de fichiers, à l'exception de celui de zsh.

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

Est considérablement plus rapide sur ma machine mais le répertoire local . Est ajouté au décompte.

17
Joel Taylor

ls -1U avant que le canal ne dépense un peu moins de ressources, car il n'essaie pas de trier les entrées du fichier, il les lit simplement lorsqu'elles sont triées dans le dossier sur le disque. Il produit également moins de sortie, ce qui signifie un peu moins de travail pour wc.

Vous pouvez également utiliser ls -f qui est plus ou moins un raccourci pour ls -1aU.

Je ne sais pas s'il existe un moyen efficace de le faire via une commande sans tuyauterie.

8
Luis Machuca

Un autre point de comparaison. Bien qu'il ne soit pas un oneliner Shell, ce programme C ne fait rien de superflu. Notez que les fichiers cachés sont ignorés pour correspondre à la sortie de ls|wc -l (ls -l|wc -l est désactivé de un en raison du nombre total de blocs dans la première ligne de sortie).

#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);
}
6
Thomas Nyman

Vous pouvez essayer Perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Il serait intéressant de comparer les timings avec votre pipe Shell.

3
doneal24

De cette réponse , je peux penser à celle-ci comme une solution possible.

/*
 * 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);
}

Copiez le programme C ci-dessus dans le répertoire dans lequel les fichiers doivent être répertoriés. Exécutez ensuite ces commandes:

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

Une solution bash uniquement, ne nécessitant aucun programme externe, mais je ne sais pas combien efficace:

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

os.listdir () in python peut faire le travail pour vous. Il donne un tableau du contenu du répertoire, à l'exclusion des fichiers spéciaux '.' et '..'. De plus, aucun besoin de s'inquiéter des fichiers abt avec des caractères spéciaux comme "\ n" dans le nom.

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

voici le temps pris par la commande python ci-dessus par rapport à la commande 'ls -Af').

 ~/test $ time ls -Af | wc -l 
 399144 
 
 réel 0m0.300s 
 utilisateur 0m0.104s 
 sys 0m0.240s 
 ~/test $ time python -c 'import os; print len ​​(os.listdir ("."))' 
 399142 
 
 réel 0m0.249s 
 utilisateur 0m0.064s 
 sys 0m0.180s 
1
indrajeet

La méthode la plupart efficace en termes de ressources n'impliquerait probablement aucune invocation de processus externe. Je parierais donc ...

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

Pour exclure les sous-répertoires du décompte, voici une variante de la réponse acceptée de Gilles:

echo $(( $( \ls -afq target | wc -l ) - $( \ls -od target | cut -f2 -d' ') ))

L'expansion arithmétique externe $(( )) soustrait la sortie du deuxième sous-shell $( ) de la première $( ). La première $( ) est exactement celle de Gilles d'en haut. La deuxième $( ) affiche le nombre de répertoires "reliant" à la cible. Cela vient de ls -od (Remplacez ls -ld Si vous le souhaitez), où la colonne qui répertorie le nombre de liens durs a cela comme une signification spéciale pour les répertoires. Le nombre de "liens" comprend ., .. Et tous les sous-répertoires.

Je n'ai pas testé les performances, mais cela semble similaire. Il ajoute une statistique du répertoire cible et une surcharge pour le sous-shell et le canal ajoutés.

0
user361782

Après avoir corrigé le problème à partir de la réponse de @Joel, où il a ajouté . en tant que fichier:

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

tail supprime simplement la première ligne, ce qui signifie que . n'est plus compté.

0
haneefmubarak

ls -1 | wc -l me vient immédiatement à l'esprit. Qu'il s'agisse ls -1U est plus rapide que ls -1 est purement académique - la différence devrait être négligeable mais pour les très gros répertoires.

0
countermode

Je sais que c'est vieux mais je pense que awkhas à mentionner ici. Les suggestions qui incluent l'utilisation de wc ne sont tout simplement pas correctes en ce qui concerne la question d'OP: "la manière la plus efficace en termes de ressources". J'ai récemment eu un fichier journal hors de contrôle (en raison d'un mauvais logiciel) et suis donc tombé sur ce post. Il y avait environ 232 millions d'entrées! J'ai d'abord essayé wc -l et a attendu 15 minutes - il n'a même pas pu finir de compter les lignes. L'instruction awk suivante m'a donné un nombre de lignes précis en 3 minutes sur ce fichier journal. J'ai appris au fil des ans à ne jamais sous-estimer la capacité d'AWK à simuler des programmes Shell standard d'une manière beaucoup plus efficace.

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

Et si vous devez remplacer une commande comme ls pour compter les fichiers dans un répertoire:

`#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/)
0
user.friendly

Réponse un peu tardive (après 6 ans), mais ...

La manière la plus rapide est simplement de faire ls -lsur le répertoire parent, et vérifiez la colonne du nombre de liens pour le sous-répertoire donné.

Démo: disons, je veux compter le nombre de fichiers/répertoires dans mon /usr/lib répertoire.

Donc, en entrant ls -l /usr produit:

total 0
drwxr-xr-x  978 root  wheel  31296 29 apr  2019 bin
drwxr-xr-x  267 root  wheel   8544 30 okt  2018 include
drwxr-xr-x  312 root  wheel   9984 23 jan  2019 lib
drwxr-xr-x  240 root  wheel   7680 29 apr  2019 libexec
drwxr-xr-x   17 root  wheel    544 14 nov  2018 local
drwxr-xr-x  248 root  wheel   7936 23 jan  2019 sbin
drwxr-xr-x   47 root  wheel   1504  4 okt  2018 share
drwxr-xr-x    5 root  wheel    160 25 okt  2017 standalone

Le nombre juste après les autorisations est le link count du fichier. Pour un répertoire, c'est juste le nombre d'entrées qu'il contient. Ainsi, dans l'exemple ci-dessus, le /usr/lib a 12 entrées.

Vérifions:

$ ls -1a /usr/lib | wc -l
     312

Sans afficher les autres répertoires dans le parent, utilisez simplement -d par exemple.

$ ls -ld /usr/lib
drwxr-xr-x  312 root  wheel  9984 23 jan  2019 /usr/lib
#           ^^^ - the number of entries in the /usr/lib (including . and ..)
0
jm666