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

Zan*_*nna 15 command-line bash scripts files find

我想知道.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
Run Code Online (Sandbox Code Playgroud)

这会输出关于不明确重定向的抱怨,错过当前目录中的文件,并在特殊字符上绊倒(例如,重定向find输出在文件名中打印换行符)并写入一大堆空文件(oops)。

如何可靠地枚举我的.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
Run Code Online (Sandbox Code Playgroud)

在生成的结构中,7 个目录包含.c文件,29 个常规文件以.c(如果dotglob在运行命令时关闭)(如果我数错了,请告诉我)。这些是我想要的数字。

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

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

mur*_*uru 17

我没有用符号链接检查输出,但是:

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}'
Run Code Online (Sandbox Code Playgroud)
  • find命令打印.c它找到的每个文件的目录名称。
  • sort | uniq -c将为我们提供每个目录中有多少文件(sort这里可能没有必要,不确定)
  • with 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}'
Run Code Online (Sandbox Code Playgroud)

但是,即使**不应该将符号链接扩展到目录,我也无法在 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
Run Code Online (Sandbox Code Playgroud)

但是 zsh 工作正常,命令可以简化:

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

D启用此 glob 以选择点文件,.选择常规文件(因此,不是符号链接),并:h仅打印目录路径而不是文件名(如find's %h)(请参阅文件名生成修饰符部分)。所以使用 awk 命令我们只需要计算出现的唯一目录的数量,行数就是文件数。


Eli*_*gan 12

Python 具有os.walk,即使面对奇怪的文件名(例如包含换行符的文件名),这样的任务也变得简单、直观且自动健壮。这个 Python 3 脚本,我最初发布在 chat 中,打算在当前目录中运行(但不必位于当前目录中,您可以更改它传递到的路径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)
Run Code Online (Sandbox Code Playgroud)

这将打印直接包含至少一个.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). 我重申,您很少需要这样做,尤其是对于这样的任务,您递归地枚举整个目录结构,无论它有多大,并计算其中满足某些要求的所有文件。


ter*_*don 7

查找 + Perl:

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

解释

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" '
Run Code Online (Sandbox Code Playgroud)