Quel est le moyen le plus efficace de compter le nombre de fichiers dans un répertoire?

55

CentOS 5.9

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

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

Ma question est la suivante: existe-t-il un moyen plus rapide de compter? Quel serait le moyen le plus efficace d’obtenir le décompte?

Mike B
la source
5
ls -l|wc -lserait un par un en raison du nombre total de blocs dans la première ligne de ls -lsortie
Thomas Nyman
3
@ThomasNyman Il serait en fait désactivé par plusieurs à cause des pseudo-entrées point et pointdot, mais celles-ci peuvent être évitées en utilisant le -Adrapeau. -lest également problématique en raison de la lecture des métadonnées du fichier afin de générer le format de liste étendue. Forcer NOT -len utilisant \lsest une bien meilleure option (cela -1est supposé lors de la sortie de la tuyauterie.) Voir la réponse de Gilles pour la meilleure solution ici.
Caleb
2
@Caleb ls -lne génère aucun fichier caché ni les entrées .et ... ls -ala sortie inclut les fichiers cachés, y compris . et ..tandis que la ls -Asortie inclut les fichiers cachés, à l' exclusion de . et ... Dans la réponse de Gilles, l' dotglob option shell bash fait en sorte que l'extension inclue les fichiers cachés, à l' exclusion de . et ...
Thomas Nyman

Réponses:

61

Réponse courte:

\ls -afq | wc -l

(Ceci inclut .et .., donc soustrayez 2.)


Lorsque vous répertoriez les fichiers dans un répertoire, trois problèmes courants peuvent survenir:

  1. Enumérer les noms de fichiers dans le répertoire. C'est inévitable: il n'y a aucun moyen de compter les fichiers d'un répertoire sans les énumérer.
  2. Tri des noms de fichiers. Les caractères génériques de shell et la lscommande le font.
  3. Appel statpour récupérer des métadonnées sur chaque entrée de répertoire, par exemple s'il s'agit d'un répertoire.

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 nouvelles lignes dans les noms de fichiers, un simple ls -A | wc -lindique le nombre de fichiers contenus dans le répertoire. Attention, si vous avez un alias pour ls, cela peut déclencher un appel à stat(par exemple, ls --colorou si vous avez ls -Fbesoin de connaître le type de fichier, ce qui nécessite un appel à stat), c'est-à-dire à partir de la ligne de commande, appelez command ls -A | wc -lou \ls -A | wc -lévitez un alias.

S'il y a des sauts de lignes dans le nom du fichier, le fait de savoir si les sauts de lignes sont répertoriés dépend de la variante Unix. GNU coreutils et BusyBox s’affichent ?par défaut pour une nouvelle ligne, donc ils sont sûrs.

