Как подсчитать файлы с определенным расширением и каталоги, в которых они находятся?


14

Я хочу знать, сколько обычных файлов имеют расширение .cв большой сложной структуре каталогов, а также сколько каталогов распределено по этим файлам. Я хочу получить только эти два числа.

Я видел этот вопрос о том, как узнать количество файлов, но мне нужно знать количество каталогов, в которых находятся файлы.

  • Мои имена файлов (включая каталоги) могут содержать любые символы; они могут начинаться с .или -иметь пробелы или переводы строк.
  • У меня могут быть некоторые символические ссылки, имена которых заканчиваются .c, и символические ссылки на каталоги. Я не хочу, чтобы символические ссылки отслеживались или учитывались, или я, по крайней мере, хочу знать, когда и когда они подсчитываются.
  • Структура каталогов имеет много уровней, и каталог верхнего уровня (рабочий каталог) содержит по крайней мере один .cфайл.

Я поспешно написал некоторые команды в оболочке (Bash), чтобы подсчитать их сам, но я не думаю, что результат точен ...

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

Это приводит к жалобам на неоднозначные перенаправления, пропускает файлы в текущем каталоге и отключает специальные символы (например, перенаправленный findвывод печатает новые строки в именах файлов ) и записывает целую кучу пустых файлов (упс).

Как я могу надежно перечислить мои .cфайлы и содержащиеся в них каталоги?


Если это поможет, вот несколько команд для создания тестовой структуры с плохими именами и символическими ссылками:

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

В полученной структуре 7 каталогов содержат .cфайлы, а 29 обычных файлов заканчиваются .c(если dotglobво время выполнения команд отключено) (если я пропустил, пожалуйста, дайте мне знать). Это цифры, которые я хочу.

Пожалуйста, не стесняйтесь не использовать этот конкретный тест.

Примечание: ответы в любой оболочке или на другом языке будут проверены и оценены мной. Если мне нужно установить новые пакеты, нет проблем. Если вы знаете решение с графическим интерфейсом, я призываю вас поделиться им (но я не могу пойти так далеко, чтобы установить целый DE для его тестирования) :) Я использую Ubuntu MATE 17.10.


Написание программы для борьбы с вредными привычками программирования оказалось довольно сложным
делом

Ответы:


16

Я не проверял вывод с помощью символических ссылок, но:

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}'
  • Команда findпечатает имя каталога каждого .cнайденного файла.
  • sort | uniq -cwill даст нам количество файлов в каждом каталоге (здесь sortможет быть ненужно, не уверен)
  • с sed, я заменяю имя каталога на 1, таким образом устраняя все возможные странные символы, только с количеством и 1оставшимися
  • что позволяет мне преобразовать в разделенный строкой вывод с tr
  • который я затем суммирую с помощью awk, чтобы получить общее количество файлов и количество каталогов, содержащих эти файлы. Обратите внимание, что dздесь по существу так же, как NR. Я мог бы опустить вставку 1в sedкоманду и просто напечатать NRздесь, но я думаю, что это немного яснее.

До тех пор tr, пока данные не разделены NUL, они защищены от всех допустимых имен файлов.


С zsh и bash вы можете использовать printf %qдля получения строки в кавычках, в которой не будет символов новой строки. Таким образом, вы можете сделать что-то вроде:

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

Однако, хотя **предполагается , что он не будет расширяться для символических ссылок на каталоги , я не смог получить желаемый результат на 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

Но zsh работал нормально, и команду можно упростить:

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

Dпозволяет этому глобусу выбирать точечные файлы, .выбирает обычные файлы (то есть, не символические ссылки) и :hпечатает только путь к каталогу, а не имя файла (например, find«s» %h) (см. разделы « Генерация имени файла и модификаторы» ). Таким образом, с помощью команды awk нам просто нужно подсчитать количество появляющихся уникальных каталогов, а количество строк - это количество файлов.


Это потрясающе. Использует именно то, что нужно и не более. Спасибо за обучение :)
Zanna

@Zanna, если вы отправите несколько команд для воссоздания структуры каталогов с символическими ссылками и ожидаемого вывода с символическими ссылками, я мог бы исправить это соответствующим образом.
Муру

Я добавил несколько команд для создания (как обычно, излишне сложной) тестовой структуры с символическими ссылками.
Занна

@Zanna Я думаю, что эта команда не нуждается в каких-либо настройках, чтобы получить 29 7. Если я добавлю -Lк find, это идет до 41 10. Какой выход вам нужен?
Муру

1
Добавлен метод zsh + awk. Возможно, есть какой-то способ заставить zsh сам распечатать счет для меня, но понятия не имею, как.
Муру

11

