Como posso contar arquivos com uma extensão específica e os diretórios em que estão?

14

Quero saber quantos arquivos regulares têm a extensão .cem uma estrutura de diretórios grande e complexa e também quantos diretórios esses arquivos estão espalhados. A saída que eu quero é apenas esses dois números.

Vi essa pergunta sobre como obter o número de arquivos, mas também preciso saber o número de diretórios em que os arquivos estão.

  • Meus nomes de arquivos (incluindo diretórios) podem ter caracteres; eles podem começar com .ou -e ter espaços ou novas linhas.
  • Talvez eu tenha alguns links simbólicos cujos nomes terminem com .ce links simbólicos para diretórios. Não quero que links simbólicos sejam seguidos ou contados, ou pelo menos quero saber se e quando eles estão sendo contados.
  • A estrutura de diretórios possui muitos níveis e o diretório de nível superior (o diretório de trabalho) possui pelo menos um .carquivo.

Escrevi rapidamente alguns comandos no shell (Bash) para contá-los, mas não acho que o resultado seja preciso ...

shopt -s dotglob
shopt -s globstar
mkdir out
for d in **/; do
     find "$d" -maxdepth 1 -type f -name "*.c" >> out/$(basename "$d")
done
ls -1Aq out | wc -l
cat out/* | wc -l

Isso gera reclamações sobre redirecionamentos ambíguos, perde arquivos no diretório atual e ativa caracteres especiais (por exemplo, a saída redirecionada findimprime novas linhas nos nomes de arquivos ) e grava um monte de arquivos vazios (oops).

Como enumerar meus .carquivos de maneira confiável e os diretórios que os contêm?


Caso isso ajude, aqui estão alguns comandos para criar uma estrutura de teste com nomes incorretos e links simbólicos:

mkdir -p cfiles/{1..3}/{a..b} && cd cfiles
mkdir space\ d
touch -- i.c -.c bad\ .c 'terrible
.c' not-c .hidden.c
for d in space\ d 1 2 2/{a..b} 3/b; do cp -t "$d" -- *.c; done
ln -s 2 dirlink
ln -s 3/b/i.c filelink.c

Na estrutura resultante, 7 diretórios contêm .carquivos e 29 arquivos regulares terminam com .c(se dotglobestiver desativado quando os comandos forem executados) (se eu tiver errado a conta, informe-me). Estes são os números que eu quero.

Por favor, sinta-se livre para não usar este teste específico.

NB: Respostas em qualquer shell ou outro idioma serão testadas e apreciadas por mim. Se eu tiver que instalar novos pacotes, não há problema. Se você conhece uma solução GUI, recomendo que você compartilhe (mas talvez não vá tão longe quanto instalar um DE inteiro para testá-lo) :) Eu uso o Ubuntu MATE 17.10.

Zanna
fonte
Escrever um programa para lidar com maus hábitos de programação acabou por ser bastante desafiador;)
WinEunuuchs2Unix

Respostas:

16

Não examinei a saída com links simbólicos, mas:

find . -type f -iname '*.c' -printf '%h\0' |
  sort -z |
  uniq -zc |
  sed -zr 's/([0-9]) .*/\1 1/' |
  tr '\0' '\n' |
  awk '{f += $1; d += $2} END {print f, d}'
  • O findcomando imprime o nome do diretório de cada .carquivo encontrado.
  • sort | uniq -cwill nos fornece quantos arquivos existem em cada diretório (o que sortpode ser desnecessário aqui, não tenho certeza)
  • com sed, substituo o nome do diretório por 1, eliminando todos os caracteres estranhos possíveis, apenas com a contagem e o 1restante
  • permitindo-me converter em saída separada por nova linha com tr
  • que então resumo com o awk, para obter o número total de arquivos e o número de diretórios que os continham. Observe que daqui é essencialmente o mesmo queNR . Eu poderia ter omitido a inserção 1no sedcomando e apenas impresso NRaqui, mas acho que isso é um pouco mais claro.

Até o momento tr, os dados são delimitados por NUL, seguros contra todos os nomes de arquivos válidos.


Com zsh e bash, você pode usar printf %q para obter uma string entre aspas, que não teria novas linhas. Portanto, você pode fazer algo como:

shopt -s globstar dotglob nocaseglob
printf "%q\n" **/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'

No entanto, embora ** não deva se expandir para links simbólicos para diretórios , não consegui obter a saída desejada no bash 4.4.18 (1) (Ubuntu 16.04).

$ shopt -s globstar dotglob nocaseglob
$ printf "%q\n" ./**/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'
34 15
$ echo $BASH_VERSION
4.4.18(1)-release

