¿Por qué ls -R se denomina listado "recursivo"?

36

Entiendo que ls -Rmuestra una lista de directorios. ¿Pero por qué es recursivo? ¿Cómo se usa la recursión en el proceso?

Mint.K
fuente
12
La intuición es que los directorios y sus subdirectorios se pueden modelar fácilmente usando un árbol. Los algoritmos para caminar por los árboles suelen ser recursivos.
Kevin - Restablece a Mónica el
1
@Kevin No creo que sea necesario invocar el concepto de árboles para responder a cada pregunta: la respuesta es simplemente que cuando lsencuentra un directorio, enumera recursivamente ese directorio.
user253751

Respuestas:

67

En primer lugar, definamos una estructura de carpetas 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]

Cuando lo hacemos ls, solo obtenemos la salida de la carpeta base:

a1 a2 a3 a4

Sin embargo, cuando llamamos ls -R, obtenemos 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 puede ver, se ejecuta lsen la carpeta base y luego en todas las carpetas secundarias. Y todas las carpetas de nietos, hasta el infinito. Efectivamente, el comando pasa por cada carpeta de forma recursiva hasta que llega al final del árbol de directorios. En ese punto, vuelve a subir una rama en el árbol y hace lo mismo para cualquier subcarpeta, si corresponde.

O, en 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
        }
    }
}

Y porque puedo, una implementación Java de referencia de la misma.

Kaz Wolfe
fuente
23

En efecto, hay dos preguntas estrechamente relacionadas que puede estar haciendo.

  • ¿Por qué el proceso de caminar a cada entrada en una jerarquía del sistema de archivos es un proceso inherentemente recursivo? Esto se aborda en las otras respuestas, como las de Zanna y Kaz Wolfe .
  • ¿Cómo se usa la técnica de recursión en la implementación de ls? Por tu fraseo ("¿Cómo se usa la recursividad en el proceso?"), Creo que esto es parte de lo que quieres saber. Esta respuesta aborda esa pregunta.

Por qué tiene sentido lsimplementarse con una técnica recursiva:

FOLDOC define la recursividad como:

Cuando una función (o procedimiento ) se llama a sí misma. Dicha función se llama "recursiva". Si la llamada se realiza a través de una o más funciones, este grupo de funciones se llama "recursivo mutuo".

La forma natural de implementar lses escribir una función que construya una lista de entradas del sistema de archivos para mostrar, y otro código para procesar argumentos de ruta y opciones y para mostrar las entradas como se desee. Es muy probable que esa función se implemente recursivamente.

Durante el procesamiento de opciones, lsdeterminará si se le ha pedido que opere recursivamente (al ser invocado con la -Rbandera). Si es así, la función que crea una lista de entradas para mostrar se llamará a sí misma una vez por cada directorio que enumere, excepto .y ... Puede haber versiones recursivas y no recursivas por separado de esta función, o la función puede verificar cada vez si se supone que está funcionando recursivamente.

Ubuntu's /bin/ls, el ejecutable que se ejecuta cuando se ejecuta ls, es proporcionado por GNU Coreutils , y tiene muchas características. Como resultado, su código es algo más largo y más complicado de lo que cabría esperar. Pero Ubuntu también contiene una versión más simple de ls, proporcionada por BusyBox . Puede ejecutar esto escribiendo busybox ls.

Cómo busybox lsusa la recursividad:

lsen BusyBox se implementa en coreutils/ls.c. Contiene una scan_and_display_dirs_recur()función que se invoca para imprimir un árbol de directorios de forma recursiva:

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 línea donde tiene lugar la llamada a la función recursiva es:

                    scan_and_display_dirs_recur(dnd, 0);

Ver la función recursiva llama a medida que ocurren:

Puede ver esto en funcionamiento si se ejecuta busybox lsen un depurador. En primer lugar instalar los símbolos de depuración por permitir paquetes -dbgsym.ddeb y después de instalar el busybox-static-dbgsympaquete. Instalar gdbtambién (ese es el depurador).

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

Sugiero depurar coreutils lsen un árbol de directorios simple.

Si no tiene uno a mano, haga uno (esto funciona de la misma manera que el mkdir -pcomando en la respuesta de WinEunuuchs2Unix ):

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

Y llenarlo con algunos archivos:

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

Puede verificar los busybox ls -R footrabajos como se esperaba, produciendo esta salida:

foo:
bar    baz    file0

foo/bar:
file1   foobar

foo/bar/foobar:
file2

foo/baz:
file3  quux

foo/baz/quux:
file4

Abrir busyboxen el depurador:

gdb busybox

GDB imprimirá alguna información sobre sí mismo. Entonces debería decir algo como:

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

(gdb)es su aviso en el depurador. Lo primero que le dirá a GDB que haga en este indicador es establecer un punto de interrupción al comienzo de la scan_and_display_dirs_recur()función:

b scan_and_display_dirs_recur