Python имеет os.walk, что делает такие задачи простыми, интуитивно понятными и автоматически устойчивыми даже перед лицом странных имен файлов, таких как те, которые содержат символы новой строки. Этот сценарий Python 3, который я первоначально разместил в чате , предназначен для запуска в текущем каталоге (но он не обязательно должен находиться в текущем каталоге, и вы можете изменить путь, по которому он проходит 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)

Это печатает число каталогов, которые непосредственно содержат по крайней мере один файл, имя которого заканчивается .c, после пробела, а затем количество файлов, имена которых заканчиваются .c. «Скрытые» файлы - то есть файлы, имена которых начинаются с .- включены, и скрытые каталоги просматриваются аналогичным образом.

os.walkрекурсивно пересекает иерархию каталогов. Он перечисляет все каталоги, которые рекурсивно доступны из начальной точки, которую вы им даете, получая информацию о каждом из них в виде кортежа из трех значений root, dirs, files. Для каждого каталога, в который он попадает (включая первый, имя которого вы даете):

  • rootсодержит путь к этому каталогу. Обратите внимание, что это совершенно не связано с «корневым каталогом» системы /(и также не связано с ним /root), хотя оно и пойдет на это, если вы начнете там. В этом случае rootначинается с пути .--ie, текущего каталога - и идет везде под ним.
  • dirsсодержит список путей всех подкаталогов каталога, имя которого в настоящее время хранится в root.
  • filesсодержит список путей всех файлов, которые находятся в каталоге, чье имя в настоящее время хранится, rootно которые сами не являются каталогами. Обратите внимание, что это включает в себя другие виды файлов, отличные от обычных файлов, в том числе символические ссылки, но похоже, что вы не ожидаете, что такие записи заканчиваются, .cи заинтересованы в том, чтобы увидеть какие-либо записи.

В этом случае мне нужно только изучить третий элемент кортежа files(который я называю fsв сценарии). Как и findкоманда, Python os.walkпереходит в подкаталоги для меня; единственное, что я должен проверить сам - это имена файлов, которые каждый из них содержит. В отличие от findкоманды, однако, os.walkавтоматически предоставляет мне список этих имен файлов.

Этот скрипт не следует по символическим ссылкам. Скорее всего, вы не хотите, чтобы символические ссылки использовались для такой операции, потому что они могут образовывать циклы, и потому что даже если циклов нет, одни и те же файлы и каталоги могут просматриваться и подсчитываться несколько раз, если они доступны через разные символические ссылки.

Если вы когда-нибудь захотели os.walkперейти по символическим ссылкам - что вы обычно не хотели бы - тогда вы можете перейти followlinks=trueк нему. То есть вместо того, чтобы писать, os.walk('.')ты можешь писать os.walk('.', followlinks=true). Я повторяю, что вы бы редко этого хотели, особенно для такой задачи, когда вы рекурсивно перечисляете всю структуру каталогов, независимо от ее размера, и подсчитываете все файлы в ней, которые удовлетворяют некоторому требованию.


7

Найти + Perl:

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

объяснение

