Come posso contare i file con una particolare estensione e le directory in cui si trovano?


14

Voglio sapere quanti file regolari hanno l'estensione .cin una grande struttura di directory complessa e anche quante directory questi file sono distribuiti. L'output che voglio è solo quei due numeri.

Ho visto questa domanda su come ottenere il numero di file, ma devo sapere anche il numero di directory in cui si trovano i file.

  • I miei nomi di file (comprese le directory) potrebbero avere caratteri; possono iniziare con .o -e avere spazi o nuove righe.
  • Potrei avere alcuni symlink i cui nomi finiscono .ce symlink alle directory. Non voglio che i collegamenti simbolici vengano seguiti o contati, o almeno voglio sapere se e quando vengono contati.
  • La struttura della directory ha molti livelli e la directory di livello superiore (la directory di lavoro) contiene almeno un .cfile.

Ho scritto in fretta alcuni comandi nella shell (Bash) per contarli da solo, ma non credo che il risultato sia accurato ...

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

Ciò genera reclami su reindirizzamenti ambigui, manca i file nella directory corrente e scatta su caratteri speciali (ad esempio, l' output reindirizzato findstampa nuove righe nei nomi dei file ) e scrive un intero gruppo di file vuoti (oops).

Come posso enumerare in modo affidabile i miei .cfile e le loro directory di contenimento?


Nel caso in cui aiuti, ecco alcuni comandi per creare una struttura di test con nomi errati e collegamenti simbolici:

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

Nella struttura risultante, 7 directory contengono .cfile e 29 file regolari terminano con .c(se dotglobè disattivato quando vengono eseguiti i comandi) (se ho sbagliato il conto, per favore fatemi sapere). Questi sono i numeri che voglio.

Sentiti libero di non usare questo particolare test.

NB: Le risposte in qualsiasi shell o altra lingua saranno testate e apprezzate da me. Se devo installare nuovi pacchetti, nessun problema. Se conosci una soluzione GUI, ti incoraggio a condividere (ma potrei non arrivare al punto di installare un intero DE per testarlo) :) Uso Ubuntu MATE 17.10.


Scrivere un programma per affrontare cattive abitudini di programmazione si è rivelato piuttosto impegnativo;)
WinEunuuchs2Unix

Risposte:


16

Non ho esaminato l'output con collegamenti simbolici ma:

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}'
  • Il findcomando stampa il nome della directory di ciascun .cfile trovato.
  • sort | uniq -cci darà quanti file ci sono in ogni directory ( sortqui potrebbe non essere necessario, non sono sicuro)
  • con sed, sostituisco il nome della directory con 1, eliminando così tutti i possibili caratteri strani, con solo il conteggio e 1rimanendo
  • permettendomi di convertire in output separato da newline con tr
  • che poi riassumo con awk, per ottenere il numero totale di file e il numero di directory che contenevano quei file. Si noti che dqui è essenzialmente lo stesso di NR. Avrei potuto omettere l'inserimento 1nel sedcomando e appena stampato NRqui, ma penso che sia leggermente più chiaro.

Fino al tr, i dati sono delimitati da NUL, sicuri contro tutti i nomi di file validi.


Con zsh e bash, puoi usare printf %qper ottenere una stringa tra virgolette, che non contiene righe. Quindi, potresti essere in grado di fare qualcosa del tipo:

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

Tuttavia, anche se **non dovrebbe espandersi per i collegamenti simbolici alle directory , non ho potuto ottenere l'output desiderato su 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

Ma zsh ha funzionato bene e il comando può essere semplificato:

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

