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


36

我了解会ls -R显示目录列表。但是为什么要递归呢?在此过程中如何使用递归?


12
直觉是可以使用树轻松地对目录及其子目录进行建模。走树的算法通常是递归的。
凯文-恢复莫妮卡

1
@Kevin我认为不需要调用树的概念来回答每个问题-答案很简单,当ls遇到目录时,它会递归列出该目录。
user253751

Answers:


67

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

.
├── 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]

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

a1 a2 a3 a4

但是,当我们调用时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:

如您所见,它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
        }
    }
}

而且由于可以,因此引用Java的实现也相同。


23

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

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

为什么ls使用递归技术实现它有意义:

FOLDOC递归定义为:

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

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

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

Ubuntu的/bin/ls运行时运行的可执行文件lsGNU Coreutils提供,它具有许多功能。结果,它的代码比您预期的要长一些,也更复杂。但是Ubuntu也包含lsBusyBox提供的一个更简单的版本。您可以通过键入来运行它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);
        }
    }
}

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

                    scan_and_display_dirs_recur(dnd, 0);

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

如果您busybox ls在调试器中运行,则可以在运行中看到它。首先通过启用-dbgsym.ddeb软件包安装调试符号,然后安装该软件包。还要安装(这就是调试器)。busybox-static-dbgsymgdb

sudo apt-get update
sudo apt-get install gdb busybox-static-dbgsym

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

如果您没有方便的话,那就帮个忙(这与WinEunuuchs2Unix的answer中mkdir -p命令的工作方式相同):

mkdir -pv foo/{bar/foobar,baz/quux}

并用一些文件填充它:

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

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

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

busybox在调试器中打开:

gdb busybox

GDB将打印一些有关其自身的信息。然后它应该说像:

Reading symbols from busybox...Reading symbols from /usr/lib/debug/.build-id/5c/e436575b628a8f54c2a346cc6e758d494c33fe.debug...done.
done.
(gdb)

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

b scan_and_display_dirs_recur

运行该代码时,GDB应该告诉您以下信息:

Breakpoint 1 at 0x5545b4: file coreutils/ls.c, line 1026.

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

run ls -R foo

您可能会看到以下内容:

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.

如果您确实看到No such file or directory,如上所述,那没关系。该演示的目的只是为了查看何时scan_and_display_dirs_recur()调用该函数,因此GDB无需检查实际的源代码。

请注意,甚至在打印任何目录条目之前,调试器都会达到断点。它在该函数的entrace上中断,但是必须运行该函数中的代码才能枚举要打印的任何目录。

要告诉GDB继续,请运行:

c

每次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]

该函数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]

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

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

q

如何scan_and_display_dirs_recur()知道它是否应该自称:

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

            if (ENABLE_FEATURE_LS_RECURSIVE
             && (G.all_fmt & DISP_RECURSIVE)

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

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

                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);
                }

否则,该功能仅运行一次(每个命令行上指定的目录)。


Eliah再一次给出了一个非常全面的答案。当之无愧的+1。
卡兹·沃尔夫

2
哦,所以它甚至都不是尾递归。这必须意味着存在一些目录内容,该目录内容将由于堆栈溢出而使busybox崩溃(尽管这是一个非常深的嵌套)。
Ruslan

2
这太惊人了。基本上,您为OP提供了调试方面的快速课程,以便他们可以准确了解事物的工作方式。高超。
Andrea Lazzarotto

16

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

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


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:

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

然后,ls -R递归地列出每个以temp开头的目录,并将其沿目录树向下延伸到所有分支。

现在让我们看一下ls -R命令的补充,即tree命令:

$ tree temp
temp
├── a
├── b
│   └── 1
└── c
    └── 1
        └── 2

6 directories, 0 files

如您所见treels -R除了简明扼要,我敢说“更漂亮”。

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

$ rm -r temp

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


如果“ ls -R”要重复执行某件事那么您将多次获得相同的输出;tree尽管如此,但+1 。这是一个很棒的工具。
波德

是的,外行话语的声音很差。我试图在主流中找到一个词,以使非程序员类型的人更容易理解。我会尝试考虑一个更好的词或稍后删除。
WinEunuuchs2Unix

5

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

用伪代码看起来像这样:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.