web-dev-qa-db-fra.com

Comment compter les fichiers portant une extension particulière et les répertoires dans lesquels ils se trouvent?

Je veux savoir combien de fichiers normaux portent l'extension .c dans une grande structure de répertoires complexe, et combien de répertoires ces fichiers sont répartis. La sortie que je veux est juste ces deux chiffres.

J'ai vu cette question sur la façon d'obtenir le nombre de fichiers, mais j'ai besoin de connaître le nombre de répertoires dans lesquels se trouvent les fichiers.

  • Mes noms de fichiers (y compris les répertoires) peuvent comporter des caractères. ils peuvent commencer par . ou - et avoir des espaces ou des nouvelles lignes.
  • Je pourrais avoir des liens symboliques dont le nom se termine par .c et des liens symboliques vers des répertoires. Je ne veux pas que les liens symboliques soient suivis ou comptés, ou du moins que je sache si et quand ils sont comptés.
  • La structure de répertoires comporte plusieurs niveaux et le répertoire de niveau supérieur (le répertoire de travail) contient au moins un fichier .c.

J'ai à la hâte écrit quelques commandes dans (Bash) Shell pour les compter moi-même, mais je ne pense pas que le résultat soit précis ...

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

Cela génère des réclamations concernant des redirections ambiguës, des fichiers manquants dans le répertoire en cours, des déclenchements de caractères spéciaux (par exemple, la sortie redirigée find___ imprime des nouvelles lignes dans les noms de fichiers ) et écrit tout un tas de fichiers vides (oops) .

Comment puis-je énumérer de manière fiable mes fichiers .c et leurs répertoires?


Au cas où cela vous aiderait, voici quelques commandes pour créer une structure de test avec des noms et des liens symboliques incorrects:

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

Dans la structure résultante, 7 répertoires contiennent des fichiers .c et 29 fichiers normaux se terminent par .c (si dotglobest désactivé lors de l'exécution des commandes) (si j'ai mal compté, merci de me le faire savoir). Ce sont les chiffres que je veux.

S'il vous plaît n'hésitez pas pas à utiliser ce test particulier.

NB: Les réponses dans n'importe quel shell ou autre langue seront testées et appréciées par moi. Si je dois installer de nouveaux paquets, pas de problème. Si vous connaissez une solution graphique, je vous encourage à partager (mais je n'irais peut-être pas jusqu'à installer un DE complet pour le tester) :) J'utilise Ubuntu MATE 17.10.

14
Zanna

Je n'ai pas examiné la sortie avec des liens symboliques mais:

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}'
  • La commande findimprime le nom du répertoire de chaque fichier .c trouvé.
  • sort | uniq -c nous indiquera le nombre de fichiers contenus dans chaque répertoire (le sortpourrait être inutile ici, pas sûr)
  • avec sedname__, je remplace le nom du répertoire par 1, éliminant ainsi tous les caractères étranges possibles, avec seulement le nombre et le 1 restant
  • me permettant de convertir en sortie séparée par une nouvelle ligne avec trname__
  • je résume ensuite avec awk, pour obtenir le nombre total de fichiers et le nombre de répertoires contenant ces fichiers. Notez que dest essentiellement identique à NRname__. J'aurais pu omettre d'insérer 1 dans la commande sedet juste d'imprimer NRici, mais je pense que cela est un peu plus clair.

Jusqu'au trname__, les données sont délimitées par NUL, elles sont protégées contre tous les noms de fichiers valides.


Avec zsh et bash, vous pouvez utiliser printf %q pour obtenir une chaîne entre guillemets, qui ne contiendrait pas de nouvelles lignes. Donc, vous pourriez être capable de faire quelque chose comme:

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

Cependant, même si ** n'est pas censé développer les liens symboliques vers les répertoires , je n'ai pas pu obtenir le résultat souhaité sous 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

Mais zsh a bien fonctionné et la commande peut être simplifiée:

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

