如何计算包括子目录在内的代码行数

94

如果我想计算一个项目中的代码行数。如果所有的文件都在同一个目录中,我可以执行:

cat * | wc -l

但是,如果存在子目录,则此方法无效。为了使其有效,cat命令需要具有递归模式。我猜这可能是xargs的工作,但我想知道是否有更优雅的解决方案?


11
为什么大家都觉得需要猫?是谁告诉你的? - Oliver Friedrich
18
因为有多种方法可以达到同样的目的。 - user295190
1
使用D.Wheeler的sloccount程序。 - Basile Starynkevitch
使用命令 find . -name '*' | xargs wc -l 可以获取每个文件的行数统计以及总行数。请注意,该命令也会对子文件夹进行统计。如果您只想查看 PHP 文件的行数,请将 * 替换为 *.php 等相应的文件类型。 - arcseldon
我们就用这个方式关闭吧,尽管它比较老,但是另一个得到了正确的谷歌关键词并上升了 :-) - Ciro Santilli OurBigBook.com
11个回答

167

首先,您无需使用cat命令来计算行数。这是一种名为“Useless Use of Cat”(UUoC)的反模式。要计算当前目录中文件的行数,请使用wc命令:

wc -l * 

然后find命令会递归子目录:

find . -name "*.c" -exec wc -l {} \;
  • . 是要从顶级目录开始搜索的名称。

  • -name "*.c" 是您感兴趣的文件的模式。

  • -exec 给出要执行的命令。

  • {} 是要传递给命令 (这里是 wc-l) 的查找命令的结果。

  • \; 表示命令结束。

该命令会列出所有找到的文件及其行数,如果您想要所有找到的文件的总行数,则可以使用 find 列出文件(使用 -print 选项),然后再使用 xargs 将此列表作为参数传递给 wc-l。

find . -name "*.c" -print | xargs wc -l 

编辑以回应Robert Gamble的评论(感谢):如果文件名中有空格或换行符(!),则必须使用-print0选项替代-printxargs -null,以便文件名列表被交换为以null结尾的字符串。

find . -name "*.c" -print0 | xargs -0 wc -l
Unix哲学是拥有只做一件事情并且做好的工具。

5
赞同。想指出UUoC(无用的猫用法),但没有这么做。 - ayaz
3
"find ... -print0 | xargs -0 ..." 这个技巧值得记忆。 - detly
6
不仅仅是 OP 使用 cat 不是无用功,而且像 cat * | wc -l 这样的管道正是 cat(代表连接)设计的精髓所在。"无用的 cat" 是指使用 cat 读取一个单一文件并将其通过管道传递给程序,而不是使用输入重定向。如果您不相信在这里使用 cat 不是无用功,只需尝试 wc -l *cat * | wc -l,观察它们的输出差异即可。 - user4815162342
1
如果要调用的命令是可并行化的,则xargs会更有用,因为xargs具有'-P'参数,而find没有该参数,它会生成N个作业。值得注意的是,“xargs”是“findutils”软件包中除“find”之外的少数几个二进制文件之一,它们的设计显然是打算一起使用的。“xargs程序通过收集标准输入上读取的参数来构建和执行命令行。最常见的情况是,这些参数是由find生成的文件名列表。”- GNU findutils - Kent Fredric
2
我对所有关于反模式的讨论感到惊讶,因为没有人提到使用代码行数作为一种虚假伪指标的反模式。 - twalberg
显示剩余8条评论

32
如果你想要一份代码高尔夫解答:
grep '' -R . | wc -l 

使用 wc -l 命令的问题在于它不能很好地递归查找,而使用 oneliners 的方法可以解决这个问题。
find . -exec wc -l {} \;

不会给你总行数,因为它会对每个文件运行一次wc命令(哈哈!),

find . -exec wc -l {} + 

当查找命中约200k字符时,xargs命令的参数限制会导致混乱,因此xargs会多次调用wc,每次只提供部分摘要。1,2

此外,上述grep技巧在遇到二进制文件时不会将超过1行的内容添加到输出中,这可能有助于特殊情况。

通过增加1个额外的命令字符,您可以完全忽略二进制文件:

 grep '' -IR . | wc -l

如果您想在二进制文件上运行行数统计,也可以这样做。
 grep '' -aR . | wc -l 

文档不太清楚它是字符串大小限制还是令牌数量限制。
cd /usr/include;
find -type f -exec perl -e 'printf qq[%s => %s\n], scalar @ARGV, length join q[ ], @ARGV' {} + 
# 4066 => 130974
# 3399 => 130955
# 3155 => 130978
# 2762 => 130991
# 3923 => 130959
# 3642 => 130989
# 4145 => 130993
# 4382 => 130989
# 4406 => 130973
# 4190 => 131000
# 4603 => 130988
# 3060 => 95435

这意味着它非常容易被分割成块。