Команда findнайдет все обычные файлы (без символических ссылок или каталогов), а затем напечатает имя каталога, в котором они находятся ( %h), а затем \0.

  • perl -0 -ne: прочитайте строку за строкой ( -n) и примените скрипт, заданный -eдля каждой строки. -0Устанавливает входную строку разделитель для \0так что мы можем прочитать нуль-разделители входа.
  • $k{$_}++: $_это специальная переменная, которая принимает значение текущей строки. Это используется как ключ к хешу %k , значения которого - количество раз, которое каждая строка ввода (имя каталога) была замечена.
  • }{: это сокращенный способ написания END{}. Любые команды после }{будут выполнены один раз, после того, как все входные данные были обработаны.
  • print scalar keys %k, " $.\n": keys %kвозвращает массив ключей в хэше %k. scalar keys %kдает количество элементов в этом массиве, количество просмотренных каталогов. Это печатается вместе с текущим значением $., специальной переменной, которая содержит текущий номер строки ввода. Так как это выполняется в конце, текущий номер строки ввода будет номером последней строки, так что количество строк, видимых до сих пор.

Вы можете расширить команду perl для этого, для ясности:

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

Вот мое предложение:

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

Этот короткий сценарий создает временный файл, находит все файлы в текущем каталоге и под ним, оканчивающиеся на, .cи записывает список в временный файл . grepзатем используется для подсчета файлов (следуя инструкциям Как получить количество файлов в каталоге с помощью командной строки? ) дважды: во второй раз каталоги, которые перечислены несколько раз, удаляются с использованием sort -uпосле удаления имен файлов из каждой строки с помощью sed.

Это также правильно работает с символами новой строки в именах файлов: grep -c /считает только строки с косой чертой и, следовательно, рассматривает только первую строку многострочного имени файла в списке.

Выход

$ 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

Небольшой шеллскрипт

Я предлагаю небольшой командный скрипт bash с двумя основными командными строками (и переменную, filetypeчтобы облегчить переключение для поиска других типов файлов).

Он не ищет или в символических ссылках, только обычные файлы.

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

Подробный шеллскрипт

Это более подробная версия, которая также рассматривает символические ссылки,

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

Тестовый вывод

Из краткого сценария:

$ ./ccntr 
29 7

Из подробного шеллскрипта:

$ 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

Простой 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

Или проще с findкомандой:

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

Если вы любите играть в гольф и имеете недавно (например, менее десяти лет) 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," $."'

2

Попробуйте использовать locateкоманду, которая намного быстрее, чем findкоманда.

Бег на тестовых данных

$ 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

Спасибо Муру за его ответ, который помог мне убрать символические ссылки из числа файлов в ответах Unix и Linux .

Спасибо Тердону за его ответ $PWD(не направленный на меня) в ответе Unix & Linux .


Оригинальный ответ ниже, на который ссылаются комментарии

Короткая форма:

$ 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Обновите базу данных, используемую locateкомандой, если .cфайлы были созданы сегодня или если вы удалили .cфайлы сегодня.
  • locate -cr "$PWD.*\.c$"найдите все .cфайлы в текущем каталоге и его дочерние элементы ( $PWD). Вместо того, чтобы печатать имена файлов и печатать количество с -cаргументом. В rопределяет регулярное выражение , а не по умолчанию *pattern*соответствия , которые могут дать слишком много результатов.
  • locate -r "$PWD.*\.c$" | sed 's%/[^/]*$%/%' | uniq -c | wc -l, Найдите все *.cфайлы в текущем каталоге и ниже. Удалите имя файла, sedоставив только имя каталога. Подсчитайте количество файлов в каждом каталоге, используя uniq -c. Подсчитать количество каталогов с wc -l.

Начать в текущем каталоге с одной строки

$ 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

Обратите внимание, как количество файлов и количество каталогов изменились. Я полагаю, что у всех пользователей есть /usr/srcкаталог, и они могут выполнять вышеуказанные команды с разным количеством в зависимости от количества установленных ядер.

Длинная форма:

Длинная форма включает в себя время, чтобы вы могли видеть, насколько быстрее locateзакончится find. Даже если вам нужно бежать, sudo updatedbон во много раз быстрее, чем один 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
───────────────────────────────────────────────────────────────────────────────────────────

Примечание. Это все файлы на ВСЕХ дисках и разделах. то есть мы можем искать команды 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

У меня есть три раздела NTFS Windows 10, автоматически смонтированные в /etc/fstab . Будьте в курсе, найти все знает!

Интересный граф:

$ 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

Для подсчета 1 637 135 файлов в 286 705 каталогах требуется 15 секунд. YMMV.

Для подробного locateразбора обработки регулярных выражений команды (кажется, не требуется в этом вопросе и ответе, но используется на всякий случай), пожалуйста, прочитайте это: Использовать «locate» в каком-то определенном каталоге?

Дополнительное чтение из последних статей:


1
Это не считает файлы в определенном каталоге. Как вы указали, он считает все файлы (или каталоги, или файлы любого другого типа), совпадающие .c(обратите внимание, что он сломается, если -.cв текущем каталоге есть файл с именем, поскольку вы не цитируете *.c), а затем напечатает все каталоги. в системе, независимо от того, содержат ли они .c файлы.
Terdon

@terdon Вы можете передать каталог ~/my_c_progs/*.c. Он насчитывает 638 каталогов с .cпрограммами, общее количество каталогов будет показано позже 286,705. Я исправлю ответ в двойной кавычке `" * .c ". Спасибо за чаевые.
WinEunuuchs2Unix

3
Да, вы можете использовать что-то подобное locate -r "/path/to/dir/.*\.c$", но это нигде не упоминается в вашем ответе. Вы только даете ссылку на другой ответ, который упоминает об этом, но без объяснения того, как адаптировать его для ответа на вопрос, задаваемый здесь. Весь ваш ответ сфокусирован на том, как подсчитать общее количество файлов и каталогов в системе, что не относится к заданному вопросу: «Как я могу подсчитать количество файлов .c и количество содержащихся каталогов. c файлами в определенном каталоге ". Кроме того, ваши цифры неверны, попробуйте это на примере в ОП.
тердон

@terdon Спасибо за ваш вклад. Я улучшил ответ с вашими предложениями и ответом, который вы разместили на другом сайте SE для $PWDпеременной: unix.stackexchange.com/a/188191/200094
WinEunuuchs2Unix

1
Теперь вы должны убедиться, что в нем $PWDнет символов, которые могут быть особенными в регулярном выражении
muru
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.