Dpermet à ce glob de sélectionner les fichiers de points, . sélectionne les fichiers normaux (donc, pas de liens symboliques) et :h imprime uniquement le chemin du répertoire et non le nom de fichier (comme findname __'s %h) (Voir les sections sur Filename GenerationModificateurs ). Donc, avec la commande awk, nous avons juste besoin de compter le nombre de répertoires uniques qui apparaissent, et le nombre de lignes est le nombre de fichiers.

16
muru

Python a os.walk , ce qui rend les tâches de ce type faciles, intuitives et automatiquement robustes, même face à des noms de fichiers étranges tels que ceux contenant des caractères de nouvelle ligne. Ce script Python 3, que j'avais posté à l'origine dans le chat , est destiné à être exécuté dans le répertoire actuel (mais it ​​n'a pas à le faire être situé dans le répertoire en cours, et vous pouvez changer le chemin qu'il passe à 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)

Cela imprime le nombre de répertoires contenant directement au moins un fichier dont le nom se termine par .c, suivi d'un espace, suivi du nombre de fichiers dont le nom se termine par .c. Les fichiers "cachés", c'est-à-dire ceux dont le nom commence par .--, sont inclus et les répertoires cachés sont parcourus de la même façon.

os.walk parcourt de manière récursive une hiérarchie de répertoires. Il énumère tous les répertoires accessibles de manière récursive à partir du point de départ indiqué, générant des informations sur chacun d'eux sous la forme d'un tuple de trois valeurs, root, dirs, files. Pour chaque répertoire qu'il parcourt (y compris le premier dont vous lui donnez le nom):

  • rootcontient le chemin d'accès de ce répertoire. Notez que cela n’a aucun rapport avec le "répertoire racine" du système / (et n’a pas non plus de relation avec /root), même s’il voudrait ​​aller à ceux-là si vous commencez par là. Dans ce cas, rootcommence par le chemin .--, c’est-à-dire le répertoire en cours, et passe partout en dessous.
  • dirscontient la liste des chemins d'accès de tous les sous-répertoires du répertoire dont le nom est actuellement conservé dans rootname__.
  • filescontient la liste des chemins d'accès de tous les fichiers qui résident dans le répertoire dont le nom est actuellement conservé dans rootmais qui ne sont pas eux-mêmes des répertoires. Notez que cela inclut d'autres types de fichiers que les fichiers normaux, y compris les liens symboliques, mais il semble que vous ne vous attendiez pas à ce que ces entrées se terminent par .c et que vous êtes intéressé à en voir une.

Dans ce cas, je n'ai qu'à examiner le troisième élément du nuplet, files(que j'appelle fsdans le script). À l'instar de la commande findname__, le os.walk de Python se déplace dans les sous-répertoires pour moi; la seule chose que je doive vérifier moi-même est le nom des fichiers qu’ils contiennent. Contrairement à la commande findname__, os.walk me fournit automatiquement une liste de ces noms de fichiers.

Ce script ne suit pas les liens symboliques. Vous avez très probablement ne pas vouloir que les liens symboliques soient suivis pour une telle opération, car ils pourraient: former des cycles, et parce que même s'il n'y a pas de cycles, les mêmes fichiers et répertoires peuvent être parcourus et comptés plusieurs fois s'ils sont accessibles via différents liens symboliques.

Si vous avez déjà voulu que os.walk suive des liens symboliques - ce que vous ne feriez pas d'habitude - vous pouvez alors lui passer followlinks=true. C'est-à-dire qu'au lieu d'écrire os.walk('.'), vous pouvez écrire os.walk('.', followlinks=true). Je répète que vous souhaiteriez rarement cela, en particulier pour une tâche telle que celle-ci énumérant de manière récursive une structure de répertoires entière, quelle que soit sa taille, et en comptant tous les fichiers qui répondent à certaines exigences.

11
Eliah Kagan

Trouver + Perl:

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

Explication

La commande find trouvera tous les fichiers normaux (donc pas de liens symboliques ni de répertoires), puis imprimera le nom du répertoire dans lequel ils se trouvent (%h) suivi de \0.

  • Perl -0 -ne: lit ligne par ligne (-n) et applique le script donné par -e à chaque ligne. Le -0 définit le séparateur de ligne en entrée sur \0 afin que nous puissions lire les entrées délimitées par un caractère nul.
  • $k{$_}++: $_ est une variable spéciale prenant la valeur de la ligne en cours. Ceci est utilisé comme clé de hash%k, dont les valeurs correspondent au nombre de fois que chaque ligne d'entrée (nom de répertoire) a été vue.
  • }{: il s'agit d'une manière abrégée d'écrire END{}. Toute commande après le }{ sera exécutée une fois, après que toutes les entrées aient été traitées.
  • print scalar keys %k, " $.\n": keys %k renvoie un tableau des clés dans le hachage %k. scalar keys %k donne le nombre d'éléments dans ce tableau, le nombre de répertoires vus. Ceci est imprimé avec la valeur actuelle de $., une variable spéciale qui contient le numéro de ligne actuel. Comme cela se passe à la fin, le numéro de ligne actuel sera le numéro de la dernière ligne, donc le nombre de lignes vues jusqu'à présent.

Vous pouvez développer la commande Perl à ceci, pour plus de clarté:

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

Voici ma suggestion:

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

Ce court script crée un fichier temporaire, trouve tous les fichiers dans et sous le répertoire en cours se terminant par .c et écrit la liste dans le fichier temporaire. grep est ensuite utilisé pour compter les fichiers (après Comment puis-je obtenir un nombre de fichiers dans un répertoire à l'aide de la ligne de commande? ) deux fois: la deuxième fois, les répertoires répertoriés plusieurs fois sont supprimés à l'aide de sort -u après avoir enlevé les noms de fichiers de chaque ligne en utilisant sed.

Cela fonctionne également correctement avec les nouvelles lignes dans les noms de fichiers: grep -c / ne compte que les lignes avec une barre oblique et considère par conséquent uniquement la première ligne d'un nom de fichier multiligne dans la liste.

Sortie

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

Petite coquille

Je suggère un petit script shellh bash avec deux lignes de commande principales (et une variable filetypepour faciliter les permutations afin de rechercher d'autres types de fichiers).

Il ne cherche pas ou dans des liens symboliques, seulement des fichiers normaux.

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

Coquillages verbeux

Ceci est une version plus détaillée qui considère également les liens symboliques,

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

Test de sortie

De shellscript court:

$ ./ccntr 
29 7

De shellscript verbose:

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

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 plus simple avec la commande find:

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

Si vous aimez le golf et avez récemment (comme moins de dix ans) Perl:

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

Pensez à utiliser la commande locatequi est beaucoup plus rapide que la commande findname__.

Exécution sur des données de 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

Merci à Muru pour sa réponse qui m'a aidé à éliminer les liens symboliques du nombre de fichiers dans réponse Unix et Linux .

Merci à Terdon pour sa réponse de $PWD (qui ne m'est pas adressée) dans réponse Unix et Linux .


Réponse originale ci-dessous référencée par des commentaires

Forme courte:

$ 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 Mettre à jour la base de données utilisée par la commande locatesi les fichiers .c ont été créés aujourd'hui ou si vous avez supprimé les fichiers .c aujourd'hui.
  • locate -cr "$PWD.*\.c$" localisez tous les fichiers .c du répertoire en cours et ses enfants ($PWD). Au lieu d’imprimer les noms de fichiers, imprimez le nombre avec l’argument -c. rspécifie une expression rationnelle au lieu de la correspondance par défaut *pattern*, ce qui peut générer trop de résultats.
  • locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l. Localisez tous les fichiers *.c dans le répertoire en cours et au-dessous. Supprimez le nom du fichier avec seden ne laissant que le nom du répertoire. Compter le nombre de fichiers dans chaque répertoire en utilisant uniq -c. Compter le nombre de répertoires avec wc -l.

Commencer au répertoire actuel avec one-liner

$ 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

Notez comment le nombre de fichiers et le nombre de répertoires ont changé. Je crois que tous les utilisateurs ont le répertoire /usr/src et peuvent exécuter des commandes ci-dessus avec des nombres différents en fonction du nombre de noyaux installés.

Forme longue:

La forme longue inclut le temps afin que vous puissiez voir combien locateest plus rapide que findname__. Même si vous devez exécuter Sudo updatedb, il est plusieurs fois plus rapide qu'un seul 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
───────────────────────────────────────────────────────────────────────────────────────────

Remarque: Il s'agit de tous les fichiers sur DE _ TOUS lecteurs et partitions. Nous pouvons aussi rechercher des commandes 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

J'ai trois partitions Windows 10 NTFS montées automatiquement dans /etc/fstab. Sachez que localiser sait tout!

Comte intéressant:

$ 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

Il faut 15 secondes pour compter 1 637 135 fichiers dans 286 705 répertoires. YMMV.

Pour une ventilation détaillée de la gestion des regex de la commande locate(ne semble pas nécessaire dans ce Q & A, mais utilisée au cas où), veuillez lire ceci: tilisez "localize" dans un répertoire spécifique?

Lectures supplémentaires d'articles récents:

2
WinEunuuchs2Unix