Qual é a maneira mais econômica de contar quantos arquivos estão em um diretório?

55

CentOS 5.9

Me deparei com um problema outro dia em que um diretório tinha muitos arquivos. Para contar, eu corrils -l /foo/foo2/ | wc -l

Acontece que havia mais de 1 milhão de arquivos em um único diretório (história longa - a causa raiz está sendo corrigida).

Minha pergunta é: existe uma maneira mais rápida de fazer a contagem? Qual seria a maneira mais eficiente de obter a contagem?

Mike B
fonte
5
ls -l|wc -lseria desligado por uma devido ao total de blocos na primeira linha de ls -lsaída
Thomas Nyman
3
@ThomasNyman Na verdade, seria desativado por vários por causa das pseudo-entradas dot e dotdot, mas elas podem ser evitadas usando a -Aflag. -ltambém é problemático devido aos metadados do arquivo de leitura para gerar o formato de lista estendida. Forçar NÃO a -lusar \lsé uma opção muito melhor ( -1é assumida ao canalizar a saída). Consulte a resposta de Gilles para obter a melhor solução aqui.
Caleb
2
O @Caleb ls -lnão gera arquivos ocultos nem as entradas .e ... ls -aA saída inclui arquivos ocultos, incluindo . e ..enquanto a ls -Asaída inclui arquivos ocultos, excluindo . e ... Na resposta de Gilles, a dotglob opção bash shell faz com que a expansão inclua arquivos ocultos, excluindo . e ...
Thomas Nyman

Respostas:

61

Resposta curta:

\ls -afq | wc -l

(Isso inclui .e .., portanto, subtraia 2.)


Quando você lista os arquivos em um diretório, três coisas comuns podem acontecer:

  1. Enumerando os nomes de arquivo no diretório Isso é inevitável: não há como contar os arquivos em um diretório sem enumerá-los.
  2. Classificando os nomes dos arquivos. Os curingas do shell e o lscomando fazem isso.
  3. Chamando statpara recuperar metadados sobre cada entrada de diretório, como se é um diretório.

O número 3 é o mais caro, de longe, porque requer o carregamento de um inode para cada arquivo. Em comparação, todos os nomes de arquivos necessários para o nº 1 são armazenados compactamente em alguns blocos. O nº 2 desperdiça algum tempo de CPU, mas geralmente não é um disjuntor.

Se não houver novas linhas nos nomes dos arquivos, um simples ls -A | wc -linforma quantos arquivos existem no diretório. Lembre-se de que, se você tiver um apelido para ls, isso pode acionar uma chamada para stat(por exemplo, ls --colorou ls -Fprecisar saber o tipo de arquivo, o qual requer uma chamada para stat); portanto, na linha de comando, ligue command ls -A | wc -lou \ls -A | wc -lpara evitar um apelido.

Se houver novas linhas no nome do arquivo, se as novas linhas estão listadas ou não, depende da variante Unix. O coreutils GNU e o BusyBox assumem o padrão de exibição ?para uma nova linha, para que sejam seguros.

Ligue ls -fpara listar as entradas sem classificá-las (nº 2). Isso é ativado automaticamente -a(pelo menos nos sistemas modernos). A -fopção está no POSIX, mas com status opcional; a maioria das implementações suporta, mas não o BusyBox. A opção -qsubstitui caracteres não imprimíveis, incluindo novas linhas por ?; é POSIX, mas não é suportado pelo BusyBox, portanto, omita-o se você precisar do suporte ao BusyBox às custas da contagem excessiva de arquivos cujo nome contém um caractere de nova linha.

Se o diretório não tiver subdiretórios, a maioria das versões findnão chamará statsuas entradas (otimização de diretório em folha: um diretório com uma contagem de links 2 não pode ter subdiretórios, portanto, findnão é necessário procurar os metadados das entradas, a menos que condição -typerequerida). Assim, find . | wc -lé uma maneira portátil e rápida de contar arquivos em um diretório, desde que o diretório não tenha subdiretórios e que nenhum nome de arquivo contenha uma nova linha.

Se o diretório não possuir subdiretórios, mas os nomes dos arquivos puderem conter novas linhas, tente um destes (o segundo deve ser mais rápido se for suportado, mas pode não ser notável).

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

Por outro lado, não use findse o diretório tiver subdiretórios: inclusive find . -maxdepth 1chama statcada entrada (pelo menos com o GNU find e BusyBox find). Você evita a classificação (nº 2), mas paga o preço de uma pesquisa de inode (nº 3) que reduz o desempenho.

