Por que ls -R é chamado de listagem "recursiva"?

36

Eu entendo que ls -Rexibe uma lista de diretórios. Mas por que é recursivo? Como a recursão é usada no processo?

Mint.K
fonte
12
A intuição é que os diretórios e seus subdiretórios possam ser facilmente modelados usando uma árvore. Algoritmos para andar em árvores são tipicamente recursivos.
Kevin - Restabelece Monica
11
@ Kevin Eu não acho necessário invocar o conceito de árvores para responder a cada pergunta - a resposta é simplesmente que, quando lsencontra um diretório, ele recursivamente lista esse diretório.
precisa saber é o seguinte

Respostas:

67

Primeiro, vamos definir uma estrutura de pastas arbitrária:

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

Quando o fazemos ls, obtemos apenas a saída da pasta base:

a1 a2 a3 a4

No entanto, quando ligamos ls -R, obtemos algo diferente:

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

Como você pode ver, ele está sendo executado lsna pasta base e depois em todas as pastas filho. E todas as pastas de netos, ad infinitum. Efetivamente, o comando percorre cada pasta recursivamente até atingir o final da árvore de diretórios. Nesse ponto, ele retorna uma ramificação na árvore e faz o mesmo para todas as subpastas, se houver.

Ou, no pseudocódigo:

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

E porque eu posso, uma implementação Java de referência do mesmo.

Kaz Wolfe
fonte
23

Com efeito, existem duas perguntas intimamente ligadas que você pode estar fazendo.

  • Por que o processo de caminhar para cada entrada na hierarquia do sistema de arquivos é um processo inerentemente recursivo? Isso é abordado por outras respostas, como as de Zanna e Kaz Wolfe .
  • Como a técnica de recursão é usada na implementação de ls? Do seu fraseado ("Como a recursão é usada no processo?"), Acho que isso faz parte do que você deseja saber. Esta resposta aborda essa questão.

Por que faz sentido lsser implementado com uma técnica recursiva:

O FOLDOC define recursão como:

Quando uma função (ou procedimento ) se chama. Essa função é chamada "recursiva". Se a chamada for realizada por meio de uma ou mais funções, esse grupo de funções será chamado "recursivo".

A maneira natural de implementar lsé escrever uma função que construa uma lista de entradas do sistema de arquivos a serem exibidas e outro código para processar argumentos de caminho e opção e exibir as entradas conforme desejado. É altamente provável que essa função seja implementada recursivamente.

Durante o processamento da opção, lsdeterminará se foi solicitado a operar recursivamente (sendo invocado com o -Rsinalizador). Nesse caso, a função que cria uma lista de entradas a serem exibidas se chamará uma vez para cada diretório listado, exceto .e ... Pode haver versões recursivas e não recursivas separadas dessa função, ou a função pode verificar cada vez se é suposto estar operando recursivamente.

O Ubuntu /bin/ls, o executável que é executado quando você executa ls, é fornecido pelo GNU Coreutils e possui muitos recursos. Como resultado, seu código é um pouco mais longo e mais complicado do que você imagina. Mas o Ubuntu também contém uma versão mais simples ls, fornecida pelo BusyBox . Você pode executar isso digitando busybox ls.

Como busybox lsusa a recursão:

lsno BusyBox é implementado em coreutils/ls.c. Ele contém uma scan_and_display_dirs_recur()função que é chamada para imprimir uma árvore de diretórios recursivamente:

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

A linha onde a chamada de função recursiva ocorre é:

                    scan_and_display_dirs_recur(dnd, 0);

Vendo as chamadas de função recursiva à medida que acontecem:

Você pode ver isso em operação se você executar busybox lsem um depurador. Primeiro instalar os símbolos de depuração por permitindo pacotes -dbgsym.ddeb e, em seguida, instalar o busybox-static-dbgsympacote. Instale gdbtambém (esse é o depurador).

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

Eu sugiro a depuração coreutils lsem uma árvore de diretórios simples.

Se você não tiver um, faça um (isso funciona da mesma maneira que o mkdir -pcomando na resposta do WinEunuuchs2Unix ):

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

E preencha-o com alguns arquivos:

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

Você pode verificar as busybox ls -R fooobras conforme o esperado, produzindo esta saída:

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

Abra busyboxno depurador:

gdb busybox

O GDB imprimirá algumas informações sobre si mesmo. Então deve dizer algo como:

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

(gdb)é o seu prompt no depurador. A primeira coisa que você instruirá o GDB a fazer nesse prompt é definir um ponto de interrupção no início da scan_and_display_dirs_recur()função:

b scan_and_display_dirs_recur

Quando você executa isso, o GDB deve dizer algo como:

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

Agora diga ao GDB para executar busyboxcom (ou qualquer nome de diretório que você deseja) como seus argumentos:ls -R foo

