我如何在文件夹层次结构中找到所有不同的文件扩展名?

334

在 Linux 機器上,我想遍歷一個文件夾層次結構並獲取其中所有不同的文件擴展名列表。

從 shell 中實現這個任務的最佳方法是什麼?

18个回答

478
尝试这个(不确定是否是最好的方法,但它可以工作):
find . -type f | perl -ne 'print $1 if m/\.([^.\/]+)$/' | sort -u

它的工作方式如下:

  • 从当前文件夹中查找所有文件
  • 打印文件的扩展名(如果有)
  • 生成一个唯一的排序列表

9
仅供参考:如果您想从搜索中排除某些目录(例如 .svn),请使用 find . -type f -path '*/.svn*' -prune -o -print | perl -ne 'print $1 if m/\.([^.\/]+)$/' | sort -u [来源] (https://dev59.com/43E95IYBdhLWcg3wbtXO#2314680) - Dennis Golomazov
空格不会有任何影响。每个文件名将在单独的一行中,因此文件列表分隔符将是“\n”,而不是空格。 - Ivan Nevostruev
1
在Windows上,这比find命令更好用,而且速度快得多:dir /s /b | perl -ne 'print $1 if m/.([^^.\\]+)$/' | sort -u - Ryan Shillington
4
使用git ls-tree -r HEAD --name-only代替find命令来获取git仓库中的文件列表。 - jakub.g
17
另一种变体,这将显示每个扩展名的计数列表:find . -type f | perl -ne 'print $1 if m/\.([^.\/]+)$/' | sort | uniq -c | sort -n - marcovtwout
显示剩余6条评论

89

不需要使用管道来进行sort,awk可以完成所有操作:

find . -type f | awk -F. '!a[$NF]++{print $NF}'

我无法将其作为别名工作,我得到了awk:在源代码第1行处的语法错误 上下文是
!a[] <<< awk:在源代码第1行处退出。我做错了什么?我的别名定义如下:alias file_ext="find . -type f -name '.' | awk -F. '!a[$NF]++{print $NF}'"
- user2602152
2
@user2602152,问题在于您试图用引号将整个单行命令括起来作为alias命令的参数,但该命令本身已经在find命令中使用了引号。为了解决这个问题,我建议使用bash的字面字符串语法,如下所示:alias file_ext=$'find . -type f -name "*.*" | awk -F. \'!a[$NF]++{print $NF}\'' - SiegeX
如果一个子目录的名称中带有点号,并且文件没有文件扩展名,则此方法将无法正常工作。例如:当我们从主目录运行时,对于maindir/test.dir/myfile,它会失败。 - Nelson Teixeira
1
@NelsonTeixeira 在“查找”命令的末尾添加“-printf“%f\n”并重新运行测试。 - SiegeX
我找到了我想要的东西。你的命令帮助我列出了文件类型,但我想要在类型旁边有一个数字。我谷歌搜索并找到了这个命令:find . -type f | sed -n 's/..*.//p' | sort | uniq -c。感谢你的帮助。 - Big Joe

72
我的无awk、无sed、无Perl、无Python的POSIX兼容替代方案:
find . -name '*.?*' -type f | rev | cut -d. -f1 | rev  | tr '[:upper:]' '[:lower:]' | sort | uniq --count | sort -rn

这个技巧是将行反转并在开头截断扩展名。
它还将扩展名转换为小写。
示例输出:
   3689 jpg
   1036 png
    610 mp4
     90 webm
     90 mkv
     57 mov
     12 avi
     10 txt
      3 zip
      2 ogv
      1 xcf
      1 trashinfo
      1 sh
      1 m4v
      1 jpeg
      1 ini
      1 gqv
      1 gcs
      1 dv

3
在 Mac 上,uniq 没有完整的 --count 标志,但是 -c 完全可以正常工作。 - worc
非常酷,如果不包括没有扩展名的文件就更好了。在存储库的基础上运行此操作会产生大量没有扩展名的git文件。 - Chris Hayes
1
@ChrisHayes,简单帮助:find . -type f -name '*.?* ....',没有完全测试,但应该可以工作。 - Ondra Žižka
busybox的uniq也缺少--count参数,但它具有-c选项。 - user1593842
太棒了!我已经将它添加到一个别名中。谢谢! - mariano-daniel
找到 . -name '.' -type f | rev | cut -d. -f1 | rev | tr '[:upper:]' '[:lower:]' | sort | uniq -c | sort -rn - Jieiku

61

递归版本:

find . -type f | sed -e 's/.*\.//' | sed -e 's/.*\///' | sort -u
如果你想要总数(扩展名出现的次数):
find . -type f | sed -e 's/.*\.//' | sed -e 's/.*\///' | sort | uniq -c | sort -rn

非递归(单文件夹):

for f in *.*; do printf "%s\n" "${f##*.}"; done | sort -u

我基于这篇论坛帖子实现的,功劳应归给那里。


太棒了!这种方法也适用于我的git情境。我一直在试图找出我在上次提交中触及了哪些类型的文件: git show --name-only --pretty="" | sed -e 's/.*\.//' | sed -e 's/.*\///' | sort -u - vulcan raven

41

22
楼主说:“在Linux机器上”。 - Forbesmyester
12
现在已经有Linux版的Powershell了:https://github.com/Microsoft/PowerShell-DSC-for-Linux。 - KIC
5
按照原本的写法,这也会检索包含 . 的目录(例如 jquery-1.3.4 会出现在输出中作为 .4)。改成 dir -file -recurse | select-object extension -unique 只获取文件扩展名。 - mcw
2
@Forbesmyester:像我这样使用Windows的人也会遇到这个问题。所以这很有用。 - Roel
3
感谢您提供PowerShell的答案。您不应该假设用户的搜索方式。很多人给出点赞是有原因的。 - Mahesh

19

在这里,我加入了自己的变化。我认为这是最简单的方法,并且在效率不是一个大问题时很有用。

find . -type f | grep -oE '\.(\w+)$' | sort -u

1
+1 是为了可移植性,尽管正则表达式相当有限,因为它只匹配由单个字母组成的扩展名。使用被接受答案中的正则表达式似乎更好:$ find . -type f | grep -o -E '\.[^.\/]+$' | sort -u - mMontu
1
同意。我在那里有点懈怠了。编辑我的回答以修复你发现的错误。 - gkb0986
很酷。我将引号更改为双引号,更新了grep 二进制文件和依赖项(因为git提供的已经过时),现在这个程序可以在Windows下运行。感觉像Linux用户一样。 - msangel
1
我喜欢这种方式。只是会稍微改变一下正则表达式 $ find . -type f | grep -Eo '\.(\w+)$' | sort -u。原本的正则表达式会显示没有扩展名的文件,在我的情况下那不是我需要的。 - Fernando Crespo
Nr1,非常感谢您提供这个简洁而优雅的示例。 - wuseman

13

查找所有带有点的内容,并仅显示后缀。

find . -type f -name "*.*" | awk -F. '{print $NF}' | sort -u

如果你知道所有的后缀都有三个字符,那么

find . -type f -name "*.???" | awk -F. '{print $NF}' | sort -u

使用sed命令可以显示所有拥有1到4个字符的后缀。将{1,4}修改为你期望后缀中包含的字符范围。

find . -type f | sed -n 's/.*\.\(.\{1,4\}\)$/\1/p'| sort -u

1
不需要使用管道“sort”,awk可以完成所有操作:find . -type f -name "." | awk -F. '!a[$NF]++{print $NF}' - SiegeX
@SiegeX,你应该提供一个单独的答案。我发现这个命令在处理大文件夹时效果最好,因为它会在找到扩展名时打印出来。但请注意,应该是:-name“*.*”。 - Ralf
@Ralf已完成,答案发布在这里。不太确定你所说的“-name“.””是什么意思,因为它已经是这样了。 - SiegeX
1
我的意思是应该是-name "*.*",但是StackOverflow会删除星号字符,这也可能发生在你的评论中。 - Ralf
这似乎应该是被接受的答案,awk比perl更适合作为命令行工具,并且它拥抱将小型可互操作程序管道化成连贯易读的过程的Unix哲学。 - Jon z

9

我尝试了这里的很多答案,甚至是“最佳”答案,但它们都没有达到我特别想要的效果。因此,除了在多个程序的正则表达式代码中坐了12小时之外,还阅读和测试这些答案,我得出了下面的结果,它完全符合我的要求。

 find . -type f -name "*.*" | grep -o -E "\.[^\.]+$" | grep -o -E "[[:alpha:]]{2,16}" | awk '{print tolower($0)}' | sort -u

找到所有可能有扩展名的文件。 从中筛选出扩展名。 查找2到16个字符之间的文件扩展名(如果不合适,请自行调整数字)。这有助于避免缓存文件和系统文件(系统文件位用于搜索jail)。 使用Awk将扩展名打印为小写。 排序并仅带入唯一值。最初我尝试使用awk答案,但它会双倍打印区分大小写的项。 如果需要文件扩展名的计数,请使用以下代码。
find . -type f -name "*.*" | grep -o -E "\.[^\.]+$" | grep -o -E "[[:alpha:]]{2,16}" | awk '{print tolower($0)}' | sort | uniq -c | sort -rn

虽然这些方法需要一些时间才能完成,而且可能不是解决问题的最佳方式,但它们确实有效。
更新: 根据@alpha_989的建议,长文件扩展名会导致问题。这是由于原始正则表达式“[[:alpha:]]{3,6}”。我已更新答案,包括正则表达式“[[:alpha:]]{2,16}”。但是,任何使用此代码的人都应该知道,这些数字是允许的扩展名的最小和最大长度。超出该范围的任何内容都将分成多行输出。
注意:原帖读作“-在3到6个字符之间grep文件扩展名(如果不适合您的需求,请调整数字)。这有助于避免缓存文件和系统文件(系统文件位用于搜索监狱)”。
想法:可以通过以下方式找到超过特定长度的文件扩展名:
 find . -type f -name "*.*" | grep -o -E "\.[^\.]+$" | grep -o -E "[[:alpha:]]{4,}" | awk '{print tolower($0)}' | sort -u

在此处,“4”指要包括的文件扩展名长度,还要查找超出该长度的任何扩展名。


计数版本是否是递归的? - Fernando Montoya
@Shinrai,总的来说效果不错。但是如果你有一些非常长的随机文件扩展名,比如“.download”,它会将“.download”拆分成两部分,并报告两个文件,一个是“downlo”,另一个是“ad”。 - alpha_989
@alpha_989,这是由于正则表达式"[[:alpha:]]{3,6}"还会导致扩展名小于3个字符的问题。根据您的需求进行调整。个人认为,2,16在大多数情况下都可以使用。 - Shinrai
谢谢回复。是的,我后来意识到了这一点。在我进行了类似于您提到的修改之后,它运行得很好。 - alpha_989
找到当前目录下所有扩展名为php的文件,计算它们的总大小:find . -type f -name "*.php" -exec stat -c "%s" {} + | awk '{s+=$1} END {print s}' - anjanesh

5

在Python中,使用生成器处理非常大的目录,包括空扩展名,并获取每个扩展名出现的次数:

import json
import collections
import itertools
import os

root = '/home/andres'
files = itertools.chain.from_iterable((
    files for _,_,files in os.walk(root)
    ))
counter = collections.Counter(
    (os.path.splitext(file_)[1] for file_ in files)
)
print json.dumps(counter, indent=2)

4

既然已经有了使用Perl的解决方案:

如果您安装了Python,也可以这样做(从Shell):

python -c "import os;e=set();[[e.add(os.path.splitext(f)[-1]) for f in fn]for _,_,fn in os.walk('/home')];print '\n'.join(e)"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接