Mas o zsh funcionou bem, e o comando pode ser simplificado:

$ printf "%q\n" ./**/*.c(D.:h) | awk '!c[$0]++ {d++} END {print NR, d}'
29 7

Dpermite que este glob selecione arquivos de ponto, .selecione arquivos regulares (portanto, não links simbólicos) e :himprima apenas o caminho do diretório e não o nome do arquivo (como findos %h) (consulte as seções em Geração e modificadores de nome de arquivo ). Portanto, com o comando awk, precisamos apenas contar o número de diretórios exclusivos que aparecem e o número de linhas é a contagem de arquivos.

muru
fonte
Fantástico. Usa exatamente o que é necessário e não mais. Obrigado para o ensino :)
Zanna
@ Zanna, se você postar alguns comandos para recriar uma estrutura de diretórios com links simbólicos e a saída esperada com links simbólicos, talvez eu consiga corrigir isso de acordo.
Muru
Eu adicionei alguns comandos para criar uma estrutura de teste (desnecessariamente complicada, como de costume) com links simbólicos.
Zanna
@ Zanna Acho que esse comando não precisa de ajustes para obter 29 7. Se eu adicionar -La find, que vai até 41 10. Qual saída você precisa?
Muru
1
Adicionado um método zsh + awk. Provavelmente existe uma maneira de o próprio zsh imprimir a contagem para mim, mas não faço ideia de como.
Muru
11

O Python possui os.walk, o que torna tarefas como essa fáceis, intuitivas e automaticamente robustas, mesmo diante de nomes de arquivos estranhos, como aqueles que contêm caracteres de nova linha. Este script Python 3, que eu tinha originalmente publicado no chat , se destina a ser executado no diretório atual (mas ele não tem que ser localizado no diretório atual, e você pode mudar o caminho que ele passa a os.walk):

#!/usr/bin/env python3

import os

dc = fc = 0
for _, _, fs in os.walk('.'):
    c = sum(f.endswith('.c') for f in fs)
    if c:
        dc += 1
        fc += c
print(dc, fc)

Isso imprime a contagem de diretórios que contêm diretamente pelo menos um arquivo cujo nome termina em .c, seguido por um espaço, seguido pela contagem de arquivos cujos nomes terminam em .c. Arquivos "ocultos" - ou seja, arquivos cujos nomes começam com - .são incluídos e os diretórios ocultos são atravessados ​​de maneira semelhante.

os.walkpercorre recursivamente uma hierarquia de diretórios. Ele enumera todos os diretórios que são recursivamente acessíveis a partir do ponto inicial que você fornece, fornecendo informações sobre cada um deles como uma tupla de três valores root, dirs, files. Para cada diretório para o qual ele acessa (incluindo o primeiro cujo nome você deu):

  • rootmantém o nome do caminho desse diretório. Note-se que este é totalmente alheios ao "diretório raiz" do sistema /(e também alheios a /root) embora seria ir para aqueles se você começar por aí. Nesse caso,root inicia no caminho .- ou seja, o diretório atual - e vai para todo lugar abaixo dele.
  • dirscontém uma lista dos nomes de caminho de todos os subdiretórios do diretório cujo nome está atualmente emroot .
  • filesmantém uma lista dos nomes de caminho de todos os arquivos que residem no diretório cujo nome está atualmente armazenado, rootmas que não são os próprios diretórios. Observe que isso inclui outros tipos de arquivos que não os regulares, incluindo links simbólicos, mas parece que você não espera que essas entradas terminem .ce está interessado em ver o que faz.

Nesse caso, só preciso examinar o terceiro elemento da tupla files(que chamo fsno script). Como o findcomando, o Python os.walkatravessa subdiretórios para mim; a única coisa que tenho para me inspecionar são os nomes dos arquivos que cada um deles contém. Ao contrário do findcomando, no entanto, os.walkfornece-me automaticamente uma lista desses nomes de arquivos.

Esse script não segue links simbólicos. Você provavelmente não deseja que os links simbólicos sejam seguidos para essa operação, porque eles podem formar ciclos e, mesmo que não haja ciclos, os mesmos arquivos e diretórios podem ser percorridos e contados várias vezes, se estiverem acessíveis através de links simbólicos diferentes.

Se você já quis os.walkseguir links simbólicos - o que normalmente não faria -, pode passar followlinks=truepara ele. Ou seja, em vez de escrever, os.walk('.')você poderia escrever os.walk('.', followlinks=true). Reitero que você raramente desejaria isso, especialmente para uma tarefa como essa, na qual recursivamente enumera toda uma estrutura de diretórios, não importa o tamanho, e conte todos os arquivos nela que atendem a algum requisito.

