Tại sao ls -R được gọi là danh sách đệ quy của hồi giáo?


36

Tôi hiểu rằng ls -Rhiển thị một danh sách các thư mục. Nhưng tại sao nó lại đệ quy? Làm thế nào là đệ quy được sử dụng trong quá trình?


12
Trực giác là các thư mục và thư mục con của chúng có thể được mô hình hóa dễ dàng bằng cách sử dụng cây. Các thuật toán để đi bộ cây thường được đệ quy.
Kevin - Hồi phục lại

1
@Kevin Tôi không nghĩ cần phải gọi khái niệm cây để trả lời từng câu hỏi - câu trả lời đơn giản là khi lsgặp một thư mục, nó sẽ liệt kê đệ quy thư mục đó.
dùng253751

Câu trả lời:


67

Trước hết, hãy xác định cấu trúc thư mục tùy ý:

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

Khi chúng tôi làm ls, chúng tôi chỉ nhận được đầu ra của thư mục cơ sở:

a1 a2 a3 a4

Tuy nhiên, khi chúng tôi gọi ls -R, chúng tôi nhận được một cái gì đó khác nhau:

.:
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:

Như bạn có thể thấy, nó đang chạy lstrên thư mục cơ sở, và sau đó là tất cả các thư mục con. Và tất cả các thư mục cháu, ad infinitum. Thực tế, lệnh sẽ đi qua từng thư mục một cách đệ quy cho đến khi nó chạm vào cuối cây thư mục. Tại thời điểm đó, nó quay trở lại một nhánh trong cây và thực hiện điều tương tự cho bất kỳ thư mục con nào, nếu có.

Hoặc, trong mã giả:

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

Và bởi vì tôi có thể, một triển khai Java tham chiếu giống nhau.


23

Trên thực tế, có hai câu hỏi được kết hợp chặt chẽ mà bạn có thể hỏi.

  • Tại sao quá trình đi bộ đến từng mục trong hệ thống phân cấp tệp hệ thống là một quy trình đệ quy vốn có? Điều này được giải quyết bằng các câu trả lời khác, chẳng hạn như của ZannaKaz Wolfe .
  • Làm thế nào là kỹ thuật đệ quy được sử dụng trong việc thực hiện ls? Từ cụm từ của bạn ("đệ quy được sử dụng như thế nào trong quy trình?"), Tôi nghĩ đây là một phần của những gì bạn muốn biết. Câu trả lời này giải quyết câu hỏi đó.

Tại sao nó có ý nghĩa lsđể được thực hiện với một kỹ thuật đệ quy:

FOLDOC định nghĩa đệ quy là:

Khi một chức năng (hoặc thủ tục ) gọi chính nó. Một chức năng như vậy được gọi là "đệ quy". Nếu cuộc gọi thông qua một hoặc nhiều chức năng khác thì nhóm chức năng này được gọi là "đệ quy lẫn nhau".

Cách tự nhiên để thực hiện lslà viết một hàm xây dựng danh sách các mục hệ thống tệp sẽ được hiển thị và mã khác để xử lý các đối số đường dẫn và tùy chọn và hiển thị các mục theo mong muốn. Chức năng đó rất có thể được thực hiện đệ quy.

Trong quá trình xử lý tùy chọn, lssẽ xác định xem nó có được yêu cầu hoạt động đệ quy không (bằng cách được gọi bằng -Rcờ). Nếu vậy, hàm xây dựng danh sách các mục sẽ được hiển thị sẽ tự gọi một lần cho mỗi thư mục mà nó liệt kê, ngoại trừ .... Có thể có các phiên bản đệ quy và không đệ quy riêng biệt của chức năng này hoặc chức năng có thể kiểm tra mỗi lần nếu nó được cho là hoạt động đệ quy.

Ubuntu /bin/ls, chương trình thực thi chạy khi bạn chạy ls, được cung cấp bởi GNU Coreutils và nó có nhiều tính năng. Do đó, mã của nó có phần dài hơn và phức tạp hơn bạn mong đợi. Nhưng Ubuntu cũng chứa một phiên bản đơn giản hơn ls, được cung cấp bởi BusyBox . Bạn có thể chạy nó bằng cách gõ busybox ls.