Dpermette questo glob per selezionare i file di punti, .seleziona i file regolari (così, non link simbolici), e :hstampa solo il percorso della directory e non il nome del file (come finds' %h) (vedere paragrafi sul nome del file Generazione e modificatori ). Quindi con il comando awk dobbiamo solo contare il numero di directory univoche che appaiono e il numero di righe è il conteggio dei file.


È fantastico Utilizza esattamente ciò che è necessario e non di più. Grazie per l'insegnamento :)
Zanna,

@Zanna se pubblichi alcuni comandi per ricreare una struttura di directory con collegamenti simbolici e l'output previsto con collegamenti simbolici, potrei essere in grado di risolvere il problema di conseguenza.
muru,

Ho aggiunto alcuni comandi per creare una struttura di test (inutilmente complicata come al solito) con collegamenti simbolici.
Zanna,

@Zanna Penso che questo comando non abbia bisogno di alcuna regolazione per ottenere 29 7. Se aggiungo -La find, questo va a 41 10. Di quale uscita hai bisogno?
muru,

1
Aggiunto un metodo zsh + awk. Probabilmente c'è un modo per ottenere zsh stesso per stampare il conteggio per me, ma non ho idea di come.
muru,

11

Python ha os.walk, il che rende compiti come questo facili, intuitivi e automaticamente robusti anche di fronte a nomi di file strani come quelli che contengono caratteri di nuova riga. Questo script Python 3, che avevo originariamente postato in Chat , è destinato ad essere eseguito nella directory corrente (ma che non deve essere situato nella directory corrente, e si può cambiare ciò che il percorso che 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)

Ciò stampa il conteggio delle directory che contengono direttamente almeno un file il cui nome termina .c, seguito da uno spazio, seguito dal conteggio dei file i cui nomi finiscono .c. I file "nascosti" - ovvero i file i cui nomi iniziano con - .sono inclusi e le directory nascoste vengono attraversate in modo simile.

os.walkattraversa ricorsivamente una gerarchia di directory. Esso enumera tutte le directory che sono ricorsivamente accessibili dal punto di partenza si dà, ottenendo informazioni su ciascuno di loro come una tupla di tre valori, root, dirs, files. Per ogni directory che attraversa (incluso il primo di cui gli dai il nome):

  • rootcontiene il percorso di quella directory. Si noti che questo è totalmente estraneo alla "directory root" del sistema /(e anche non correlata /root) sebbene andrebbe a quelli se si avvia lì. In questo caso, rootinizia dal percorso - .cioè, la directory corrente - e va ovunque sotto di essa.
  • dirscontiene un elenco dei percorsi di tutte le sottodirectory della directory il cui nome è attualmente presente root.
  • filescontiene un elenco dei percorsi di tutti i file che risiedono nella directory il cui nome è attualmente contenuto rootma che non sono essi stessi directory. Si noti che questo include altri tipi di file rispetto ai file normali, compresi i collegamenti simbolici, ma sembra che non ti aspetti che tali voci finiscano .ce che tu sia interessato a vedere quelli che lo fanno.

In questo caso, ho solo bisogno di esaminare il terzo elemento della tupla, files(che chiamo fsnello script). Come il findcomando, Python os.walkattraversa le mie sottodirectory; l'unica cosa che devo ispezionare da solo è il nome dei file che ognuno di essi contiene. A differenza del findcomando, però, os.walkmi fornisce automaticamente un elenco di quei nomi di file.

Quel copione non segue collegamenti simbolici. Molto probabilmente non vuoi seguire i link simbolici per un'operazione del genere, perché potrebbero formare cicli e perché anche se non ci sono cicli, gli stessi file e le stesse directory possono essere attraversati e conteggiati più volte se sono accessibili tramite link simbolici diversi.

Se hai mai voluto os.walkseguire i link simbolici - cosa che di solito non vorresti - allora puoi passarci followlinks=truesopra. Cioè, invece di scrivere os.walk('.')potresti scrivere os.walk('.', followlinks=true). Ribadisco che raramente lo vorrai, soprattutto per un'attività come questa in cui stai enumerando in modo ricorsivo un'intera struttura di directory, non importa quanto sia grande, e contando tutti i file in essa che soddisfano alcuni requisiti.


7

Trova + Perl:

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

Spiegazione

Il findcomando troverà tutti i file regolari (quindi nessun symlink o directory) e quindi stampa il nome della directory in cui si trovano ( %h) seguito da \0.

  • perl -0 -ne: legge l'input riga per riga ( -n) e applica lo script fornito da -eciascuna riga. L' -0imposta il separatore di linea di ingresso \0in modo da poter leggere l'input nullo delimitato.
  • $k{$_}++: $_è una variabile speciale che accetta il valore della riga corrente. Questo è usato come chiave per l' hash %k , i cui valori sono il numero di volte in cui ogni riga di input (nome della directory) è stata vista.
  • }{: questo è un modo abbreviato di scrivere END{}. Qualsiasi comando dopo il }{sarà eseguito una volta, dopo che tutti gli input sono stati elaborati.
  • print scalar keys %k, " $.\n": keys %krestituisce un array di chiavi nell'hash %k. scalar keys %kfornisce il numero di elementi in quell'array, il numero di directory viste. Questo viene stampato insieme al valore corrente di $., una variabile speciale che contiene il numero della riga di input corrente. Poiché questo viene eseguito alla fine, il numero della riga di input corrente sarà il numero dell'ultima riga, quindi il numero di righe visualizzate finora.

È possibile espandere il comando perl a questo, per chiarezza:

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" '

4

Ecco il mio suggerimento:

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

Questo breve script crea un file temporaneo, trova tutti i file dentro e sotto la directory corrente che termina in .ce scrive l'elenco nel file temporaneo. grepviene quindi utilizzato per contare i file (seguendo Come posso ottenere un conteggio dei file in una directory usando la riga di comando? ) due volte: la seconda volta, le directory che sono elencate più volte vengono rimosse usando sort -udopo aver rimosso i nomi di file da ogni linea usando sed.

Questo funziona anche correttamente con le nuove righe nei nomi dei file: grep -c /conta solo le righe con una barra e quindi considera solo la prima riga di un nome file su più righe nell'elenco.

Produzione

$ 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

4

Piccolo shellscript

Suggerisco un piccolo shellscript bash con due righe di comando principali (e una variabile filetypeper facilitare il passaggio per cercare altri tipi di file).

Non cerca o nei collegamenti simbolici, solo file regolari.

#!/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 dettagliato

Questa è una versione più dettagliata che considera anche collegamenti simbolici,

#!/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

Uscita di prova

Da breve shellscript:

$ ./ccntr 
29 7

Da shellscript dettagliato:

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

4

Liner semplice Perl:

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

O più semplice con il findcomando:

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

Se ti piace giocare a golf e hai un Perl recente (come meno di un decennio):

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," $."'

2

Prendi in considerazione l'uso del locatecomando che è molto più veloce del findcomando.

In esecuzione su dati di test

$ 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

Grazie a Muru per la sua risposta che mi ha aiutato a rimuovere i collegamenti simbolici dal conteggio dei file nella risposta Unix e Linux .

Grazie a Terdon per la sua risposta di $PWD(non indirizzata a me) nella risposta Unix e Linux .


Risposta originale di seguito a cui fanno riferimento i commenti

Forma breve:

$ 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 updatedbAggiorna il database utilizzato dal locatecomando se i .cfile sono stati creati oggi o se li hai eliminati .coggi.
  • locate -cr "$PWD.*\.c$"trova tutti i .cfile nella directory corrente ed è children ( $PWD). Invece di stampare i nomi dei file e stampare conteggio con -cargomento. I rspecifica REGEX anziché predefinito *pattern*corrispondente che può produrre troppi risultati.
  • locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l. Individua tutti i *.cfile nella directory corrente e in basso. Rimuovi il nome del file sedlasciando solo il nome della directory. Contare il numero di file in ciascuna directory usando uniq -c. Conta il numero di directory con wc -l.

Inizia dalla directory corrente con una riga

$ 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

Notare come sono cambiati il ​​conteggio dei file e il conteggio delle directory. Credo che tutti gli utenti abbiano la /usr/srcdirectory e possano eseguire comandi sopra con conteggi diversi a seconda del numero di kernel installati.

Forma lunga:

La forma lunga include il tempo in modo da poter vedere quanto più veloce locateè finito find. Anche se devi eseguirlo sudo updatedbè molte volte più veloce di un singolo 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: si tratta di tutti i file su TUTTE le unità e le partizioni. cioè possiamo anche cercare i comandi di 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

Ho tre partizioni NTFS di Windows 10 montate automaticamente /etc/fstab. Essere consapevoli di individuare tutto sa!

Conte 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

Ci vogliono 15 secondi per contare 1.637.135 file in 286.705 directory. YMMV.

Per una suddivisione dettagliata sulla locategestione della regex del comando (sembra non essere necessaria in queste domande e risposte ma usata per ogni evenienza), leggi questo: Usa "individuare" in una directory specifica?

Letture aggiuntive di articoli recenti:


1
Questo non conta i file in una directory specifica. Come fai notare, conta tutti i file (o directory o qualsiasi altro tipo di file) corrispondenti .c(nota che si romperà se c'è un file nominato -.cnella directory corrente poiché non stai citando *.c) e quindi stamperà tutte le directory nel sistema, indipendentemente dal fatto che contengano file .c.
terdon,

@terdon Puoi passare una directory ~/my_c_progs/*.c. Conta 638 directory con .cprogrammi, le directory totali vengono mostrate in seguito come 286,705. Revisionerò la risposta alla doppia citazione `" * .c ". Grazie per il consiglio.
WinEunuuchs2Unix

3
Sì, puoi usare qualcosa del genere locate -r "/path/to/dir/.*\.c$", ma questo non è menzionato da nessuna parte nella tua risposta. Fornisci solo un link a un'altra risposta che menziona questo, ma senza spiegazioni su come adattarlo per rispondere alla domanda che viene posta qui. Tutta la tua risposta è focalizzata su come contare il numero totale di file e directory sul sistema, il che non è rilevante per la domanda che era "come posso contare il numero di file .c e il numero di directory che contengono. c file in una directory specifica ". Inoltre, i tuoi numeri sono sbagliati, provalo nell'esempio nell'OP.
terdon,

@terdon Grazie per il tuo contributo. Ho migliorato la risposta con i tuoi suggerimenti e una risposta che hai pubblicato su un altro sito SE per $PWDvariabile: unix.stackexchange.com/a/188191/200094
WinEunuuchs2Unix

1
Ora devi assicurarti che $PWDnon contenga personaggi che potrebbero essere speciali in una regex
muru,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.