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

15

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

Eu já vi essa pergunta sobre como obter o número de arquivos, mas também preciso saber o número de diretórios dos arquivos.

  • Meus nomes de arquivos (incluindo diretórios) podem ter qualquer caractere; eles podem começar com . ou - e ter espaços ou novas linhas.
  • Eu posso ter alguns links simbólicos cujos nomes terminam com .c e links simbólicos para diretórios. Eu não quero que os links simbólicos sejam seguidos ou contados, ou pelo menos eu 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 arquivo .c .

Eu escrevi apressadamente alguns comandos na shell (Bash) para contá-los eu mesmo, 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 dispara caracteres especiais (por exemplo, redirecionado find output imprime novas linhas em nomes de arquivos ) e grava um monte de arquivos vazios (oops).

Como posso enumerar meus arquivos .c e seus diretórios contidos de forma confiável?

Caso isso ajude, aqui estão alguns comandos para criar uma estrutura de teste com nomes inválidos 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 .c arquivos, e 29 arquivos regulares terminam com .c (se dotglob estiver desativado quando os comandos forem executados) (se eu tiver cometido erros de cálculo, por favor me avise) . Estes são os números que eu quero.

Por favor, sinta-se livre não para usar este teste em particular.

N.B .: As respostas em qualquer shell ou outro idioma serão testadas & apreciado por mim. Se eu tiver que instalar novos pacotes, não há problema. Se você conhece uma solução de GUI, eu encorajo você a compartilhar (mas eu posso não ir tão longe a ponto de instalar um DE inteiro para testá-lo) :) Eu uso o Ubuntu MATE 17.10.

    
por Zanna 09.04.2018 / 09:29

7 respostas

16

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

find . -type f -iname '*.c' -printf '%h
shopt -s globstar dotglob nocaseglob
printf "%q\n" **/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'
' | sort -z | uniq -zc | sed -zr 's/([0-9]) .*/ 1/' | tr '
$ 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
' '\n' | awk '{f += $1; d += $2} END {print f, d}'
  • O comando find imprime o nome do diretório de cada arquivo .c encontrado.
  • sort | uniq -c nos dará quantos arquivos estão em cada diretório (o sort pode ser desnecessário aqui, não tenho certeza)
  • com sed , substituo o nome do diretório por 1 , eliminando assim todos os possíveis caracteres estranhos, com apenas a contagem e 1 restantes
  • permitindo que eu converta para saída separada por nova linha com tr
  • que então eu so passo com o awk, para obter o número total de arquivos e o número de diretórios que continham esses arquivos. Note que d aqui é essencialmente o mesmo que NR . Eu poderia ter omitido a inserção de 1 no comando sed , e apenas imprimi NR aqui, mas acho que isso é um pouco mais claro.

Até 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 nela. Então, você pode fazer algo como:

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

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

find . -type f -iname '*.c' -printf '%h
shopt -s globstar dotglob nocaseglob
printf "%q\n" **/*.c | awk -F/ '{NF--; f++} !c[$0]++{d++} END {print f, d}'
' | sort -z | uniq -zc | sed -zr 's/([0-9]) .*/ 1/' | tr '
$ 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
' '\n' | awk '{f += $1; d += $2} END {print f, d}'

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