Appelez ls -fpour lister les entrées sans les trier (# 2). Cela s'allume automatiquement -a(au moins sur les systèmes modernes). L' -foption est dans POSIX mais avec un statut optionnel; la plupart des implémentations le supportent, mais pas BusyBox. L'option -qremplace 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 nombre excessif de fichiers dont le nom contient un caractère de nouvelle ligne.

Si le répertoire ne comporte pas de sous-répertoires, la plupart des versions de findne feront pas appel statà ses entrées (optimisation du répertoire feuille: un répertoire avec un nombre de liens égal à 2 ne peut pas avoir de sous-répertoires, il findn'est donc pas nécessaire de rechercher les métadonnées des entrées sauf condition telle que l' -typeexige). Il find . | wc -lexiste donc un moyen rapide et portable de compter les fichiers dans un répertoire, à condition que celui-ci ne comporte aucun sous-répertoire et qu'aucun nom de fichier ne contienne de nouvelle ligne.

Si le répertoire ne contient pas de sous-répertoires mais que les noms de fichiers peuvent contenir des nouvelles lignes, essayez l'une d'entre elles (la deuxième devrait être plus rapide si elle est prise en charge, mais peut ne pas l'être de manière notable).

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

D'autre part, n'utilisez pas findsi le répertoire a des sous-répertoires: même des find . -maxdepth 1appels statà chaque entrée (au moins avec GNU find et BusyBox find). Vous évitez de trier (# 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 du répertoire en cours avec set -- *; echo $#. Cela manque des fichiers point (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 n’a pas besoin de démarrer un programme externe, mais (sauf dans zsh) une perte de temps pour les plus grands répertoires en raison de l’étape de tri (# 2).

  • En bash, c’est un moyen fiable de compter les fichiers du répertoire en cours:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
  • Dans ksh93, il s'agit d'un moyen fiable de compter les fichiers du répertoire en cours:

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

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

    Si vous avez l' mark_dirsensemble des options, assurez - vous de le désactiver: a=(*(DNoN^M)).

  • Dans n’importe quel shell POSIX, c’est un moyen fiable de compter les fichiers du répertoire en cours:

    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.

Gilles, arrête de faire le mal
la source
1
Mes tests empiriques sur plus d'un million de fichiers montrent que cela find -maxdepth 1suit facilement \ls -Utant que vous n'ajoutez rien qui ressemble à une -typedéclaration qui doit faire des vérifications supplémentaires. Êtes-vous sûr que GNU trouve réellement des appels stat? Même le ralentissement find -typen'est rien comparé à combien de ls -lmarges si vous faites retourner les détails du fichier. D'autre part, le vainqueur de la vitesse claire zshutilise le glob sans tri. (les globs triés sont 2x plus lents que ceux qui lsne trient pas sont 2x plus rapides). Je me demande si les types de système de fichiers auraient un effet significatif sur ces résultats.
Caleb
@Caleb j'ai couru strace. Ceci n'est vrai que si le répertoire a des sous-répertoires: sinon findl'optimisation des répertoires feuille entre en jeu (même sans -maxdepth 1), j'aurais dû le mentionner. Beaucoup de choses peuvent affecter le résultat, y compris le type de système de fichiers (l'appel statcoûte beaucoup plus cher sur les systèmes de fichiers qui représentent des répertoires sous forme de listes linéaires que sur les systèmes de fichiers qui représentent des répertoires sous forme d'arbres), si les inodes ont tous été créés ensemble et sont donc proches. sur le disque, cache froide ou chaude, etc.
Gilles 'SO- arrête de faire le mal'
1
Historiquement, c’est ls -fle moyen le plus fiable d’empêcher les appels stat; c’est souvent simplement décrit aujourd’hui comme "la sortie n’est pas triée" (ce qu’elle provoque également), et inclut .et ... -Aet -Une sont pas des options standard.
Random832
1
Si vous souhaitez spécifiquement compter les fichiers avec une extension commune (ou une autre chaîne), son insertion dans la commande élimine les 2 supplémentaires. Voici un exemple:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell
FYI, avec ksh93 version sh (AT&T Research) 93u+ 2012-08-01sur mon système basé sur Debian, FIGNOREne semble pas fonctionner. Les entrées .et ..sont incluses dans le tableau résultant
Sergiy Kolodyazhnyy
17
find /foo/foo2/ -maxdepth 1 | wc -l

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

Joel Taylor
la source
1
Merci. Je suis cependant obligé de poser une question idiote: pourquoi est-ce plus rapide? Parce que ce n'est pas la peine de chercher les attributs de fichier?
Mike B
2
Oui, c'est ce que j'ai compris. Tant que vous n'utilisez pas le -typeparamètre finddoit être plus rapide quels
Joel Taylor
1
Hmmm .... si je comprends bien la documentation de find , cela devrait en réalité être meilleur que ma réponse. Toute personne ayant plus d'expérience peut vérifier?
Luis Machuca
Ajoutez un -mindepth 1pour omettre le répertoire lui-même.
Stéphane Chazelas le
8

ls -1Uavant que le canal ne dépense un peu moins de ressources, car il ne tente pas de trier les entrées de fichiers, il les lit simplement au fur et à mesure de leur tri dans le dossier du disque. Il produit également moins de sortie, ce qui signifie légèrement moins de travail pour wc.

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

Je ne sais pas s'il existe un moyen économe en ressources de le faire via une commande sans canalisation.

Luis Machuca
la source
8
Btw, -1 est impliqué lorsque la sortie passe dans un tuyau
enzotib
@enzotib - c'est? Wow ... on apprend quelque chose de nouveau chaque jour!
Luis Machuca
6

Un autre point de comparaison. Bien que n'étant pas un shell de base, ce programme C ne fait rien de trop. Notez que les fichiers cachés sont ignorés pour correspondre à la sortie de ls|wc -l( ls -l|wc -lest désactivé par 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);
}
Thomas Nyman
la source
L'utilisation de l' readdir()API stdio ajoute une surcharge et ne vous permet pas de contrôler la taille de la mémoire tampon transmise à l'appel système sous-jacent ( getdentssous Linux)
Stéphane Chazelas le
3

Tu pourrais 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.

doneal24
la source
Sur mes tests, ce qui maintient à peu près exactement le même rythme que les trois autres plus rapides solutions ( find -maxdepth 1 | wc -l, \ls -AU | wc -let le zshnombre en fonction glob et tableau non tri). En d’autres termes, il défie les options avec diverses inefficacités telles que le tri ou la lecture de propriétés de fichiers superflues. Je me risquerais à dire que cela ne vous rapporte rien non plus, cela ne vaut pas la peine d'utiliser une solution plus simple à moins que vous ne soyez déjà en Perl :)
Caleb
Notez que cela inclura les entrées de répertoire .et ..dans le compte. Vous devez donc soustraire deux pour obtenir le nombre réel de fichiers (et de sous-répertoires). En Perl moderne, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'le ferais.
Ilmari Karonen
2

À partir 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 listés. Puis exécutez ces commandes:

gcc getdents.c -o getdents
./getdents | wc -l
Ramesh
la source
1
Quelques choses: 1) si vous êtes prêt à utiliser un programme personnalisé pour cela, vous pouvez aussi bien compter les fichiers et imprimer le décompte; 2) à comparer avec ls -f, ne pas filtrer du d_typetout, juste sur d->d_ino != 0; 3) soustrayez 2 pour .et ...
Matei David
Voir la réponse liée pour un exemple de minutage où il est 40 fois plus rapide que celui accepté ls -f.
Matei David
1