No shell sem ferramentas externas, é possível executar a contagem dos arquivos no diretório atual com set -- *; echo $#. Isso perde arquivos de ponto (arquivos cujo nome começa com .) e informa 1 em vez de 0 em um diretório vazio. Essa é a maneira mais rápida de contar arquivos em diretórios pequenos, pois não requer o início de um programa externo, mas (exceto no zsh) perde tempo para diretórios maiores devido à etapa de classificação (# 2).

  • No bash, esta é uma maneira confiável de contar os arquivos no diretório atual:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
  • No ksh93, esta é uma maneira confiável de contar os arquivos no diretório atual:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
  • No zsh, esta é uma maneira confiável de contar os arquivos no diretório atual:

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

    Se você tem o mark_dirsconjunto de opção, certifique-se de desligá-lo: a=(*(DNoN^M)).

  • Em qualquer shell POSIX, esta é uma maneira confiável de contar os arquivos no diretório atual:

    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 esses métodos classificam os nomes dos arquivos, exceto o zsh.

Gilles 'SO- parar de ser mau'
fonte
11
Meu teste empírico em> 1 milhão de arquivos mostra que ele find -maxdepth 1acompanha o ritmo facilmente \ls -Udesde que você não adicione nada como uma -typedeclaração que precise fazer verificações adicionais. Você tem certeza que o GNU encontra realmente chamadas stat? Mesmo a desaceleração find -typenão é nada comparada com a quantidade de ls -lpântanos, se você retornar detalhes do arquivo. Por outro lado, o vencedor da velocidade clara está zshusando o globo sem classificação. (os globs classificados são lsduas vezes mais lentos do que os não classificados são 2x mais rápidos). Gostaria de saber se os tipos de sistema de arquivos afetariam significativamente esses resultados.
Caleb
@Caleb eu corri strace. Isso só é verdade se o diretório tiver subdiretórios: caso contrário find, a otimização do diretório leaf entra em ação (mesmo sem -maxdepth 1), eu deveria ter mencionado isso. Muitas coisas podem afetar o resultado, incluindo o tipo de sistema de arquivos (chamar staté muito mais caro em sistemas de arquivos que representam diretórios como listas lineares do que em sistemas de arquivos que representam diretórios como árvores), se todos os inodes foram criados juntos e, portanto, estão por perto no disco, cache frio ou quente, etc.
Gilles 'SO- stop be evil' (
11
Historicamente, ls -ftem sido a maneira confiável de impedir a chamada stat- isso geralmente é descrito hoje como "a saída não é classificada" (o que também causa) e inclui .e ... -Ae -Unão são opções padrão.
Random832
11
Se você quer especificamente para contar arquivo com uma extensão comum (ou outro string), inserindo que no comando elimina o extra de 2. Aqui está um exemplo:\ls -afq *[0-9].pdb | wc -l
Steven C. Howell
Para sua informação, com o ksh93 version sh (AT&T Research) 93u+ 2012-08-01no meu sistema baseado no Debian, FIGNOREnão parece funcionar. As entradas .e ..são incluídas na matriz resultante
Sergiy Kolodyazhnyy
17
find /foo/foo2/ -maxdepth 1 | wc -l

É consideravelmente mais rápido na minha máquina, mas o .diretório local é adicionado à contagem.

Joel Taylor
fonte
11
Obrigado. Sou obrigado a fazer uma pergunta boba: por que é mais rápido? Porque não está incomodando procurar atributos de arquivo?
Mike B
2
Sim, esse é o meu entendimento. Enquanto o seu não usando o -typeparâmetro finddeve ser mais rápido do quels
Joel Taylor
11
Hmmm .... se eu estou entendendo a documentação de encontrar bem, isso deve ser realmente melhor do que minha resposta. Alguém com mais experiência pode verificar?
Luis Machuca
Adicione a -mindepth 1para omitir o próprio diretório.
Stéphane Chazelas
8

ls -1Uantes que o canal gaste um pouco menos de recursos, pois não tenta classificar as entradas do arquivo, apenas as lê conforme são classificadas na pasta em disco. Também produz menos produção, o que significa um pouco menos de trabalho wc.

Você também pode usar o ls -fque é mais ou menos um atalho para ls -1aU.

Eu não sei se existe uma maneira eficiente de fazer isso através de um comando sem canalização.

Luis Machuca
fonte
8
Aliás, -1 está implícito que a saída vai para um tubo
enzotib
@enzotib - é? Uau ... aprende-se algo novo todos os dias!
Luis Machuca 10/09
6

Outro ponto de comparação. Apesar de não ser um shell oneliner, este programa C não faz nada supérfluo. Observe que os arquivos ocultos são ignorados para corresponder à saída de ls|wc -l( ls -l|wc -lestá desativada em um devido ao total de blocos na primeira linha de saída).

#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
fonte
O uso da readdir()API stdio adiciona alguma sobrecarga e não fornece controle sobre o tamanho do buffer passado para a chamada do sistema subjacente ( getdentsno Linux)
Stéphane Chazelas
3

Você poderia tentar perl -e 'opendir($dh,".");$i=0;while(readdir $dh){$i++};print "$i\n";'

Seria interessante comparar horários com seu cachimbo.

doneal24
fonte
Em meus testes, isso mantém praticamente exatamente o mesmo ritmo que os outros três soluções mais rápidas ( find -maxdepth 1 | wc -l, \ls -AU | wc -le o zshglob não triagem e matriz de contagem com base). Em outras palavras, ele supera as opções com várias ineficiências, como classificar ou ler propriedades de arquivos estranhos. Atrevo-me a dizer uma vez que não ganhar nada também, não vale a pena usar mais de uma solução mais simples a menos que você esteja em perl já :)
Caleb
Observe que isso incluirá as entradas de diretório .e ..na contagem, portanto, é necessário subtrair dois para obter o número real de arquivos (e subdiretórios). No Perl moderno, perl -E 'opendir $dh, "."; $i++ while readdir $dh; say $i - 2'faria isso.
Ilmari Karonen
2

