Чому ls -R називається "рекурсивним" лістингом?


36

Я розумію, що ls -Rвідображається перелік каталогів. Але чому це рекурсивно? Як використовується рекурсія в процесі?


12
Інтуїція полягає в тому, що каталоги та їхні підкаталоги можна легко моделювати за допомогою дерева. Алгоритми ходіння по деревах, як правило, є рекурсивними.
Кевін - Відновіть Моніку

1
@Kevin Я не думаю, що для відповіді на кожне питання не потрібно застосовувати концепцію дерев - відповідь полягає лише в тому, що при lsзустрічі з каталогом він рекурсивно перераховує цей каталог.
користувач253751

Відповіді:


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в базовій папці, а потім у всіх дочірніх папках. І всі папки онука, ad infinitum. Ефективно команда проходить через кожну папку рекурсивно, поки не потрапить у кінець дерева каталогів. У цей момент він повертається до гілки дерева і робить те саме для будь-яких підпапок, якщо такі є.

Або в псевдокоді:

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

Насправді є два тісно пов'язані питання, які ви можете задавати.

  • Чому процес переходу до кожного запису в ієрархії файлової системи є властивим рекурсивним процесом? На це звертаються й інші відповіді, такі як Занна та Каз Вулф .
  • Як застосовується техніка рекурсії при здійсненні ls? З вашого фразування ("Як використовується рекурсія в процесі?"), Я думаю, що це частина того, що ви хочете знати. Ця відповідь стосується цього питання.

Чому це має сенс lsреалізувати за допомогою рекурсивної техніки:

FOLDOC визначає рекурсію як:

Коли функція (або процедура ) викликає себе. Така функція називається "рекурсивною". Якщо виклик здійснюється через одну або декілька інших функцій, то ця група функцій називається "взаємно рекурсивною".

Природним способом реалізації lsє написання функції, яка будує список записів файлової системи, що відображається, та інший код для обробки аргументів шляху та параметрів та відображення записів за бажанням. Ця функція з великою ймовірністю може бути реалізована рекурсивно.

Під час обробки опції lsвизначить, чи було запропоновано діяти рекурсивно (викликаючи -Rпрапор). Якщо це так, то функція , яка створює список записів , які будуть відображатися буде називати себе один раз для кожного каталогу , вона перераховує, за винятком .і ... Можуть існувати окремі рекурсивні та нерекурсивні версії цієї функції, або функція може перевіряти кожен раз, чи передбачається, що вона працює рекурсивно.

Ubuntu's /bin/ls, виконуваний файл, який працює під час запуску ls, надається GNU Coreutils , і він має багато функцій. Як результат, його код дещо довший і складніший, ніж ви могли очікувати. Але Ubuntu також містить більш просту версію ls, надану 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);
        }
    }
}

Рядок, в якому відбувається рекурсивний виклик функції:

                    scan_and_display_dirs_recur(dnd, 0);

Бачачи рекурсивні виклики функції, коли вони відбуваються:

Це можна побачити в роботі, якщо ви працюєте busybox lsв налагоджувачі. Спочатку встановіть символи налагодження , включивши пакети -dbgsym.ddeb, а потім встановітьbusybox-static-dbgsym пакет. Встановіть gdbтакож (це налагоджувач).

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

Я пропоную налагодження coreutils lsна простому дереві каталогів.

Якщо у вас немає однієї зручності, зробіть її (це працює так само, як mkdir -pкоманда у відповіді WinEunuuchs2Unix ):

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 не потрібно вивчати фактичний вихідний код.

Зауважте, що налагоджувач потрапив до точки розриву ще до того, як були надруковані будь-які записи каталогу. Вона порушується на вході до цієї функції, але код у цій функції повинен працювати для будь-яких каталогів, які перераховуються для друку.

Щоб сказати 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це правда, запускається код, що містить рекурсивний виклик функції.

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

В іншому випадку функція працює лише один раз (для каталогу, вказаного в командному рядку).


Знову Елія наштовхується на гіперкрупну відповідь. Заслужений +1.
Каз Вулф

2
О, так це навіть не хвоста рекурсії. Це повинно означати, що існує деякий вміст каталогів, перелік якого вийде з ладу зайнятим через переповнення стека (хоча це буде надзвичайно глибоке вкладення).
Руслан

2
Це вражає. Ви в основному забезпечили ОП швидкий урок налагодження, щоб вони могли зрозуміти, як саме ця справа працює. Прекрасно.
Андреа Лацаротто

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

Як ви бачите, treeвиконує те саме, що, ls -Rкрім того, є більш лаконічним і смію сказати "красивіше".

Тепер давайте розглянемо, як рекурсивно видалити створені нами каталоги в одній простій команді:

$ rm -r temp

Це рекурсивно видаляє tempі всі під-каталоги під ним. тобто temp/a, temp/b/1і temp/c/1/2плюс середні каталоги між ними.


Якби "ls -R" щось робив повторно, тоді ви отримаєте один і той же вихід кілька разів;) +1 для treeхоч. Це чудовий інструмент.
Под

Так бідний голос слова непростого. Я намагався знайти слово в мейнстрімі, що полегшує розуміння непрограмістів. Спробую придумати краще слово або видалити пізніше.
WinEunuuchs2Unix

5

Ось просте пояснення, це має сенс, оскільки, коли мова йде про відображення вмісту підкаталогів, та сама функція вже знає, що робити з каталогом. Тому для отримання цього результату просто потрібно зателефонувати в кожен підкаталог!

У псевдокоді це виглядає приблизно так:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.