Une solution réservée à Bash, ne nécessitant aucun programme externe, mais ne sachant pas combien elle est efficace:

list=(*)
echo "${#list[@]}"
enzotib
la source
L’expansion globale n’est pas nécessaire, c’est le moyen le plus économe en ressources. Outre le fait que la plupart des réservoirs ont une limite maximale au nombre d’articles qu’ils vont même traiter, ce qui risque de provoquer une bombe lorsqu’il s’agit d’un million d’articles et plus, cela trie également la sortie. Les solutions impliquant find ou ls sans options de tri seront plus rapides.
Caleb
@Caleb, seules les anciennes versions de ksh avaient de telles limites (et ne supportaient pas cette syntaxe) autant que je sache. Dans la plupart des autres shells, la limite est simplement la mémoire disponible. Vous avez un point sur le fait que ça va être très inefficace, surtout en bash.
Stéphane Chazelas le
1

Le moyen le plus efficace en termes de ressources ne ferait probablement pas appel à des processus externes. Donc, je parie sur ...

cglb() ( c=0 ; set --
    tglb() { [ -e "$2" ] || [ -L "$2" ] &&
       c=$(($c+$#-1))
    }
    for glb in '.?*' \*
    do  tglb $1 ${glb##.*} ${glb#\*}
        set -- ..
    done
    echo $c
)
Mikeserv
la source
1
Vous avez des nombres relatifs? pour combien de fichiers?
smci
0

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

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

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

haneefmubarak
la source
1
L'ajout d'une paire de canaux afin d'omettre une ligne d' wcentrée n'est pas très efficace car le temps système augmente de manière linéaire en fonction de la taille de l'entrée. Dans ce cas, pourquoi ne pas simplement décrémenter le décompte final pour le compenser d'une unité, ce qui correspond à une opération à temps constant:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman le
1
Plutôt que d’alimenter autant de données par un autre processus, il serait probablement préférable de faire quelques calculs sur le résultat final. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb
0

os.listdir () en python peut faire le travail pour vous. Il donne un tableau du contenu du répertoire, en excluant le spécial '.' et '..' fichiers. En outre, inutile de s’inquiéter des fichiers contenant des caractères spéciaux tels que "\ n" dans le nom.

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

Ce qui suit est 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

real 0m0.249s
utilisateur 0m0.064s
sys 0m0.180s
indrajeet
la source
0

ls -1 | wc -lvient immédiatement à mon esprit. Que ce ls -1Usoit plus rapide que ls -1purement théorique - la différence devrait être négligeable, mais pour les très grands annuaires.

contre mode
la source
0

Pour exclure les sous-répertoires du nombre, voici une variation de la réponse acceptée de Gilles:

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

Le $(( ))développement arithmétique externe soustrait la sortie du deuxième $( )sous-shell du premier $( ). Le premier $( )est exactement celui de Gilles d'en haut. La deuxième $( )sortie indique le nombre de répertoires "liés" à la cible. Cela vient de ls -od(substituez ls -ldsi vous le souhaitez), où la colonne qui répertorie le nombre de liens durs a cela comme signification spéciale pour les répertoires. Le nombre de « lien » comprend ., ..et les sous - répertoires.

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

utilisateur361782
la source
-2

Je pense que echo * serait plus efficace que n'importe quelle commande 'ls':

echo * | wc -w
Dan Garthwaite
la source
4
Qu'en est-il des fichiers avec un espace dans leur nom? echo 'Hello World'|wc -wproduit 2.
Joseph R.
@JosephR. Caveat Emptor
Dan Garthwaite