Cách busybox lssử dụng đệ quy:

lstrong BusyBox được triển khai trong coreutils/ls.c. Nó chứa một scan_and_display_dirs_recur()hàm được gọi để in một cây thư mục theo cách đệ quy:

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

Dòng nơi diễn ra cuộc gọi hàm đệ quy là:

                    scan_and_display_dirs_recur(dnd, 0);

Xem các hàm gọi đệ quy khi chúng xảy ra:

Bạn có thể thấy điều này trong hoạt động nếu bạn chạy busybox lstrong trình gỡ lỗi. Đầu tiên cài đặt các ký hiệu gỡ lỗi bằng cách kích hoạt các gói -dbgsym.ddeb và sau đó cài đặt busybox-static-dbgsymgói. Cài đặt gdblà tốt (đó là trình gỡ lỗi).

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

Tôi đề nghị gỡ lỗi coreutils lstrên một cây thư mục đơn giản.

Nếu bạn không có tiện ích, hãy tạo một cái (cách này hoạt động giống như mkdir -plệnh trong câu trả lời của WinEunuuchs2Unix ):

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

Và điền vào nó một số tệp:

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

Bạn có thể xác minh busybox ls -R foocông việc như mong đợi, tạo ra đầu ra này:

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

Mở busyboxtrong trình gỡ lỗi:

gdb busybox

GDB sẽ in một số thông tin về chính nó. Sau đó, nó sẽ nói một cái gì đó như:

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

(gdb)là lời nhắc của bạn trong trình gỡ lỗi. Điều đầu tiên bạn sẽ bảo GDB thực hiện trên lời nhắc này là đặt điểm dừng khi bắt đầu scan_and_display_dirs_recur()chức năng:

b scan_and_display_dirs_recur

Khi bạn chạy nó, GDB sẽ cho bạn biết một cái gì đó như:

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

Bây giờ hãy nói với GDB để chạy busyboxvới (hoặc bất kỳ tên thư mục nào bạn muốn) làm đối số của nó:ls -R foo

run ls -R foo

Bạn có thể thấy một cái gì đó như thế này:

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.

Nếu bạn thấy No such file or directory, như trên, thì không sao. Mục đích của trình diễn này là để xem khi nào scan_and_display_dirs_recur()hàm được gọi, vì vậy GDB không cần kiểm tra mã nguồn thực tế.

Lưu ý rằng trình gỡ lỗi đã chạm đến điểm dừng ngay cả trước khi bất kỳ mục nhập thư mục nào được in. Nó phá vỡ trên entrace để chức năng đó, nhưng các mã trong chức năng đó phải chạy cho bất kỳ thư mục được liệt kê để in.

Để báo cho GDB tiếp tục, hãy chạy:

c

Mỗi lần scan_and_display_dirs_recur()được gọi, điểm dừng sẽ được nhấn lại, vì vậy bạn sẽ thấy đệ quy trong hành động. Nó trông như thế này (bao gồm (gdb)lời nhắc và các lệnh của bạn):

(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]

Hàm có recurtên của nó ... BusyBox chỉ sử dụng nó khi -Rcờ được đưa ra? Trong trình gỡ lỗi, điều này rất dễ tìm ra:

(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]

Ngay cả khi không có -R, việc triển khai cụ thể này lssử dụng cùng một chức năng để tìm ra những mục hệ thống tập tin tồn tại và hiển thị chúng.

Khi bạn muốn thoát khỏi trình gỡ lỗi, chỉ cần nói với nó:

q

Làm thế nào để scan_and_display_dirs_recur()biết nếu nó nên gọi chính nó:

Cụ thể nó hoạt động khác nhau như thế nào khi -Rcờ được thông qua? Kiểm tra mã nguồn (có thể không phải là phiên bản chính xác trên hệ thống Ubuntu của bạn) cho thấy rằng nó kiểm tra cấu trúc dữ liệu bên trong của nó G.all_fmt, nơi nó lưu trữ các tùy chọn mà nó đã được gọi với:

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