Cuando ejecuta eso, GDB debería decirle algo como:

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

Ahora dígale a GDB que se ejecute busyboxcon (o cualquier nombre de directorio que desee) como sus argumentos:ls -R foo

run ls -R foo

Puede ver algo como esto:

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.

Si ve No such file or directory, como arriba, está bien. El propósito de esta demostración es simplemente ver cuándo scan_and_display_dirs_recur()se ha llamado a la función, por lo que GDB no necesita examinar el código fuente real.

Observe que el depurador alcanzó el punto de interrupción incluso antes de que se imprimieran las entradas de directorio. Se rompe en el entrace a esa función, pero el código de esa función debe funcionar para todos los directorios que se enumeran para imprimir.

Para decirle a GDB que continúe, ejecute:

c

Cada vez que scan_and_display_dirs_recur()se llama, el punto de interrupción se alcanzará nuevamente, por lo que podrá ver la recursión en acción. Se ve así (incluyendo el (gdb)indicador y sus 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]

La función tiene recursu nombre ... ¿BusyBox solo la usa cuando -Rse da la bandera? En el depurador, esto es fácil de descubrir:

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

Incluso sin -R, esta implementación particular de lsutiliza la misma función para averiguar qué entradas del sistema de archivos existen y mostrarlas.

Cuando desee salir del depurador, solo dígale:

q

Cómo scan_and_display_dirs_recur()sabe si debería llamarse a sí mismo:

Específicamente, ¿cómo funciona de manera diferente cuando -Rse pasa la bandera? El examen del código fuente (que puede no ser la versión exacta en su sistema Ubuntu) revela que verifica su estructura de datos interna G.all_fmt, donde almacena las opciones con las que se ha invocado:

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

(Si BusyBox ha sido compilado sin soporte -R, tampoco intentará mostrar las entradas del sistema de archivos de forma recursiva; de eso se trata la ENABLE_FEATURE_LS_RECURSIVEparte).

Solo cuando G.all_fmt & DISP_RECURSIVEes verdadero se ejecuta el código que contiene la llamada de función recursiva.

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

De lo contrario, la función solo se ejecuta una vez (por directorio especificado en la línea de comando).

Eliah Kagan
fuente
Una vez más, Eliah llega con una respuesta hipercompleta. Un merecido +1.
Kaz Wolfe el
2
Oh, entonces ni siquiera es la recursión de la cola. Esto debe significar que existen algunos contenidos del directorio, una lista que bloqueará busybox debido al desbordamiento de la pila (aunque sería un anidamiento extremadamente profundo).
Ruslan
2
Esto es asombroso. Básicamente, proporcionó a OP una lección rápida de depuración para que puedan entender exactamente cómo funciona la cosa. Soberbio.
Andrea Lazzarotto
16

Cuando lo piensa, "recursivo" tiene sentido para los comandos que actúan sobre directorios y sus archivos y directorios y sus archivos y directorios y sus archivos y directorios y sus archivos .........

.... hasta que el comando haya operado todo el árbol desde el punto especificado hacia abajo, en este caso enumerando el contenido de cualquier subdirectorio de cualquier subdirectorio de cualquier subdirectorio .......... que exista bajo el argumento (s) del comando

Zanna
fuente
7

-R es para recursión, que podría llamarse libremente "repetidamente".

Tome este código por ejemplo:

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

La -pcreación de directorios le permite crear directorios en masa con un solo comando. Si ya existen uno o más de los directorios superior-medio, no es un error y se crean los directorios medio-inferior.

Luego, ls -Rrecursivamente enumera todos los directorios, comenzando con temp y trabajando en el árbol hasta todas las ramas.

Ahora veamos un complemento al ls -Rcomando, es decir, el treecomando:

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

6 directories, 0 files

Como puede ver, treelogra lo mismo que ls -Rexcepto es más conciso y me atrevo a decir "más bonito".

Ahora veamos cómo eliminar recursivamente los directorios que acabamos de crear en un comando simple:

$ rm -r temp

Esto elimina de forma recursiva tempy todos los subdirectorios debajo de él. es decir temp/a, temp/b/1y temp/c/1/2más los directorios intermedios en el medio.

WinEunuuchs2Unix
fuente
Si "ls -R" hiciera algo repetidamente , obtendría la misma salida varias veces;) +1 por el momento tree. Es una gran herramienta.
Pod
Sí, pobre voz de la palabra del laico. Estaba tratando de encontrar una palabra en la corriente principal que facilitara la comprensión de los tipos que no son programadores. Trataré de pensar en una palabra mejor o eliminarla más tarde.
WinEunuuchs2Unix
5

Aquí hay una explicación simple, tiene sentido porque cuando se trata de mostrar el contenido de subdirectorios, la misma función ya sabe qué hacer con un directorio. ¡Por lo tanto, solo necesita llamarse a sí mismo en cada subdirectorio para obtener ese resultado!

En pseudocódigo se parece a esto:

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