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

36

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

Mint.K
источник
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плюс средние каталоги между ними.

WinEunuuchs2Unix
источник
Если бы «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
TommyD
источник