Perché ls -R è chiamato elenco "ricorsivo"?


36

Capisco che ls -Rvisualizza un elenco di directory. Ma perché è ricorsivo? Come viene utilizzata la ricorsione nel processo?


12
L'intuizione è che le directory e le loro sottodirectory possono essere facilmente modellate usando un albero. Gli algoritmi per camminare sugli alberi sono in genere ricorsivi.
Kevin - Ripristina Monica il

1
@Kevin Non penso che sia necessario invocare il concetto di alberi per rispondere a ogni domanda - la risposta è semplicemente che quando lsincontra una directory elenca ricorsivamente quella directory.
user253751

Risposte:


67

Prima di tutto, definiamo una struttura di cartelle arbitraria:

.
├── 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 lo facciamo ls, otteniamo solo l'output della cartella di base:

a1 a2 a3 a4

Tuttavia, quando chiamiamo ls -R, otteniamo qualcosa di diverso:

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

Come puoi vedere, è in esecuzione lssulla cartella di base e quindi su tutte le cartelle secondarie. E tutte le cartelle dei nipoti, all'infinito. In effetti, il comando attraversa ricorsivamente ogni cartella fino a quando non raggiunge la fine dell'albero delle directory. A quel punto, ritorna su un ramo nella struttura e fa la stessa cosa per qualsiasi sottocartella, se presente.

Oppure, in pseudo-codice:

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 poiché posso, un'implementazione Java di riferimento della stessa.


23

Ci sono, in effetti, due domande strettamente collegate che potresti porre.

  • Perché il processo di camminata verso ciascuna voce in una gerarchia di filesystem è un processo intrinsecamente ricorsivo? Questo è affrontato dalle altre risposte, come quelle di Zanna e Kaz Wolfe .
  • Come viene utilizzata la tecnica di ricorsione nell'attuazione di ls? Dal tuo fraseggio ("Come viene utilizzata la ricorsione nel processo?"), Penso che questo sia parte di ciò che vuoi sapere. Questa risposta risponde a questa domanda.

Perché ha senso lsessere implementato con una tecnica ricorsiva:

FOLDOC definisce la ricorsione come:

Quando una funzione (o procedura ) si chiama da sola. Tale funzione è chiamata "ricorsiva". Se la chiamata avviene tramite una o più altre funzioni, questo gruppo di funzioni viene chiamato "reciprocamente ricorsivo".

Il modo naturale di implementare lsè scrivere una funzione che costruisce un elenco di voci del filesystem da visualizzare, e altro codice per elaborare il percorso e gli argomenti delle opzioni e per visualizzare le voci come desiderato. È molto probabile che tale funzione venga implementata in modo ricorsivo.

Durante l'elaborazione dell'opzione, lsdeterminerà se è stato chiesto di operare in modo ricorsivo (invocando il -Rflag). In tal caso, la funzione che crea un elenco di voci da visualizzare si chiamerà una volta per ogni directory che elenca, tranne .e ... Potrebbero esistere versioni separate ricorsive e non ricorsive di questa funzione, oppure la funzione può verificare ogni volta se si suppone che funzioni in modo ricorsivo.

Ubuntu /bin/ls, l'eseguibile che viene eseguito durante l'esecuzione ls, è fornito da GNU Coreutils e ha molte funzionalità. Di conseguenza, il suo codice è un po 'più lungo e più complicato di quanto ci si possa aspettare. Ma Ubuntu contiene anche una versione più semplice di ls, fornita da BusyBox . Puoi eseguirlo digitando busybox ls.

Come busybox lsutilizza la ricorsione:

lsin BusyBox è implementato in coreutils/ls.c. Contiene una scan_and_display_dirs_recur()funzione che viene invocata per stampare ricorsivamente un albero di directory:

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

La linea in cui ha luogo la chiamata di funzione ricorsiva è:

                    scan_and_display_dirs_recur(dnd, 0);

Vedendo la funzione ricorsiva chiama mentre si verificano:

Puoi vederlo in funzione se esegui busybox lsun debugger. In primo luogo installare i simboli di debug da consentendo pacchetti -dbgsym.ddeb e quindi l'installazione del busybox-static-dbgsympacchetto. Installa gdbpure (questo è il debugger).

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

Suggerisco il debug coreutils lssu un semplice albero di directory.

Se non ne hai uno a portata di mano, creane uno (funziona allo stesso modo del mkdir -pcomando nella risposta di WinEunuuchs2Unix ):

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

E popolarlo con alcuni file:

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

Puoi verificare che busybox ls -R foofunzioni come previsto, producendo questo output:

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

Apri busyboxnel debugger:

gdb busybox

GDB stamperà alcune informazioni su se stesso. Quindi dovrebbe dire qualcosa del tipo:

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

(gdb)è il tuo prompt nel debugger. La prima cosa che dirai a GDB di fare su questo prompt è impostare un breakpoint all'inizio della scan_and_display_dirs_recur()funzione:

b scan_and_display_dirs_recur

Quando lo esegui, GDB dovrebbe dirti qualcosa del tipo:

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