A partir desta resposta , posso pensar nesta como uma solução possível.

/*
 * 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 o programa C acima no diretório em que os arquivos precisam ser listados. Em seguida, execute estes comandos:

gcc getdents.c -o getdents
./getdents | wc -l
Ramesh
fonte
11
Algumas coisas: 1) se você estiver disposto a usar um programa personalizado para isso, basta contar os arquivos e imprimir a contagem; 2) para comparar ls -f, não filtre d_type, apenas ligue d->d_ino != 0; 3) subtraia 2 para .e ...
Matei David 17/01
Veja a resposta vinculada para um exemplo de horários em que é 40x mais rápido que o aceito ls -f.
Matei David
1

Uma solução apenas para o bash, que não requer nenhum programa externo, mas não sabe o quão eficiente:

list=(*)
echo "${#list[@]}"
enzotib
fonte
A expansão de globos não é necessária, a maneira mais eficiente de fazer isso. Além da maioria dos reservatórios terem um limite superior ao número de itens que eles processarão, provavelmente isso será bombardeado ao lidar com um milhão de itens a mais, mas também classifica a saída. As soluções envolvendo find ou sl sem opções de classificação serão mais rápidas.
Caleb
@Caleb, apenas versões antigas do ksh tinham esses limites (e não suportavam essa sintaxe) o AFAIK. Em todas as outras conchas, o limite é apenas a memória disponível. Você tem um ponto que será muito ineficiente, especialmente no bash.
Stéphane Chazelas
1

Provavelmente, a maneira mais eficiente em termos de recursos não envolveria invocações de processos externos. Então eu apostaria em ...

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
fonte
11
Tem números relativos? por quantos arquivos?
SMCI
0

Após corrigir o problema da resposta de @Joel, onde foi adicionado .como um arquivo:

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

tailsimplesmente remove a primeira linha, o que significa que .não é mais contado.

haneefmubarak
fonte
11
Adicionar um par de tubos para omitir uma linha de wcentrada não é muito eficiente, pois a sobrecarga aumenta linearmente em relação ao tamanho da entrada. Neste caso, por que não simplesmente diminuir a contagem final para compensar isso estar fora por um, que é uma operação de tempo constante:echo $(( $(find /foo/foo2 -maxdepth 1 | wc -l) - 1))
Thomas Nyman
11
Em vez de alimentar tantos dados por outro processo, provavelmente seria melhor fazer algumas contas na saída final. let count = $(find /foo/foo2 -maxdepth 1 | wc -l) - 2
Caleb
0

os.listdir () em python pode fazer o trabalho por você. Ele fornece uma matriz do conteúdo do diretório, excluindo o especial '.' e arquivos '..'. Além disso, não é necessário se preocupar com arquivos especiais com caracteres especiais como '\ n' no nome.

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

a seguir é o tempo gasto pelo comando python acima comparado com o comando 'ls -Af'.

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

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

0m0.249s reais
usuário 0m0.064s
sys 0m0.180s
indrajeet
fonte
0

ls -1 | wc -lvem imediatamente à minha mente. Seja ls -1Umais rápido do que o ls -1puramente acadêmico - a diferença deve ser insignificante, mas para diretórios muito grandes.

contra-modo
fonte
0

Para excluir subdiretórios da contagem, aqui está uma variação da resposta aceita de Gilles:

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

A $(( ))expansão aritmética externa subtrai a saída do segundo $( )subshell do primeiro $( ). O primeiro $( )é exatamente o de Gilles, de cima. O segundo $( )gera a contagem de diretórios "vinculados" ao destino. Isso vem ls -od(substitua, ls -ldse desejado), onde a coluna que lista a contagem de links físicos tem isso como um significado especial para diretórios. O "link" contagem inclui ., ..e quaisquer subdiretórios.

Não testei o desempenho, mas parece ser semelhante. Ele adiciona uma estatística do diretório de destino e alguma sobrecarga para o subshell e pipe adicionados.

user361782
fonte
-2

Eu acho que echo * seria mais eficiente do que qualquer comando 'ls':

echo * | wc -w
Dan Garthwaite
fonte
4
E os arquivos com um espaço em seu nome? echo 'Hello World'|wc -wproduz 2.
Joseph R.
@JosephR. Caveat Emptor
Dan Garthwaite