1
今日我知道了-exec cmd {} +对文件参数的上限为32000。 - sanmiguel
所有命令都有一个限制,这是操作系统级别的限制。http://www.cyberciti.biz/faq/linux-unix-arg_max-maximum-length-of-arguments/ - Kent Fredric
1
@sanmiguel,我刚刚更新了帖子,似乎限制的参数比我想象中要低得多。我的 git 存储库有足够多的文件来触发该限制。 - Kent Fredric
cat **/* | wc -l 更短一些,而且它还会忽略隐藏文件和文件夹(例如 .git 中的文件),这可能是有益的。 - psmith

13

我认为你可能需要使用xargs。

find -name '*php' | xargs cat | wc -l

chromakode的方法给出了相同的结果,但速度要慢很多。如果你使用xargs,你的catwc可以在find开始查找时就可以开始了。

关于Linux: xargs vs. exec {}有一个好的解释,请参考http://dmiessler.com/blog/linux-xargs-vs-exec


但不幸的是,您在那里无法获得多线程好处,因为管道使它们都共享同一处理线。 - Kent Fredric
1
哦,还有,那篇文章是胡扯的。-exec cmd {} +会将文件名打包。xargs也有"-1"参数来模拟find的其他行为。 - Kent Fredric
谢谢Kent。你能给我指一下“-1”的任何文档吗?我正在使用GNU xargs版本4.2.32,但手册中没有任何相关内容。 - Ken
抱歉,"-l" 是限制器。-l[max-lines](小失误) - Kent Fredric
我进行了一些严格的测试,Xargs 在各个方面仍然更快,我将其发布到该页面:http://rafb.net/p/HLOs3385.html。 - Kent Fredric

12

尝试使用find命令,它默认递归目录:

find . -type f -execdir cat {} \; | wc -l


更快的方法是通过xargs进行管道传输。 - Ken
我相信它 :) 我尽量少做一些shell脚本,所以更聪明的xargs方法逃过了我。感谢你教给我一些东西! - chromakode

10

正确的方式是:

find . -name "*.c" -print0 | xargs -0 cat | wc -l

你必须使用-print0,因为Unix文件名中只有两个无效字符:空字节和“/”(斜杠)。因此,例如“xxx \ npasswd”是一个有效的名称。实际上,你更可能遇到其中包含空格的名称。上述命令将把每个单词都计算为一个单独的文件。

您可能还想使用“-type f”而不是-name来限制搜索文件。


不,那不对。xargs 可能会执行多次 wc,导致针对不同文件集合出现多个结果。你应该使用 cat,并在最后像 Ken 所示地将其管道传输到一个 wc 中。 - Johannes Schaub - litb
你说得对。我把xargs调用了cat(而不是wc),然后通过wc将结果管道传输了过去。 - Aaron Digulla

8

如果你使用相对较新的GNU工具,包括Bash,那么在上述解决方案中使用cat或grep是浪费的:

wc -l --files0-from=<(find . -name \*.c -print0)

这可以处理带有空格的文件名,任意递归和任何数量的匹配文件,即使它们超过了命令行长度限制。


这篇文章值得更多的赞,因为它没有暴露xargs多次调用wc的问题。 - FelixJongleur42

3
wc -cl `find . -name "*.php" -type f`

2

我喜欢在项目目录中使用findhead命令一起进行“递归式查找”,例如:

find . -name "*rb" -print0 | xargs -0 head -10000

优点是head会添加文件名和路径:

==> ./recipes/default.rb <==
DOWNLOAD_DIR = '/tmp/downloads'
MYSQL_DOWNLOAD_URL = 'http://cdn.mysql.com/Downloads/MySQL-5.6/mysql-5.6.10-debian6.0-x86_64.deb'
MYSQL_DOWNLOAD_FILE = "#{DOWNLOAD_DIR}/mysql-5.6.10-debian6.0-x86_64.deb"

package "mysql-server-5.5"
...

==> ./templates/default/my.cnf.erb <==
#
# The MySQL database server configuration file.
#
...

==> ./templates/default/mysql56.sh.erb <==
PATH=/opt/mysql/server-5.6/bin:$PATH 

完整的示例请参见我的博客文章:

http://haildata.net/2013/04/using-cat-recursively-with-nicely-formatted-output-including-headers/

请注意,我使用了“head -10000”,如果文件超过10,000行,则会截断输出...但是,对于“非正式项目/目录浏览”,我可以使用head 100000,这种方法非常适合我。

注:原文中的代码未提供,因此无法翻译。

我知道最初的问题是关于 wc 的无用 cat 使用(UUoC),但我想分享一下“带标题的递归 cat 使用”,因为我认为这是一个很方便的技巧/提示。 - Dave Pitts
你也可以使用 head -n-0 命令,它将输出所有行。-n, --lines=[-]K 参数用于指定输出文件的前 K 行或除最后 K 行之外的所有行。当参数以负号开头时,表示输出除最后 K 行之外的所有行。 - Kent Fredric

1
如果您只想生成总行数而不是每个文件的行数,可以使用以下命令:
find . -type f -exec wc -l {} \; | awk '{total += $1} END{print total}'

运作良好。这样可以省去在脚本中进一步进行文本过滤的需要。


0
find . -name "*.h" -print | xargs wc -l

一些关于这段代码如何解决问题的评论会很有帮助。 - pdobb
@pdobb:find会递归搜索当前路径下的所有.h文件,并将找到的文件列表通过“|”(管道)作为输入传递给xargsxargs读取这些文件并将每行转换为空格分隔的参数,传递给wc命令,实际上是对文件中的行数进行计数。 - SD.

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