(Nếu BusyBox đã được biên dịch mà không có hỗ trợ -R, thì nó cũng sẽ không cố gắng hiển thị các mục hệ thống tệp theo cách đệ quy; đó là những gì ENABLE_FEATURE_LS_RECURSIVEphần đó nói về.)

Chỉ khi nào G.all_fmt & DISP_RECURSIVEđúng thì mã chứa hàm gọi đệ quy mới được chạy.

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

Mặt khác, hàm chỉ chạy một lần (mỗi thư mục được chỉ định trên dòng lệnh).


Một lần nữa, Eliah đi qua với một câu trả lời siêu toàn diện. Một +1 rất xứng đáng.
Kaz Wolfe

2
Oh, vì vậy nó thậm chí không đệ quy đuôi. Điều này phải có nghĩa là tồn tại một số nội dung thư mục, danh sách sẽ bị sập busybox do tràn ngăn xếp (mặc dù nó sẽ là một lồng cực kỳ sâu).
Ruslan

2
Điều này thật đáng kinh ngạc. Về cơ bản, bạn đã cung cấp cho OP một bài học nhanh về gỡ lỗi để họ có thể hiểu chính xác cách thức hoạt động của nó. Tuyệt vời.
Andrea Lazzarotto

16

Khi bạn nghĩ về nó, "đệ quy" có ý nghĩa đối với các lệnh hoạt động trên các thư mục và các tệp và thư mục của chúng cũng như các tệp và thư mục của chúng cũng như các tệp và thư mục cũng như các tệp của chúng .........

.... cho đến khi toàn bộ cây từ điểm được chỉ định xuống được vận hành bằng lệnh, trong trường hợp này liệt kê nội dung của bất kỳ thư mục con nào của bất kỳ thư mục con nào của bất kỳ thư mục con nào .......... tồn tại dưới đối số của lệnh


7

-R là để đệ quy, có thể được gọi là "lặp đi lặp lại".

Lấy mã này làm ví dụ:

───────────────────────────────────────────────────────────────────────────────
$ 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:

Việc -ptạo các thư mục cho phép bạn tạo các thư mục hàng loạt bằng một lệnh duy nhất. Nếu một hoặc nhiều thư mục ở giữa đã tồn tại thì đó không phải là lỗi và các thư mục ở giữa thấp hơn được tạo.

Sau đó, ls -Rđệ quy liệt kê tất cả các thư mục duy nhất bắt đầu bằng temp và làm việc theo cách của nó xuống cây cho tất cả các nhánh.

Bây giờ chúng ta hãy xem xét một bổ sung cho ls -Rlệnh, tức là treelệnh:

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

6 directories, 0 files

Như bạn có thể thấy treenhững thành tựu tương tự như ls -Rngoại trừ ngắn gọn hơn và tôi dám nói "xinh hơn".

Bây giờ hãy xem cách loại bỏ đệ quy các thư mục chúng ta vừa tạo trong một lệnh đơn giản:

$ rm -r temp

Điều này đệ quy loại bỏ tempvà tất cả các thư mục con bên dưới nó. tức là temp/a, temp/b/1temp/c/1/2cộng với các thư mục ở giữa.


Nếu "ls -R" phải làm điều gì đó lặp đi lặp lại thì bạn sẽ nhận được cùng một đầu ra nhiều lần;) +1 tree. Đó là một công cụ tuyệt vời.
Pod

Vâng, giọng nói nghèo nàn của giáo dân. Tôi đã cố gắng tìm một từ trong dòng chính để giúp các loại lập trình viên không dễ hiểu hơn. Tôi sẽ cố gắng nghĩ về một từ tốt hơn hoặc xóa sau.
WinEunuuchs2Unix

5

Đây là một lời giải thích đơn giản, nó có ý nghĩa bởi vì khi hiển thị nội dung của các thư mục con, cùng một chức năng đã biết phải làm gì với một thư mục. Vì vậy, nó chỉ cần gọi chính nó trên mỗi thư mục con để có được kết quả đó!

Trong mã giả, nó trông giống như thế này:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.