使用 Bash 来计数文件的可靠代码(可靠代码)是什么?

使用 Bash 来计数文件的可靠代码(可靠代码)是什么?

正在计数文件...

示例 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可能会对你有用。但是存在一般问题:

  1. 要区分常规文件和符号链接或目录,您需要使用-l并检查drwxr-xr-x或类似的字符串。如果你想同时区分隐藏文件和非隐藏文件(并且你习惯-a打印隐藏文件),那么你需要检查文件名开头是否有一个点。这并不简单,-l因为一个完全不同含义的点可能会出现在行的前面。是的,ls -p可以帮助在没有 的情况下发现目录-l,但这仅适用于目录。 并且 可能ls -F会有所帮助。根据您要计数的文件,需要不同的 选项ls以及不同的 模式grep。这可能会很快变得丑陋。请注意,像这样的方法正是我们在这种情况下所说的“解析”:分析一些可能复杂的结构以获取您需要的信息。这引出了我们的主要问题。
  2. 的输出ls不是为被解析而设计的。它被设计为易于人类阅读。解析ls就像锤击螺丝。在某些情况下,最终结果可能是可以接受的,但锤子不是用来敲螺丝的。
  3. 如果您习惯于在它可以工作的情况下解析输出,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=123evalshell 脚本中的输出,这样就可以创建 shell 变量以供稍后在脚本中使用。

注意我过去是如何awk对四个数字求和的。我可以得出find 此外打印t(如“总计”)任何单个文件并计算 的出现次数tawk那么我就不需要对 求和了awk。我的观点是,一般方案非常灵活。基本上是这样的:

  1. find执行我们选择的测试并打印我们选择的标记(行)。一个文件可能会生成零个、一个或多个标记,具体取决于我们想要什么。显然,你越熟悉find,你就可以编写越复杂的逻辑而不会出现错误。
  2. awk计算每个标记出现的次数。
  3. awk可能会做额外的计算。
  4. 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”,请问问自己为什么。

相关内容