Ora dì a GDB di funzionare busyboxcon (o qualunque sia il nome della directory che desideri) come argomenti:ls -R foo

run ls -R foo

Potresti vedere qualcosa del genere:

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 vedi No such file or directory, come sopra, va bene. Lo scopo di questa dimostrazione è solo vedere quando scan_and_display_dirs_recur()è stata chiamata la funzione, quindi GDB non ha bisogno di esaminare il codice sorgente effettivo.

Si noti che il debugger ha raggiunto il punto di interruzione anche prima che tutte le voci della directory fossero stampate. Si interrompe sull'ingresso a quella funzione, ma il codice in quella funzione deve essere eseguito affinché tutte le directory siano enumerate per la stampa.

Per dire a GDB di continuare, esegui:

c

Ogni volta che scan_and_display_dirs_recur()viene chiamato, il punto di interruzione verrà colpito di nuovo, quindi vedrai la ricorsione in azione. Sembra così (incluso il (gdb)prompt e i tuoi comandi):

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

La funzione ha recurnel suo nome ... BusyBox la usa solo quando -Rviene dato il flag? Nel debugger, questo è facile da scoprire:

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

Anche senza -R, questa particolare implementazione di lsusa la stessa funzione per scoprire quali voci del filesystem esistono e mostrarle.

Quando si desidera uscire dal debugger, basta dirlo:

q

Come scan_and_display_dirs_recur()sa se dovrebbe chiamarsi:

In particolare, come funziona in modo diverso quando -Rviene passata la bandiera? Esaminare il codice sorgente (che potrebbe non essere la versione esatta sul tuo sistema Ubuntu) rivela che controlla la sua struttura interna di dati G.all_fmt, dove memorizza le opzioni con cui è stato invocato:

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

(Se BusyBox è stato compilato senza supporto per -R, allora non tenterà nemmeno di visualizzare le voci del filesystem in modo ricorsivo; questo è ciò di cui tratta la ENABLE_FEATURE_LS_RECURSIVEparte.)

Solo quando G.all_fmt & DISP_RECURSIVEè vero viene eseguito il codice che contiene la chiamata di funzione ricorsiva.

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

Altrimenti, la funzione viene eseguita una sola volta (per directory specificata nella riga di comando).


Ancora una volta, Eliah arriva con una risposta iper-completa. Un meritato +1.
Kaz Wolfe,

2
Oh, quindi non è nemmeno ricorsione della coda. Ciò deve significare che esistono alcuni contenuti della directory, un elenco che si arresta in modo anomalo a causa di un overflow dello stack (anche se sarebbe un annidamento estremamente profondo).
Ruslan,

2
Questo è sorprendente. Fondamentalmente hai fornito a OP una rapida lezione sul debug in modo che possano capire esattamente come funziona la cosa. Superba.
Andrea Lazzarotto,

16

Quando ci pensi, "ricorsivo" ha senso per i comandi che agiscono sulle directory e sui loro file e directory e sui loro file e directory e sui loro file e directory e i loro file .........

.... fino a quando l'intero albero dal punto specificato in basso non è stato operato dal comando, in questo caso elencando il contenuto di tutte le sottodirectory di qualsiasi sottodirectory di qualsiasi sottodirectory .......... che esistono sotto il argomento (i) del comando


7

-R è per la ricorsione, che potrebbe essere liberamente definito "ripetutamente".

Prendi questo codice per esempio:

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

Le -pdirectory di creazione consentono di creare in serie directory con un solo comando. Se esiste già una o più delle directory medio-alte non è un errore e vengono create le directory medio-basse.

Quindi ls -Relenca in modo ricorsivo ogni singola directory che inizia con temp e funziona in modo discendente dall'albero verso tutti i rami.

Ora diamo un'occhiata a un complemento del ls -Rcomando, ovvero il treecomando:

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

6 directories, 0 files

Come puoi vedere, treelo stesso che ls -Rtranne è più conciso e oso dire "più bello".

Ora diamo un'occhiata a come rimuovere ricorsivamente le directory che abbiamo appena creato con un semplice comando:

$ rm -r temp

Questo rimuove ricorsivamente tempe tutte le sottodirectory sottostanti. vale a dire temp/a, temp/b/1e in temp/c/1/2più le directory intermedie in mezzo.


Se "ls -R" dovesse fare ripetutamente qualcosa , otterresti lo stesso output più volte;) +1 treeperò. È un ottimo strumento.
Pod

Sì, povera voce della parola laica. Stavo cercando di trovare una parola nel mainstream rendendo più facile la comprensione per i tipi non programmatori. Proverò a pensare a una parola migliore o a eliminarla più tardi.
WinEunuuchs2Unix

5

Ecco una semplice spiegazione, ha senso perché quando si tratta di visualizzare il contenuto delle sottodirectory, la stessa funzione sa già cosa fare con una directory. Pertanto deve solo richiamare se stesso in ogni sottodirectory per ottenere quel risultato!

In pseudocodice sembra qualcosa del genere:

recursive_ls(dir)
    print(files and directories)
    foreach (directoriy in dir)
        recursive_ls(directory)
    end foreach
end recursive_ls
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.