Eliah Kagan
fonte
7

Encontre + Perl:

$ find . -type f -iname '*.c' -printf '%h\0' | 
    perl -0 -ne '$k{$_}++; }{ print scalar keys %k, " $.\n" '
7 29

Explicação

O findcomando encontrará todos os arquivos regulares (sem links simbólicos ou diretórios) e, em seguida, imprimirá o nome do diretório em que estão ( %h) seguido por \0.

  • perl -0 -ne: leia a entrada linha por linha ( -n) e aplique o script fornecido por -ecada linha. A -0define o separador de linha de entrada para\0 que possamos ler a entrada delimitada por nulo.
  • $k{$_}++: $_é uma variável especial que aceita o valor da linha atual. Isso é usado como uma chave para o hash %k , cujos valores são o número de vezes que cada linha de entrada (nome do diretório) foi vista.
  • }{: esta é uma maneira abreviada de escrever END{}. Qualquer comando após o }{será executado uma vez, depois que toda a entrada tiver sido processada.
  • print scalar keys %k, " $.\n": keys %kretorna uma matriz das chaves no hash %k. scalar keys %kfornece o número de elementos nessa matriz, o número de diretórios vistos. Isso é impresso junto com o valor atual de $., uma variável especial que mantém o número da linha de entrada atual. Como isso é executado no final, o número da linha de entrada atual será o número da última linha, portanto, o número de linhas vistas até agora.

Você pode expandir o comando perl para isso, para maior clareza:

find  . -type f -iname '*.c' -printf '%h\0' | 
    perl -0 -e 'while($line = <STDIN>){
                    $dirs{$line}++; 
                    $tot++;
                } 
                $count = scalar keys %dirs; 
                print "$count $tot\n" '
Terdon
fonte
4

Aqui está a minha sugestão:

#!/bin/bash
tempfile=$(mktemp)
find -type f -name "*.c" -prune >$tempfile
grep -c / $tempfile
sed 's_[^/]*$__' $tempfile | sort -u | grep -c /

Esse script curto cria um arquivo temporário, localiza todos os arquivos no diretório atual que termina em .ce grava a lista no arquivo temporário. grepé então usado para contar os arquivos (a seguir: Como posso obter uma contagem de arquivos em um diretório usando a linha de comando? ) duas vezes: Na segunda vez, os diretórios listados várias vezes são removidos sort -uapós a remoção de nomes de arquivos de cada linha usando sed.

Isso também funciona corretamente com novas linhas nos nomes de arquivos: grep -c / conta apenas linhas com uma barra e, portanto, considera apenas a primeira linha de um nome de arquivo com várias linhas na lista.

Resultado

$ tree
.
├── 1
   ├── 1
      ├── test2.c
      └── test.c
   └── 2
       └── test.c
└── 2
    ├── 1
       └── test.c
    └── 2

$ tempfile=$(mktemp);find -type f -name "*.c" -prune >$tempfile;grep -c / $tempfile;sed 's_[^/]*$__' $tempfile | sort -u | grep -c /
4
3
sobremesa
fonte
4

Shellscript pequeno

Sugiro um pequeno shellscript do bash com duas linhas de comando principais (e uma variável filetypepara facilitar a alternância para procurar outros tipos de arquivo).

Ele não procura ou nos links simbólicos, apenas arquivos regulares.

#!/bin/bash

filetype=c
#filetype=pdf

# count the 'filetype' files

find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l | tr '\n' ' '

# count directories containing 'filetype' files

find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \;|grep 'contains file(s)$'|wc -l

Shellscript detalhado

Esta é uma versão mais detalhada que também considera links simbólicos,

#!/bin/bash

filetype=c
#filetype=pdf

# counting the 'filetype' files

echo -n "number of $filetype files in the current directory tree: "
find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l

echo -n "number of $filetype symbolic links in the current directory tree: "
find -type l -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l
echo -n "number of $filetype normal files in the current directory tree: "
find -type f -name "*.$filetype" -ls|sed 's#.* \./##'|wc -l
echo -n "number of $filetype symbolic links in the current directory tree including linked directories: "
find -L -type f -name "*.$filetype" -ls 2> /tmp/c-counter |sed 's#.* \./##' | wc -l; cat /tmp/c-counter; rm /tmp/c-counter

# list directories with and without 'filetype' files (good for manual checking; comment away after test)
echo '---------- list directories:'
 find    -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;
echo ''
#find -L -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;

# count directories containing 'filetype' files

echo -n "number of directories with $filetype files: "
find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \;|grep 'contains file(s)$'|wc -l