D permite que este glob selecione arquivos de pontos, . seleciona arquivos regulares (portanto, não links simbólicos) e :h imprime apenas o caminho do diretório e não o nome do arquivo (como find 's %h ) (Veja seções em Geração de nome de arquivo e Modificadores ). 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.

    
por muru 09.04.2018 / 10:36
11

O Python tem os.walk , que torna as tarefas mais fáceis, intuitivas e e automaticamente robusto, mesmo em face de nomes de arquivos estranhos, como aqueles que contêm caracteres de nova linha. Este script do Python 3, que eu tinha originalmente publicado no bate-papo , deve ser executado no diretório atual (mas ele não precisa estar localizado no diretório atual, e você pode mudar o caminho que ele passa para 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" - isto é, arquivos cujos nomes começam com . - estão incluídos, e diretórios ocultos são similarmente percorridos.

os.walk recursivamente percorre uma hierarquia de diretórios. Enumera todos os diretórios que são recursivamente acessíveis a partir do ponto de partida que você fornece, gerando informações sobre cada um deles como uma tupla de três valores, root, dirs, files . Para cada diretório que ele percorre (incluindo o primeiro cujo nome você atribuiu):

  • root contém o nome do caminho desse diretório. Note que isto é totalmente não relacionado ao "diretório raiz" do sistema / (e também não relacionado a /root ), embora para aqueles que você inicia lá. Nesse caso, root começa no caminho . - ou seja, o diretório atual - e passa por todos os lugares abaixo dele.
  • dirs contém uma lista dos nomes de caminho de todos os subdiretórios do diretório cujo nome atualmente é mantido em root .
  • files contém uma lista dos nomes de caminho de todos os arquivos que residem no diretório cujo nome atualmente é mantido em root , mas que não são diretórios em si. Observe que isso inclui outros tipos de arquivos que arquivos regulares, incluindo links simbólicos, mas parece que você não espera que essas entradas terminem em .c e estejam interessadas em ver qualquer uma delas.

Nesse caso, só preciso examinar o terceiro elemento da tupla, files (que eu chamo de fs no script). Como o comando find , o os.walk do Python atravessa subdiretórios para mim; a única coisa que tenho que inspecionar é o nome dos arquivos que cada um deles contém. Ao contrário do comando find , o os.walk fornece automaticamente uma lista desses nomes de arquivos.

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

Se você quisesse que os.walk seguisse os links simbólicos - o que você normalmente não faria - então você pode passar followlinks=true para ele. Ou seja, em vez de escrever os.walk('.') , você pode escrever os.walk('.', followlinks=true) . Eu reitero que você raramente iria querer isso, especialmente para uma tarefa como essa em que você está enumerando recursivamente uma estrutura de diretórios inteira, não importa quão grande seja, e contando todos os arquivos nela que atendem a algum requisito.

    
por Eliah Kagan 09.04.2018 / 11:27
7

Encontrar + Perl:

$ find . -type f -iname '*.c' -printf '%h
find  . -type f -iname '*.c' -printf '%h
$ find . -type f -iname '*.c' -printf '%h
find  . -type f -iname '*.c' -printf '%h%pre%' | 
    perl -0 -e 'while($line = <STDIN>){
                    $dirs{$line}++; 
                    $tot++;
                } 
                $count = scalar keys %dirs; 
                print "$count $tot\n" '
' | perl -0 -ne '$k{$_}++; }{ print scalar keys %k, " $.\n" ' 7 29
' | perl -0 -e 'while($line = <STDIN>){ $dirs{$line}++; $tot++; } $count = scalar keys %dirs; print "$count $tot\n" '
' | perl -0 -ne '$k{$_}++; }{ print scalar keys %k, " $.\n" ' 7 29

Explicação

O comando find encontrará quaisquer arquivos regulares (portanto, não há links simbólicos ou diretórios) e, em seguida, imprimirá o nome do diretório em que eles estão ( %h ) seguido por perl -0 -ne .

  • -n : leia a entrada linha por linha ( -e ) e aplique o script dado por -0 a cada linha. O $k{$_}++ configura o separador de linha de entrada como $_ para que possamos ler a entrada delimitada por nulo.
  • %k : }{ é uma variável especial que recebe o valor da linha atual. Isso é usado como uma chave para o hash END{} , cujos valores são o número de vezes que cada linha de entrada ( nome do diretório) foi visto.
  • }{ : esta é uma forma abreviada de escrever print scalar keys %k, " $.\n" . Todos os comandos após o keys %k serão executados uma vez, após toda a entrada ter sido processada.
  • %k : scalar keys %k retorna uma matriz das chaves no hash $. . %code% fornece o número de elementos nessa matriz, o número de diretórios visualizados. Isso é impresso junto com o valor atual de %code% , uma variável especial que conté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é o momento.

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

%pre%     
por terdon 10.04.2018 / 11:16
4

Esta é 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 dentro e sob o diretório atual, terminando em .c e grava a lista no arquivo temporário. grep é então usado para contar os arquivos (seguindo 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 usando sort -u após remover nomes de arquivos de cada linha usando sed .

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

Saída

$ 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
    
por dessert 09.04.2018 / 09:57
4

Pequeno shellscript

Eu sugiro um pequeno shellscript bash com duas linhas de comando principais (e uma variável filetype para facilitar a troca para procurar outros tipos de arquivos).

Não procura ou em 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 verboso

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

De short shellscript:

$ ./ccntr 
29 7

De um 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
$ 
    
por sudodus 09.04.2018 / 17:35
4

Simple Perl one liner:

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 comando find :

find dir1 dir2 -type f -name '*.c' -printf '%h
perl -MFile::Find=find -E'find(sub{/\.c$/&&-f&&($c{$File::Find::dir}=++$c)},".");say 0+keys%c," $c"'
' | perl -l -0ne'$c{$_}=1}{print 0 + keys %c, " $."'

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

find -type f -name '*.c' -printf '%h
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
'|perl -0nE'$c{$_}=1}{say 0+keys%c," $."'
find dir1 dir2 -type f -name '*.c' -printf '%h
perl -MFile::Find=find -E'find(sub{/\.c$/&&-f&&($c{$File::Find::dir}=++$c)},".");say 0+keys%c," $c"'
' | perl -l -0ne'$c{$_}=1}{print 0 + keys %c, " $."'
    
por Hynek -Pichi- Vychodil 10.04.2018 / 14:15
2

Considere o uso do comando locate , que é muito mais rápido que o comando find .

Executando em 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 ao Muru por sua resposta para me ajudar a remover os links simbólicos da contagem de arquivos em Unix & Resposta do Linux .

Obrigado a Terdon por sua resposta de $PWD (não direcionada a mim) em Unix e amp ; Resposta do Linux .

Resposta original abaixo referenciada por comentários

Forma abreviada:

$ 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 updatedb Atualizar banco de dados usado pelo comando locate se .c arquivos foram criados hoje ou se você tiver excluído .c arquivos hoje.
  • locate -cr "$PWD.*\.c$" localiza todos os arquivos .c no diretório atual e seus filhos ( $PWD ). Em vez de imprimir nomes de arquivos, e imprima a contagem com -c argumento. O r especifica o regex em vez do padrão *pattern* matching, o que pode gerar muitos resultados.
  • %código%. Localize todos os arquivos locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l no diretório atual e abaixo. Remova o nome do arquivo com *.c , deixando apenas o nome do diretório. Conte o número de arquivos em cada diretório usando sed . Conte o número de diretórios com uniq -c .

Iniciar 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 a contagem de diretórios foram alteradas. Acredito que todos os usuários tenham o diretório wc -l e possam executar comandos acima com diferentes contagens, dependendo do número de kernels instalados.

Forma longa:

O formulário longo inclui o tempo para que você possa ver quanto mais rápido o /usr/src está acima de locate . Mesmo se você tiver que executar find , é muitas vezes mais rápido que um único sudo updatedb .

───────────────────────────────────────────────────────────────────────────────────────────
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
───────────────────────────────────────────────────────────────────────────────────────────

Observação: são todos os arquivos em unidades e partições ALL . ou seja, podemos procurar por comandos do Windows também:

$ 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 Windows 10 NTFS montadas automaticamente em find / . Esteja ciente de que o locate 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

São necessários 15 segundos para contar 1.637.135 arquivos em 286.705 diretórios. YMMV.

Para uma análise detalhada sobre o tratamento de regex do comando /etc/fstab (parece não ser necessário neste Q & A, mas usado apenas no caso), por favor leia isto: Use" localizar "sob algum diretório específico?

Leitura adicional de artigos recentes:

por WinEunuuchs2Unix 10.04.2018 / 01:37