如何计算具有特定扩展名的文件及其所在目录?


14

我想知道.c在一个复杂的大型目录结构中有多少个常规文件具有扩展名,以及这些文件分布在多少个目录中。我想要的输出就是这两个数字。

我已经看到了有关如何获取文件数量的问题,但是我也需要知道文件所在目录的数量。

  • 我的文件名(包括目录)可能包含任何字符;它们可以以.或开头,-并带有空格或换行符。
  • 我可能有一些名称以结尾的.c符号链接,以及指向目录的符号链接。我不希望遵循或计数符号链接,或者我至少想知道是否以及何时对其进行计数。
  • 目录结构具有许多级别,顶层目录(工作目录)中至少包含一个.c文件。

我匆匆在(Bash)shell中编写了一些命令来自己计算,但我认为结果不准确...

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运行命令时关闭)结尾(如果我记错了,请告诉我)。这些是我想要的号码。

请随时不要使用此特定测试。

注意:任何外壳或其他语言的答案都会由我测试和赞赏。如果我必须安装新软件包,就没问题。如果您知道GUI解决方案,我建议您分享(但我可能不会安装整个DE来测试它):)我使用Ubuntu MATE 17.10。


编写程序来应对不良的编程习惯非常具有挑战性;)
WinEunuuchs2Unix

Answers:


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 -c将为我们提供每个目录中有多少个文件(sort此处可能不必要,不确定)
  • 使用sed,我将目录名称替换为1,从而消除了所有可能的怪异字符,仅1保留了计数和其余
  • 使我能够使用以下命令转换为以换行符分隔的输出 tr
  • 然后我用awk对其进行总结,以获得文件总数和包含这些文件的目录数。请注意,d此处与基本上相同NR。我本可以省略1sed命令中的插入,而只是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使此glob可以选择点文件,.选择常规文件(因此不选择符号链接),并且:h仅打印目录路径,而不打印文件名(如find%h)(请参阅“ 文件名生成修饰符”部分)。因此,使用awk命令,我们只需要计算出现的唯一目录数,而行数就是文件数。


棒极了。完全使用所需的内容,仅此而已。感谢您的指导:)
Zanna

@Zanna如果发布一些命令以使用符号链接重新创建目录结构,并使用符号链接重新创建预期的输出,则我可能能够相应地解决此问题。
muru

我添加了一些命令以使用符号链接构建(不必要地复杂)。
Zanna

@Zanna我认为此命令不需要进行任何调整即可获得29 7。如果我想补充-Lfind,即上升到41 10。您需要哪个输出?
muru

1
添加了zsh + awk方法。也许有一些方法可以让zsh自己为我打印计数,但不知道如何。
muru

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从路径.(即当前目录)开始,然后到其下的所有位置。
  • 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{$_}++$_是一个特殊变量,它采用当前行的值。它用作hash 的键%k,其值是看到每个输入行(目录名称)的次数。
  • }{:这是一种速记方式END{}。处理完}{所有输入后,后面的任何命令将执行一次。
  • print scalar keys %k, " $.\n"keys %k返回哈希值中的键数组%kscalar 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 shellscript(以及一个变量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

详细的shellscript

这是更详细的版本,也考虑了符号链接,

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

从详细的shellscript中:

$ 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

考虑使用locatefind命令快得多的命令。

运行测试数据

$ 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

感谢Muru的回答,以帮助我从Unix&Linux回答中消除符号链接的文件数量。

感谢Terdon $PWDUnix&Linux answer中对(不是针对我的)回答


以下评论引用了原始答案

简写:

$ 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 updatedblocate如果.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

我有三个Windows 10 NTFS分区自动安装在中/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

计算286,705个目录中的1,637,135个文件需要15秒。YMMV。

有关locate命令的正则表达式处理的详细分类(在本问答中似乎不需要,但以防万一,请仔细阅读):在某些特定目录下使用“定位”?

最近文章的补充阅读:


1
这不计算特定目录中的文件。如您所指出的,它会计算所有匹配的文件(或目录,或任何其他类型的文件).c(请注意,如果-.c当前目录中有一个文件,因为您未引用,它将中断*.c),然后它将打印所有目录在系统中,无论它们是否包含.c文件。
terdon

@terdon您可以传递目录~/my_c_progs/*.c。它正在用.c程序计算638个目录,总目录稍后显示为286,705。我将修改答案以双引号“ * .c”。谢谢你的提示。
WinEunuuchs2Unix

3
是的,您可以使用诸如之类的东西locate -r "/path/to/dir/.*\.c$",但是答案中没有提到。您仅提供指向另一个提及此问题的答案的链接,而没有说明如何使它适应此处提出的问题。您的整个答案集中在如何计算系统上文件和目录的总数上,这与所问的问题“我如何计算.c文件的数量以及包含.c的目录的数量”无关。特定目录中的c个文件”。另外,您输入的数字有误,请在OP中的示例上进行尝试。
terdon

@terdon感谢您的输入。我已经通过您的建议和您在其他SE网站上发布的关于$PWD变量的答案改进了答案:unix.stackexchange.com/a/188191/200094
WinEunuuchs2Unix

1
现在,您必须确保其中$PWD不包含正则表达式中可能特殊的字符
muru
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.