# list and count directories including symbolic links, containing 'filetype' files
echo '---------- list all directories including symbolic links:'
find -L -type d -exec bash -c "ls -AF '{}' |grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)' || echo '{} empty'" \;
echo ''
echo -n "number of directories (including symbolic links) with $filetype files: "
find -L -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null && echo '{} contains file(s)'" \; 2>/dev/null |grep 'contains file(s)$'|wc -l

# count directories without 'filetype' files (good for checking; comment away after test)

echo -n "number of directories without $filetype files: "
find -type d -exec bash -c "ls -AF '{}'|grep -e '\.'${filetype}$ -e '\.'${filetype}'\*'$ > /dev/null || echo '{} empty'" \;|grep 'empty$'|wc -l

Saída de teste

Do shellscript curto:

$ ./ccntr 
29 7

Do shellscript detalhado:

$ LANG=C ./c-counter
number of c files in the current directory tree: 29
number of c symbolic links in the current directory tree: 1
number of c normal files in the current directory tree: 29
number of c symbolic links in the current directory tree including linked directories: 42
find: './cfiles/2/2': Too many levels of symbolic links
find: './cfiles/dirlink/2': Too many levels of symbolic links
---------- list directories:
. empty
./cfiles contains file(s)
./cfiles/2 contains file(s)
./cfiles/2/b contains file(s)
./cfiles/2/a contains file(s)
./cfiles/3 empty
./cfiles/3/b contains file(s)
./cfiles/3/a empty
./cfiles/1 contains file(s)
./cfiles/1/b empty
./cfiles/1/a empty
./cfiles/space d contains file(s)

number of directories with c files: 7
---------- list all directories including symbolic links:
. empty
./cfiles contains file(s)
./cfiles/2 contains file(s)
find: './cfiles/2/2': Too many levels of symbolic links
./cfiles/2/b contains file(s)
./cfiles/2/a contains file(s)
./cfiles/3 empty
./cfiles/3/b contains file(s)
./cfiles/3/a empty
./cfiles/dirlink empty
find: './cfiles/dirlink/2': Too many levels of symbolic links
./cfiles/dirlink/b contains file(s)
./cfiles/dirlink/a contains file(s)
./cfiles/1 contains file(s)
./cfiles/1/b empty
./cfiles/1/a empty
./cfiles/space d contains file(s)

number of directories (including symbolic links) with c files: 9
number of directories without c files: 5
$ 
sudodus
fonte
4

Um liner Perl simples:

perl -MFile::Find=find -le'find(sub{/\.c\z/ and -f and $c{$File::Find::dir}=++$c}, @ARGV); print 0 + keys %c, " $c"' dir1 dir2

Ou mais simples com o findcomando:

find dir1 dir2 -type f -name '*.c' -printf '%h\0' | perl -l -0ne'$c{$_}=1}{print 0 + keys %c, " $."'

Se você gosta de golfe e tem Perl recente (com menos de uma década):

perl -MFile::Find=find -E'find(sub{/\.c$/&&-f&&($c{$File::Find::dir}=++$c)},".");say 0+keys%c," $c"'
find -type f -name '*.c' -printf '%h\0'|perl -0nE'$c{$_}=1}{say 0+keys%c," $."'
Hynek -Pichi- Vychodil
fonte
2

Considere usar o locatecomando que é muito mais rápido que o findcomando.

Executando dados de teste

$ sudo updatedb # necessary if files in focus were added `cron` daily.
$ printf "Number Files: " && locate -0r "$PWD.*\.c$" | xargs -0 -I{} sh -c 'test ! -L "$1" && echo "regular file"' _  {} | wc -l &&  printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -cu | wc -l
Number Files: 29
Number Dirs.: 7

Obrigado a Muru por sua resposta para me ajudar a remover links simbólicos da contagem de arquivos na resposta Unix e Linux .

Agradeço a Terdon por sua resposta $PWD(não direcionada a mim) na resposta Unix e Linux .


Resposta original abaixo referenciada por comentários

Forma curta:

$ cd /
$ sudo updatedb
$ printf "Number Files: " && locate -cr "$PWD.*\.c$"
Number Files: 3523
$ printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l 
Number Dirs.: 648
  • sudo updatedbAtualize o banco de dados usado pelo locatecomando se os .carquivos foram criados hoje ou se você excluiu .carquivos hoje.
  • locate -cr "$PWD.*\.c$"localize todos os .carquivos no diretório atual e seus filhos ( $PWD). Em vez de imprimir nomes de arquivos, imprima e conte com -cargumento. O respecifica regex em vez da *pattern*correspondência padrão, que pode gerar muitos resultados.
  • locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l. Localize todos os *.carquivos no diretório atual e abaixo. Remova o nome do arquivo seddeixando apenas o nome do diretório. Conte o número de arquivos em cada diretório usando uniq -c. Conte o número de diretórios com wc -l.

