正在计数文件...
示例 1:ls
使用 Ubuntu 20.04.3 或特定于
bash50171$ 的代码来计数目录和计数常规文件
ls -1 | wc -l
好消息是,上面的ls
代码处理了带空格的文件名,因为它' '
在两边加上了单引号'file name'
坏消息是,上述ls
代码计数错误:文件名中带有换行符的一个 (1) 文件,被算作两个 (2) 文件。
问题:文件计数错误。
示例#2:ls
代码给出正确的文件数:
ls -1qi | grep -o '^ *[0-9]*' | wc -l
上述ls
命令正确地计算了带有换行符的文件数。该命令计算了 inode 编号列表。
上面的缩短ls
命令是:
ls -1qi
使用 可正确显示带有空格的文件名' '
。 使用 可正确显示带有换行符的文件名' '
。
如何创建问题文件?使用:
touch 'a b' 'a b' a$'\xe2\x80\x82'b a$'\xe2\x80\x83'b a$'\t'b a$'\n'b
递归命令,添加R
:
ls -1qRi
ls -1qRi | grep -o '^ *[0-9]*'
ls -1qRi | grep -o '^ *[0-9]*' | wc -l
问题:
ls
何时在代码中使用?- 何时不应
ls
在代码中使用?
参考文献A:
为什么你不应该解析输出ls
解释如下简而言之,解析ls
是一种不好的做法。
参考文献B:
这个帖子解释为什么不是解析ls
(以及该做什么)?
示例#2 代码避开了一个问题(一个障碍),
即文件名中带有换行符的文件。
ls -1qi | grep -o '^ *[0-9]*' | wc -l
哪种计数代码比示例 2 更可靠?
ls -1qi | grep -o '^ *[0-9]*' | wc -l
计数代码的正确含义是:
- 计数目录
- 统计常规文件数量
- 计算符号链接数
- 统计隐藏文件数量
- 计数并显示带有空格的文件名
- 计数并显示带有新行的文件名
- 计数在一个 (1) 目录中
- 递归计数
换句话说:要算文件的话,什么是可靠代码(dependable code)?
答案1
分析
ls
使用-1
和-q
并不是计算文件数的最糟糕方法在某些情况下. 这两个选项均在POSIX 规范ls
,你可以称它们为便携式的。
标准“不解析ls
”文章反对使用 来可靠地获取文件名的想法ls
。如果你想要计算条目数,那么你实际上并不需要文件名,有时谨慎使用ls
可能会对你有用。但是存在一般问题:
- 要区分常规文件和符号链接或目录,您需要使用
-l
并检查drwxr-xr-x
或类似的字符串。如果你想同时区分隐藏文件和非隐藏文件(并且你习惯-a
打印隐藏文件),那么你需要检查文件名开头是否有一个点。这并不简单,-l
因为一个完全不同含义的点可能会出现在行的前面。是的,ls -p
可以帮助在没有 的情况下发现目录-l
,但这仅适用于目录。 并且 可能ls -F
会有所帮助。根据您要计数的文件,需要不同的 选项ls
以及不同的 模式grep
。这可能会很快变得丑陋。请注意,像这样的方法正是我们在这种情况下所说的“解析”:分析一些可能复杂的结构以获取您需要的信息。这引出了我们的主要问题。 - 的输出
ls
不是为被解析而设计的。它被设计为易于人类阅读。解析ls
就像锤击螺丝。在某些情况下,最终结果可能是可以接受的,但锤子不是用来敲螺丝的。 - 如果您习惯于在它可以工作的情况下解析输出,
ls
那么您将更愿意ls
在它不能可靠工作的情况下进行解析。如果你只有一把锤子,那么所有东西看起来都像钉子。
基本解决方案:find
这里的正确替代词是什么ls
?在我看来,find
,让我们从头构建一个示例命令并分析一些怪癖。
首先,中的默认操作find
是-print
。它将路径名作为以换行符结尾的字符串打印到 stdout。如果路径名本身包含至少一个换行符,则行数将多于文件数。这意味着find . | wc -l
不是统计所有行的好方法文件正确的携带方式是:
# count all files `find' can find, starting from `.', recursively
find . -exec printf a \; | wc -c
其中a
可以是任何单字节字符。对于find
找到的每个文件(包括.
!),都会打印一个字节;wc -c
计算这些字节数(您也可以打印固定行数并计算行数)。缺点是-exec
会为每个文件生成一个单独的printf
进程,这很昂贵,也很慢。使用 GNU,find
您可以让它find
自己完成以下工作printf
:
find . -printf a | wc -c
上述命令应该表现得更好,但它不可移植。一种可移植且可能更好的方法是使用printf
你的内置函数sh
:
# count all files `find' can find, starting from `.', recursively
find . -exec sh -c 'for f do printf a; done' find-sh {} + | wc -c
和这种方法onesh
将处理许多路径名并printf a
为每个路径名调用。可能会生成多个sh
(以避免argument list too long
错误,find
这样做很聪明),但每个路径名仍少于一个。(注意:find-sh
在此处解释:中的第二个 sh 是什么sh -c 'some shell code' sh
?)
我写 ”大概改进的方法”,因为printf
可能是也可能不是一个内置在你的 中sh
。如果它不是内置命令,那么命令的性能会比 的命令稍差-exec printf …
。实际上printf
, 基本上是 的任何实现中的内置命令sh
。但正式来说,这并不是必需的。
魔法开始 - 添加测试
find
能够对访问的文件执行各种测试。想要统计常规文件?这里:
# count all regular files `find' can find, staring from `.', recursively
find . -type f \
-exec sh -c 'for f do printf a; done' find-sh {} + | wc -c
隐藏文件?这里:
# count all hidden files `find' can find, starting from `.', recursively
find "$PWD" -name '.*' \
-exec sh -c 'for f do printf a; done' find-sh {} + | wc -c
注意,如果我使用,find . …
则当前工作目录将匹配,-name '.*'
无论其“真实”名称如何。我使用了(正确引用)$PWD
来使find
当前工作目录在其“真实”名称下被识别。
您可以组合测试。隐藏的常规文件?这里:
# count all hidden regular files `find' can find, starting from `.', recursively
find "$PWD" -type f -name '.*' \
-exec sh -c 'for f do printf a; done' find-sh {} + | wc -c
您几乎可以测试任何东西。记住-exec foo … \;
也是一个测试,当且仅当foo
返回退出状态时它才成功0
;这样您就可以构建自定义测试(例子)。
缺点是find
不容易掌握。常见的意外:
你可能会发现我的这个答案很有用(尤其是“理论”和“陷阱”)。-type f
不过,像这样的简单测试相当简单。
然后是递归性。find .
find.
和下面的所有内容。在 GNU 中,find
可以使用-mindepth 1
来省略起点,类似地-maxdepth 1
抑制下降到子目录。换句话说,GNUfind . -mindepth 1 -maxdepth 1
应该找到ls -A
打印的内容。我相信 BSDfind
会使用-depth 1
它(注意-depth n
与 非常不同-depth
)。所有这些都不是可移植的。这个答案提供便携式解决方案:
一般来说,您想要的是深度 1(
-mindepth 1 -maxdepth 1
),因为您不想考虑.
(深度 0),这样就更简单了:find . ! -name . -prune -extra-conditions-and-actions
这引出了下面的例子:
# count all hidden regular files `find' can find, inside `.', non-recursively
find . ! -name . -prune -type f -name '.*' \
-exec sh -c 'for f do printf a; done' find-sh {} + | wc -c
请注意,在这里使用 是安全的.
(不需要$PWD
),因为.
不通过! -name .
,因此它匹配的事实-name '.*'
无关紧要。实际上,如果我们使用$PWD
,我们会让事情变得复杂,因为我们需要用! -name .
其他东西替换,而且一般来说这不是一件容易的事。
ls … | … | wc -l
和我们的之间的一个很大的区别find … | wc -c
是:find
我们不解析任何东西。我们的测试find
直接测试我们想要测试的内容,而不是它的某些文本表示;它们不依赖于我们对任何工具(如ls
或其他)输出格式的理解。带有空格、换行符或其他内容的路径名不会破坏任何东西,因为它们永远不会出现在我们处理的任何内容中。
find
另一个重要的区别是运行几乎任何测试的能力。
魔法仍在继续——多重反击
我们知道如何统计几乎符合任何条件的文件。我们在上一节中使用的每个命令都为我们提供了单身的数量。如果我们想要两个数字,例如文件总数和常规文件数量,那么我们可以运行两个不同的find … | wc -c
命令。这不是最理想的,因为:
- 每个
find
都会自己遍历目录树;操作系统中的缓存可能会缓解该问题,但是仍然存在; - 如果在此期间创建了常规文件,则可能会发生这种情况,您会发现常规文件的数量比文件总数还多;每个数字在某种意义上都是正确的在计算时但作为元组,它们没有任何意义。
出于这些原因,人们可能希望单个数字find
能以某种方式给我们两个(或更多)数字。
注意:从现在起,我不再费心停止find .
测试.
自身或进行递归。此外,为了简洁起见,我将在需要时使用不可移植的-printf
(在 GNU 中有效find
);如果您需要可移植的等效物,上述示例就足够了。
此(次优)代码使用一个来计算文件总数和常规文件的数量find
:
find . -printf 'files total\n' \
-type f -printf 'regular files\n' \
| sort | uniq -c
此(次优)代码计算目录的数量、常规文件的数量、符号链接的数量以及最后的其他文件的数量:
find . \( -type d -printf 'directories\n' \) \
-o \( -type f -printf 'regular files\n' \) \
-o \( -type l -printf 'symlinks\n' \) \
-o -printf 'of other types\n' \
| sort | uniq -c
它不是最优的,因为它至少存在三个问题:
- 计数由 执行
uniq -c
,它需要事先sort
。但一般来说,你可以在不排序的情况下对事物进行计数:你看到某种类型的事物,然后增加相应的计数器。如果目录树很大,那么将做很多工作。用一些更适合这项工作的工具来sort
代替会很好。sort | uniq -c
- 行的最终顺序取决于
sort
首先如何对字符串进行排序。您可能更喜欢other types
输出的最后一行,但我们无法轻易影响该顺序。 - 如果根本没有符号链接,那么就不会有说明的行
0 symlinks
。假设您看到2 of other types
并且没有提到符号链接的行。那么很自然地会假设“其他类型”包括符号链接,但事实并非如此。
我们awk
可以解决所有三个问题:
find . \( -type d -printf 'd\n' \) \
-o \( -type f -printf 'f\n' \) \
-o \( -type l -printf 'l\n' \) \
-o -printf 'o\n' \
| awk '
BEGIN {count["d"]=0; count["f"]=0; count["l"]=0; count["o"]=0}
{count[$0]++}
END {
printf("%s directories\n", count["d"])
printf("%s regular files\n", count["f"])
printf("%s symlinks\n", count["l"])
printf("%s of other types\n", count["o"])
print "------"
printf("%s files total\n", count["d"]+count["f"]+count["l"]+count["o"])
}'
现在我们可以控制代码打印的内容。我们甚至可以让它打印类似行N_DIRS=123
和eval
shell 脚本中的输出,这样就可以创建 shell 变量以供稍后在脚本中使用。
注意我过去是如何awk
对四个数字求和的。我可以得出find
此外打印t
(如“总计”)任何单个文件并计算 的出现次数t
,awk
那么我就不需要对 求和了awk
。我的观点是,一般方案非常灵活。基本上是这样的:
find
执行我们选择的测试并打印我们选择的标记(行)。一个文件可能会生成零个、一个或多个标记,具体取决于我们想要什么。显然,你越熟悉find
,你就可以编写越复杂的逻辑而不会出现错误。awk
计算每个标记出现的次数。awk
可能会做额外的计算。awk
使用我们选择的格式打印结果。
更复杂的代码,shell函数
以下函数从不测试.
(因为我认为通常您不想计算当前工作目录)。当以 调用时,它是递归的count -R
,否则是非递归的。如果需要,请摆脱不可移植,-printf
就像我们之前在这个答案中所做的那样。
# should work in sh
count() (
unset IFS
if [ "$1" = -R ]; then
arg=''
else
arg='-prune'
fi
find . ! -name . $arg \( \
\( -type d \( -name '.*' -printf 'dh\n' -o -printf 'dn\n' \) \) \
-o \( -type f \( -name '.*' -printf 'fh\n' -o -printf 'fn\n' \) \) \
-o \( -type l \( -name '.*' -printf 'lh\n' -o -printf 'ln\n' \) \) \
-o \( -name '.*' -printf 'oh\n' -o -printf 'on\n' \) \
\) \
| awk '
BEGIN {
count["dn"]=0; count["dh"]=0
count["fn"]=0; count["fh"]=0
count["ln"]=0; count["lh"]=0
count["on"]=0; count["oh"]=0
}
{ count[$0]++ }
END {
tn=count["dn"]+count["fn"]+count["ln"]+count["on"]
th=count["dh"]+count["fh"]+count["lh"]+count["oh"]
t=tn+th
printf("%9d directories (%9d non-hidden, %9d hidden)\n", count["dn"]+count["dh"], count["dn"], count["dh"])
printf("%9d regular files (%9d non-hidden, %9d hidden)\n", count["fn"]+count["fh"], count["fn"], count["fh"])
printf("%9d symlinks (%9d non-hidden, %9d hidden)\n", count["ln"]+count["lh"], count["ln"], count["lh"])
printf("%9d of other types (%9d non-hidden, %9d hidden)\n", count["on"]+count["oh"], count["on"], count["oh"])
print "-----------------------------------------------------------------"
printf("%9d files total (%9d non-hidden, %9d hidden)\n", t, tn, th)
}'
)
我猜awk
代码不够优雅;而且我不确定它是否完全可移植(我还没有研究过规格彻底地)。
一般来说应该双引号变量类似于$arg
,但这里我们需要$arg
在为空时消失。可能的值是安全的,除非 包含$IFS
字符串 中出现的任何字符-prune
。我故意将函数设计为始终在子 shell 中运行(f() (…)
语法而不是f() {…}
),unset IFS
以确保不带引号的$arg
始终有效。
示例输出:
$ count -R
40130 directories ( 40043 non-hidden, 87 hidden)
363974 regular files ( 362220 non-hidden, 1754 hidden)
6797 symlinks ( 6793 non-hidden, 4 hidden)
25 of other types ( 25 non-hidden, 0 hidden)
-----------------------------------------------------------------
410926 files total ( 409081 non-hidden, 1845 hidden)
答案2
这看起来像是一个家庭作业问题(定义得太明确了)。
我的建议是:
find . -printf %i\\n | wc -l
它不使用“ls”,但这是因为“ls”旨在提供用户可解析的输出,而“find”旨在用于编程。如果您尝试使用“ls”,请问问自己为什么。