Почему ls -R называется «рекурсивным» листингом?


36

Я так понимаю, что ls -Rотображает список каталогов. Но почему это рекурсивно? Как в процессе используется рекурсия?


12
Интуиция заключается в том, что каталоги и их подкаталоги могут быть легко смоделированы с использованием дерева. Алгоритмы обхода деревьев обычно являются рекурсивными.
— Кевин - Восстановить Монику

1
@Kevin Я не думаю, что есть необходимость вызывать концепцию деревьев для ответа на каждый вопрос - ответ прост: когда он lsвстречает каталог, он рекурсивно перечисляет этот каталог.
— user253751

Ответы:


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

По сути, вы можете задать два тесно связанных вопроса.

  • Почему процесс перехода к каждой записи в иерархии файловой системы является по своей сути рекурсивным процессом? Это решается другими ответами, такими как Занна и Каз Вулф .
  • Как техника рекурсии используется при реализации ls? Из вашей фразы («Как рекурсия используется в процессе?») Я думаю, что это часть того, что вы хотите знать. Этот ответ отвечает на этот вопрос.

Почему имеет смысл lsбыть реализованным с помощью рекурсивной техники:

FOLDOC определяет рекурсию как:

Когда функция (или процедура ) вызывает себя. Такая функция называется «рекурсивная». Если вызов осуществляется с помощью одной или нескольких других функций, эта группа функций называется «взаимно рекурсивной».

Естественным способом реализации lsявляется написание функции, которая создает список записей файловой системы для отображения и другой код для обработки аргументов пути и параметров и для отображения записей по желанию. Скорее всего, эта функция будет реализована рекурсивно.

Во время обработки опции lsопределит, было ли ей предложено работать рекурсивно (вызывается с -Rфлагом). Если это так, функция, которая создает список записей для отображения, будет вызывать себя один раз для каждого каталога, который она перечисляет, кроме .и ... Там могут быть отдельные рекурсивные и нерекурсивные версии этой функции, или функция может проверять каждый раз, если она должна работать рекурсивно.

Ubuntu /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_RECURSIVEtrue, код, содержащий рекурсивный вызов функции, запускается.

                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
О, так что это даже не хвостовая рекурсия. Это должно означать, что существует некоторое содержимое каталога, перечисление которого приведет к сбою busybox из-за переполнения стека (хотя это будет чрезвычайно глубокая вложенность).
— Руслан

2
Это поразительно. В основном вы предоставили OP быстрый урок по отладке, чтобы они могли точно понять, как это работает. Superb.
— Андреа

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.