Comece no diretório atual com uma linha

$ cd /usr/src
$ printf "Number Files: " && locate -cr "$PWD.*\.c$" &&  printf "Number Dirs.: " && locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l
Number Files: 3430
Number Dirs.: 624

Observe como a contagem de arquivos e o diretório foram alterados. Acredito que todos os usuários tenham o /usr/srcdiretório e possam executar comandos acima com contagens diferentes, dependendo do número de kernels instalados.

Forma longa:

O formulário longo inclui o tempo para que você possa ver quanto tempo mais rápido locateacabou find. Mesmo se você precisar executá- sudo updatedblo, é muitas vezes mais rápido que um único find /.

───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ sudo time updatedb
0.58user 1.32system 0:03.94elapsed 48%CPU (0avgtext+0avgdata 7568maxresident)k
48inputs+131920outputs (1major+3562minor)pagefaults 0swaps
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Files: " && locate -cr $PWD".*\.c$")
Number Files: 3523

real    0m0.775s
user    0m0.766s
sys     0m0.012s
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Dirs.: " && locate -r $PWD".*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l) 
Number Dirs.: 648

real    0m0.778s
user    0m0.788s
sys     0m0.027s
───────────────────────────────────────────────────────────────────────────────────────────

Nota: São todos os arquivos em TODAS as unidades e partições. ou seja, também podemos procurar comandos do Windows:

$ time (printf "Number Files: " && locate *.exe -c)
Number Files: 6541

real    0m0.946s
user    0m0.761s
sys     0m0.060s
───────────────────────────────────────────────────────────────────────────────────────────
rick@alien:~/Downloads$ time (printf "Number Dirs.: " && locate *.exe | sed 's%/[^/]*$%/%' | uniq -c | wc -l) 
Number Dirs.: 3394

real    0m0.942s
user    0m0.803s
sys     0m0.092s

Eu tenho três partições NTFS do Windows 10 montadas automaticamente /etc/fstab . Esteja ciente de localizar sabe tudo!

Contagem interessante:

$ time (printf "Number Files: " && locate / -c &&  printf "Number Dirs.: " && locate / | sed 's%/[^/]*$%/%' | uniq -c | wc -l)
Number Files: 1637135
Number Dirs.: 286705

real    0m15.460s
user    0m13.471s
sys     0m2.786s

Leva 15 segundos para contar 1.637.135 arquivos em 286.705 diretórios. YMMV.

Para uma análise detalhada locatedo manuseio de expressões regulares do comando (parece não ser necessário nesta seção de perguntas e respostas, mas usada apenas para o caso), leia o seguinte: Use "localizar" em algum diretório específico?

Leitura adicional de artigos recentes:

WinEunuuchs2Unix
fonte
1
Isso não conta os arquivos em um diretório específico. Como você aponta, ele conta todos os arquivos (ou diretórios ou qualquer outro tipo de arquivo) que correspondem .c(observe que ele será quebrado se houver um arquivo nomeado -.cno diretório atual, pois você não está citando *.c) e, em seguida, imprimirá todos os diretórios no sistema, independentemente de eles conterem arquivos .c.
terdon
@terdon Você pode passar um diretório ~/my_c_progs/*.c. Está contando 638 diretórios com .cprogramas, o total de diretórios é mostrado posteriormente como 286,705. Vou revisar a resposta para aspas duplas `" * .c ". Obrigado pela dica.
WinEunuuchs2Unix
3
Sim, você pode usar algo como locate -r "/path/to/dir/.*\.c$", mas isso não é mencionado em nenhum lugar da sua resposta. Você fornece apenas um link para outra resposta que mencione isso, mas sem explicação de como adaptá-lo para responder à pergunta que está sendo feita aqui. Toda a sua resposta está focada em como contar o número total de arquivos e diretórios no sistema, o que não é relevante para a pergunta: "como posso contar o número de arquivos .c e o número de diretórios que contêm". arquivos c em um diretório específico ". Além disso, seus números estão errados, tente no exemplo no OP.
terdon
@terdon Obrigado pela sua contribuição. Eu melhorei a resposta com as suas sugestões e uma resposta que você postou em outro site SE para $PWDvariável: unix.stackexchange.com/a/188191/200094
WinEunuuchs2Unix
1
Agora você tem que garantir que $PWDnão contenha caracteres que talvez especial em um regex
Muru