run ls -R foo

Você pode ver algo assim:

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.

Se você vir No such file or directory, como acima, tudo bem. O objetivo desta demonstração é apenas ver quando a scan_and_display_dirs_recur()função foi chamada, para que o GDB não precise examinar o código-fonte real.

Observe que o depurador atingiu o ponto de interrupção antes mesmo de qualquer entrada de diretório ser impressa. Ele interrompe a entrada dessa função, mas o código nessa função deve ser executado para que todos os diretórios sejam enumerados para impressão.

Para dizer ao GDB para continuar, execute:

c

Cada vez que scan_and_display_dirs_recur()é chamado, o ponto de interrupção será atingido novamente, para que você veja a recursão em ação. Parece com isso (incluindo o (gdb)prompt e seus comandos):

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

A função tem recurseu nome ... o BusyBox a usa somente quando a -Rbandeira é fornecida? No depurador, é fácil descobrir:

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

Mesmo sem -Ressa implementação específica, lsa mesma função é usada para descobrir quais entradas do sistema de arquivos existem e mostrá-las.

Quando você quiser sair do depurador, diga-o:

q

Como scan_and_display_dirs_recur()sabe se deve se chamar:

Especificamente, como funciona de maneira diferente quando a -Rbandeira é passada? Examinar o código fonte (que pode não ser a versão exata no seu sistema Ubuntu) revela que ele verifica sua estrutura interna de dados G.all_fmt, onde armazena com quais opções ele foi chamado:

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

(Se o BusyBox tiver sido compilado sem suporte -R, ele também não tentará exibir as entradas do sistema de arquivos recursivamente; é disso que ENABLE_FEATURE_LS_RECURSIVEse trata a parte.)

Somente quando G.all_fmt & DISP_RECURSIVEverdadeiro, o código que contém a chamada de função recursiva é executado.

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

Caso contrário, a função será executada apenas uma vez (por diretório especificado na linha de comandos).

Eliah Kagan
fonte
Mais uma vez, Eliah vem com uma resposta hiper abrangente. Um bem merecido +1.
Kaz Wolfe
2
Ah, então nem é recursão de cauda. Isso deve significar que existe algum conteúdo do diretório, a lista que trava o busybox devido ao estouro de pilha (embora seja um aninhamento extremamente profundo).
Ruslan
2
Isso é surpreendente. Você basicamente forneceu ao OP uma rápida lição sobre depuração, para que eles pudessem entender exatamente como a coisa está funcionando. Soberbo.
Andrea Lazzarotto
16

Quando você pensa sobre isso, "recursivo" faz sentido para comandos que atuam em diretórios e seus arquivos e diretórios e seus arquivos e diretórios e seus arquivos e diretórios e seus arquivos .........

.... até que toda a árvore do ponto especificado para baixo tenha sido operada pelo comando, neste caso, listando o conteúdo de qualquer subdiretório de qualquer subdiretório de qualquer subdiretório .......... que exista sob o comando argumento (s) do comando

Zanna
fonte
7

-R é para recursão, que poderia ser chamada "repetidamente".

Veja este código, por exemplo:

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

Os -pdiretórios em criação permitem criar diretórios em massa com um único comando. Se um ou mais dos diretórios superior-intermediário já existir, não será um erro e os diretórios inferior-médio serão criados.

Em seguida, a ls -Rlista recursiva de todos os diretórios começa com temp e funciona da árvore para todos os ramos.

Agora vamos ver um complemento para o ls -Rcomando, ou seja, o treecomando:

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

6 directories, 0 files

Como você pode ver treerealiza o mesmo que ls -Rexceto é mais conciso e ouso dizer "mais bonita".

Agora vamos ver como remover recursivamente os diretórios que acabamos de criar em um comando simples:

$ rm -r temp

Isso remove recursivamente tempe todos os subdiretórios abaixo dele. ou seja temp/a, temp/b/1e temp/c/1/2mais os diretórios do meio entre os dois.

WinEunuuchs2Unix
fonte
Se "ls -R" fizesse algo repetidamente , você obteria a mesma saída várias vezes;) +1 por isso tree. É uma ótima ferramenta.
Pod
Sim, pobre voz da palavra do leigo. Eu estava tentando encontrar uma palavra no mainstream, facilitando a compreensão de tipos não programadores. Vou tentar pensar em uma palavra melhor ou excluir mais tarde.
WinEunuuchs2Unix 27/01
5

Aqui está uma explicação simples, faz sentido, porque quando se trata de exibir o conteúdo de subdiretórios, a mesma função já sabe o que fazer com um diretório. Portanto, ele só precisa se chamar em cada subdiretório para obter esse resultado!

No pseudocódigo, é algo como isto:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
TommyD
fonte