计算目录中文件数量的最有效资源的方法是什么?

Mik*_*e B 58 ls shell bash directory

CentOS 5.9

前几天我遇到了一个问题,一个目录有很多文件。数一数,我跑了ls -l /foo/foo2/ | wc -l

原来在一个目录中有超过 100 万个文件(长话短说——根本原因正在得到解决)。

我的问题是:有没有更快的计数方法?获得计数的最有效方法是什么?

Gil*_*il' 65

简短的回答:

\ls -afq | wc -l
Run Code Online (Sandbox Code Playgroud)

(这包括...,所以减去 2。)


当您列出目录中的文件时,可能会发生三种常见情况:

  1. 枚举目录中的文件名。这是不可避免的:如果不枚举它们,就无法计算目录中的文件。
  2. 对文件名进行排序。Shell 通配符和ls命令就是这样做的。
  3. 调用stat以检索有关每个目录条目的元数据,例如它是否是目录。

#3 是迄今为止最昂贵的,因为它需要为每个文件加载一个 inode。相比之下,#1 所需的所有文件名都紧凑地存储在几个块中。#2 浪费了一些 CPU 时间,但它通常不是一个交易破坏者。

如果文件名中没有换行符,一个简单的ls -A | wc -l语句会告诉您目录中有多少文件。请注意,如果您有 的别名ls,这可能会触发对 的调用stat(例如ls --colorls -F需要知道文件类型,这需要调用stat),因此从命令行调用command ls -A | wc -l\ls -A | wc -l避免使用别名。

如果文件名中有换行符,是否列出换行符取决于 Unix 变体。GNU coreutils 和 BusyBox 默认显示?换行符,因此它们是安全的。

调用ls -f以列出条目而不对其进行排序(#2)。这会自动打开-a(至少在现代系统上)。该-f选项在 POSIX 中,但具有可选状态;大多数实现都支持它,但不支持 BusyBox。该选项-q将包括换行符在内的不可打印字符替换为?; 它是 POSIX,但 BusyBox 不支持,所以如果您需要 BusyBox 支持,以过度计算名称包含换行符的文件为代价,请忽略它。

如果目录没有子目录,那么大多数版本find都不会调用stat它的条目(叶目录优化:链接数为 2 的目录不能有子目录,因此find不需要查找条目的元数据,除非条件,如-type需要)。find . | wc -l如果目录没有子目录并且没有文件名包含换行符,那么这是一种可移植的、快速的方法来计算目录中的文件。

如果目录没有子目录但文件名可能包含换行符,请尝试其中之一(如果支持,第二个应该更快,但可能不明显)。

find -print0 | tr -dc \\0 | wc -c
find -printf a | wc -c
Run Code Online (Sandbox Code Playgroud)

另一方面,find如果目录有子目录,则不要使用:甚至find . -maxdepth 1调用stat每个条目(至少使用 GNU find 和 BusyBox find)。您避免了排序(#2),但您付出了 inode 查找(#3)的代价,这会降低性能。

在没有外部工具的 shell 中,您可以使用set -- *; echo $#. 这会遗漏点文件(名称以 开头的文件.)并在空目录中报告 1 而不是 0。这是对小目录中的文件进行计数的最快方法,因为它不需要启动外部程序,但是(zsh 除外)由于排序步骤(#2)而浪费了较大目录的时间。

  • 在 bash 中,这是计算当前目录中文件的可靠方法:

    shopt -s dotglob nullglob
    a=(*)
    echo ${#a[@]}
    
    Run Code Online (Sandbox Code Playgroud)
  • 在 ksh93 中,这是计算当前目录中文件的可靠方法:

    FIGNORE='@(.|..)'
    a=(~(N)*)
    echo ${#a[@]}
    
    Run Code Online (Sandbox Code Playgroud)
  • 在 zsh 中,这是计算当前目录中文件的可靠方法:

    a=(*(DNoN))
    echo $#a
    
    Run Code Online (Sandbox Code Playgroud)

    如果您mark_dirs设置了该选项,请确保将其关闭:a=(*(DNoN^M))

  • 在任何 POSIX shell 中,这是计算当前目录中文件的可靠方法:

    total=0
    set -- *
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    set -- .[!.]*
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    set -- ..?*
    if [ $# -ne 1 ] || [ -e "$1" ] || [ -L "$1" ]; then total=$((total+$#)); fi
    echo "$total"
    
    Run Code Online (Sandbox Code Playgroud)

除了 zsh 之外,所有这些方法都对文件名进行排序。


Joe*_*lor 19

find /foo/foo2/ -maxdepth 1 | wc -l
Run Code Online (Sandbox Code Playgroud)

在我的机器上快得多,但本地.目录被添加到计数中。

  • 添加“-mindepth 1”以省略目录本身。 (3认同)
  • 是的,这是我的理解。只要你不使用 `-type` 参数 `find` 应该比 `ls` 快 (2认同)

Lui*_*uca 8

ls -1U在管道应该花费更少的资源之前,因为它不尝试对文件条目进行排序,它只是在它们在磁盘上的文件夹中排序时读取它们。它还产生较少的输出,这意味着wc.

您也可以使用ls -fwhich 或多或少是ls -1aU.

我不知道是否有一种节省资源的方法可以通过没有管道的命令来做到这一点。

  • 顺便说一句,当输出进入管道时隐含 -1 (8认同)

Tho*_*man 6

另一个比较点。虽然不是 shell oneliner,但这个 C 程序并没有做任何多余的事情。请注意,隐藏文件将被忽略以匹配ls|wc -l(ls -l|wc -l由于第一行输出中的总块数而关闭一个)。

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <error.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    int file_count = 0;
    DIR * dirp;
    struct dirent * entry;

    if (argc < 2)
        error(EXIT_FAILURE, 0, "missing argument");

    if(!(dirp = opendir(argv[1])))
        error(EXIT_FAILURE, errno, "could not open '%s'", argv[1]);

    while ((entry = readdir(dirp)) != NULL) {
        if (entry->d_name[0] == '.') { /* ignore hidden files */
            continue;
        }
        file_count++;
    }
    closedir(dirp);

    printf("%d\n", file_count);
}
Run Code Online (Sandbox Code Playgroud)