为什么 ls -R 被称为“递归”列表?

Min*_*t.K 35 command-line ls

我知道ls -R显示目录列表。但为什么它是递归的?过程中如何使用递归?

Kaz*_*lfe 66

首先,让我们定义一个任意的文件夹结构:

.
??? a1 [D]
?   ??? b1 [D]
?   ?   ??? c1
?   ?   ??? c2 [D]
?   ?   ?   ??? d1
?   ?   ?   ??? d2
?   ?   ?   ??? d3
?   ?   ??? c3
?   ?   ??? c4
?   ?   ??? c5
?   ??? b2 [D]
?       ??? c6
?       ??? c7
??? a2 [D]
?   ??? b3 [D]
?   ?   ??? c8
?   ?   ??? c9
?   ??? b4 [D]
?       ??? c10 
?       ??? c11
??? a3 [D]
?   ??? b5
?   ??? b6
?   ??? b7
??? a4 [D]
Run Code Online (Sandbox Code Playgroud)

当我们这样做时ls,我们只得到基本文件夹的输出:

a1 a2 a3 a4
Run Code Online (Sandbox Code Playgroud)

然而,当我们调用 时ls -R,我们得到了不同的东西:

.:
a1  a2  a3  a4

./a1:
b1  b2

./a1/b1:
c1  c2  c3  c4  c5

./a1/b1/c2:
d1  d2  d3

./a1/b2:
c6  c7

./a2:
b3  b4

./a2/b3:
c8  c9

./a2/b4:
c10  c11

./a3:
b5  b6  b7

./a4:
Run Code Online (Sandbox Code Playgroud)

如您所见,它ls在基本文件夹上运行,然后在所有子文件夹上运行。以及所有的孙文件夹,无穷无尽。实际上,该命令递归地遍历每个文件夹,直到到达目录树的末尾。此时,它会返回树中的一个分支,并对任何子文件夹(如果有)执行相同的操作。

或者,在伪代码中:

recursivelyList(directory) {
    files[] = listDirectory(directory)              // Get all files in the directory
    print(directory.name + ":\n" + files.join(" ")) // Print the "ls" output
    for (file : files) {                            // Go through all the files in the directory
        if (file.isDirectory()) {                   // Check if the current file being looked at is a directory
            recursivelyList(directory)              // If so, recursively list that directory
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

并且因为我可以,所以引用了同样的Java 实现


Eli*_*gan 23

实际上,您可能会问两个密切相关的问题。

  • 为什么遍历文件系统层次结构中的每个条目的过程本质上是递归过程?这由其他答案解决,例如ZannaKaz Wolfe 的
  • 如何在技术在实现中使用递归的ls?从你的措辞(“过程中如何使用递归?”),我认为这是你想知道的一部分。这个答案解决了这个问题。

为什么ls用递归技术实现是有意义的:

FOLDOC递归定义为:

函数(或过程)调用自身时。这样的函数称为“递归”。如果调用是通过一个或多个其他函数进行的,则这组函数称为“相互递归”。

实现的自然方法ls是编写一个函数来构造要显示的文件系统条目列表,以及其他代码来处理路径和选项参数并根据需要显示条目。该函数极有可能以递归方式实现。

在选项处理期间,ls将确定它是否被要求递归操作(通过使用-R标志调用)。如果是这样,构建要显示的条目列表的函数将针对它列出的每个目录调用一次自身,除了...。此函数可能有单独的递归和非递归版本,或者该函数可能每次都检查它是否应该以递归方式运行。

Ubuntu 的/bin/ls是在您运行时运行的可执行文件,lsGNU Coreutils提供,它具有许多功能。因此,它的代码比您想象的要长一些,也更复杂一些。但是 Ubuntu 还包含一个更简单ls.NET版本,由BusyBox提供。您可以通过键入来运行它busybox ls

如何busybox ls使用递归:

ls在 BusyBox 中实现coreutils/ls.c。它包含一个scan_and_display_dirs_recur()被调用以递归打印目录树的函数:

static void scan_and_display_dirs_recur(struct dnode **dn, int first)
{
    unsigned nfiles;
    struct dnode **subdnp;

    for (; *dn; dn++) {
        if (G.all_fmt & (DISP_DIRNAME | DISP_RECURSIVE)) {
            if (!first)
                bb_putchar('\n');
            first = 0;
            printf("%s:\n", (*dn)->fullname);
        }
        subdnp = scan_one_dir((*dn)->fullname, &nfiles);
#if ENABLE_DESKTOP
        if ((G.all_fmt & STYLE_MASK) == STYLE_LONG || (G.all_fmt & LIST_BLOCKS))
            printf("total %"OFF_FMT"u\n", calculate_blocks(subdnp));
#endif
        if (nfiles > 0) {
            /* list all files at this level */
            sort_and_display_files(subdnp, nfiles);

            if (ENABLE_FEATURE_LS_RECURSIVE
             && (G.all_fmt & DISP_RECURSIVE)
            ) {
                struct dnode **dnd;
                unsigned dndirs;
                /* recursive - list the sub-dirs */
                dnd = splitdnarray(subdnp, SPLIT_SUBDIR);
                dndirs = count_dirs(subdnp, SPLIT_SUBDIR);
                if (dndirs > 0) {
                    dnsort(dnd, dndirs);
                    scan_and_display_dirs_recur(dnd, 0);
                    /* free the array of dnode pointers to the dirs */
                    free(dnd);
                }
            }
            /* free the dnodes and the fullname mem */
            dfree(subdnp);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

递归函数调用发生的那一行是:

                    scan_and_display_dirs_recur(dnd, 0);
Run Code Online (Sandbox Code Playgroud)

看到递归函数调用发生时:

如果您busybox ls在调试器中运行,您可以在运行中看到这一点。首先通过启用 -dbgsym.ddeb 包安装调试符号,然后安装该包。也安装(那是调试器)。busybox-static-dbgsymgdb

sudo apt-get update
sudo apt-get install gdb busybox-static-dbgsym
Run Code Online (Sandbox Code Playgroud)

我建议coreutils ls在一个简单的目录树上调试。

如果您手边没有,请制作一个(这与WinEunuuchs2Unix 的答案中mkdir -p命令的工作方式相同):

mkdir -pv foo/{bar/foobar,baz/quux}
Run Code Online (Sandbox Code Playgroud)

并用一些文件填充它:

(shopt -s globstar; for d in foo/**; do touch "$d/file$((i++))"; done)
Run Code Online (Sandbox Code Playgroud)

您可以busybox ls -R foo按预期验证工作,产生以下输出:

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4
Run Code Online (Sandbox Code Playgroud)

busybox在调试器中打开:

gdb busybox
Run Code Online (Sandbox Code Playgroud)

GDB 会打印一些关于它自己的信息。然后它应该说:

Reading symbols from busybox...Reading symbols from /usr/lib/debug/.build-id/5c/e436575b628a8f54c2a346cc6e758d494c33fe.debug...done.
done.
(gdb)
Run Code Online (Sandbox Code Playgroud)

(gdb)是您在调试器中的提示。在此提示下,您将告诉 GDB 要做的第一件事是在scan_and_display_dirs_recur()函数的开头设置断点:

b scan_and_display_dirs_recur
Run Code Online (Sandbox Code Playgroud)

当你运行它时,GDB 应该告诉你类似的信息:

Breakpoint 1 at 0x5545b4: file coreutils/ls.c, line 1026.
Run Code Online (Sandbox Code Playgroud)

现在告诉GDB运行busybox与(你想或任何目录名)作为它的参数:ls -R foo

run ls -R foo
Run Code Online (Sandbox Code Playgroud)

你可能会看到这样的事情:

Starting program: /bin/busybox ls -R foo

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6c60, first=1) at coreutils/ls.c:1026
1026    coreutils/ls.c: No such file or directory.
Run Code Online (Sandbox Code Playgroud)

如果你确实看到了No such file or directory,如上所述,那没关系。这个演示的目的只是看看scan_and_display_dirs_recur()函数何时被调用,所以 GDB 不需要检查实际的源代码。

请注意,调试器甚至在打印任何目录条目之前就命中了断点。它在该函数的入口处中断,但该函数中的代码必须针对要枚举以进行打印的任何目录运行。

要告诉 GDB 继续,请运行:

c
Run Code Online (Sandbox Code Playgroud)

每次scan_and_display_dirs_recur()调用时,都会再次命中断点,因此您将看到正在运行的递归。它看起来像这样(包括(gdb)提示和您的命令):

(gdb) c
Continuing.
foo:
bar    baz    file0

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cb0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/bar:
file1   foobar

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cf0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/bar/foobar:
file2

foo/baz:
file3  quux

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6cf0, first=first@entry=0) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.

foo/baz/quux:
file4
[Inferior 1 (process 2321) exited normally]
Run Code Online (Sandbox Code Playgroud)

该函数recur的名称中包含... BusyBox 仅在-R给出标志时才使用它吗?在调试器中,这很容易找到:

(gdb) run ls foo
Starting program: /bin/busybox ls foo

Breakpoint 1, scan_and_display_dirs_recur (dn=dn@entry=0x7e6c60, first=1) at coreutils/ls.c:1026
1026    in coreutils/ls.c
(gdb) c
Continuing.
bar    baz    file0
[Inferior 1 (process 2327) exited normally]
Run Code Online (Sandbox Code Playgroud)

即使没有-R,这个特定的实现也ls使用相同的函数来找出存在哪些文件系统条目并显示它们。

当您想退出调试器时,只需告诉它:

q
Run Code Online (Sandbox Code Playgroud)

如何scan_and_display_dirs_recur()知道它是否应该调用自己:

具体来说,-R传递标志时它的工作方式有何不同?检查源代码(可能不是您的 Ubuntu 系统上的确切版本)显示它检查其内部数据结构G.all_fmt,其中存储了调用它的选项:

            if (ENABLE_FEATURE_LS_RECURSIVE
             && (G.all_fmt & DISP_RECURSIVE)
Run Code Online (Sandbox Code Playgroud)

(如果 BusyBox 是在不支持 的情况下编译的-R,那么它也不会尝试递归地显示文件系统条目;这就是该ENABLE_FEATURE_LS_RECURSIVE部分的内容。)

只有当G.all_fmt & DISP_RECURSIVE为真时,包含递归函数调用的代码才会运行。

                struct dnode **dnd;
                unsigned dndirs;
                /* recursive - list the sub-dirs */
                dnd = splitdnarray(subdnp, SPLIT_SUBDIR);
                dndirs = count_dirs(subdnp, SPLIT_SUBDIR);
                if (dndirs > 0) {
                    dnsort(dnd, dndirs);
                    scan_and_display_dirs_recur(dnd, 0);
                    /* free the array of dnode pointers to the dirs */
                    free(dnd);
                }
Run Code Online (Sandbox Code Playgroud)

否则,该函数只运行一次(每个在命令行上指定的目录)。

  • 哦,所以它甚至不是尾递归。这一定意味着存在一些目录内容,列表会由于堆栈溢出而导致busybox崩溃(尽管它会是一个非常深的嵌套)。 (2认同)
  • 这令人震惊。您基本上为 OP 提供了调试方面的快速课程,以便他们能够准确了解事情的工作方式。高超。 (2认同)

Zan*_*nna 16

当您考虑它时,“递归”对于作用于目录及其文件和目录以及它们的文件和目录以及它们的文件和目录以及它们的文件的命令是有意义的.......

.... 直到从指定点向下的整个树都被命令操作过,在这种情况下列出任何子目录的任何子目录的任何子目录的内容..........命令的参数


Win*_*nix 7

-R 用于递归,可以粗略地称为“重复”。

以这段代码为例:

???????????????????????????????????????????????????????????????????????????????
$ mkdir -p temp/a
???????????????????????????????????????????????????????????????????????????????
$ mkdir -p temp/b/1
???????????????????????????????????????????????????????????????????????????????
$ mkdir -p temp/c/1/2
???????????????????????????????????????????????????????????????????????????????
$ ls -R temp
temp:
a  b  c

temp/a:

temp/b:
1

temp/b/1:

temp/c:
1

temp/c/1:
2

temp/c/1/2:
Run Code Online (Sandbox Code Playgroud)

-p制造目录,您可以用质量一个命令创建目录。如果一个或多个中上目录已经存在,这不是错误并且会创建中下目录。

然后ls -R递归地列出从 temp 开始的每个目录,并沿着树向下工作到所有分支。

现在我们来看一个对ls -R命令的补充,即tree命令:

$ tree temp
temp
??? a
??? b
?   ??? 1
??? c
    ??? 1
        ??? 2

6 directories, 0 files
Run Code Online (Sandbox Code Playgroud)

正如你所看到tree的,ls -R除了更简洁,我敢说“更漂亮”。

现在让我们看看如何通过一个简单的命令递归删除我们刚刚创建的目录:

$ rm -r temp
Run Code Online (Sandbox Code Playgroud)

这将递归删除temp其下的所有子目录。即temp/atemp/b/1temp/c/1/2加之间的中间目录。


小智 5

这是一个简单的解释,它是有道理的,因为在显示子目录的内容时,相同的函数已经知道如何处理目录。因此,它只需要在每个子目录上调用自己即可获得该结果!

在伪代码中它看起来像这样:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
Run Code Online (Sandbox Code Playgroud)