Iterar sobre una lista de archivos con espacios

Resuelto gregseth asked hace 13 años • 12 respuestas

Quiero iterar sobre una lista de archivos. Esta lista es el resultado de un findcomando, así que se me ocurrió:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Está bien excepto si un archivo tiene espacios en su nombre:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

¿Qué puedo hacer para evitar la división de espacios?

gregseth avatar Aug 12 '11 17:08 gregseth
Aceptado

Podrías reemplazar la iteración basada en palabras por una basada en líneas:

find . -iname "foo*" | while read f
do
    # ... loop body
done
martin clayton avatar Aug 12 '2011 11:08 martin clayton

Hay varias formas viables de lograr esto.

Si quisieras ceñirte a tu versión original, podrías hacerlo de esta manera:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Esto seguirá fallando si los nombres de los archivos tienen nuevas líneas literales, pero los espacios no lo romperán.

Sin embargo, no es necesario jugar con IFS. Esta es mi forma preferida de hacer esto:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Si la < <(command)sintaxis no le resulta familiar, debería leer acerca de la sustitución de procesos . La ventaja de esto for file in $(find ...)es que los archivos con espacios, nuevas líneas y otros caracteres se manejan correctamente. Esto funciona porque findwith -print0utilizará un null(también conocido como \0) como terminador para cada nombre de archivo y, a diferencia de la nueva línea, nulo no es un carácter legal en un nombre de archivo.

La ventaja de esto sobre la versión casi equivalente.

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

Es que se conserva cualquier asignación de variable en el cuerpo del bucle while. Es decir, si canaliza whilecomo se indicó anteriormente, entonces el cuerpo de whileestá en una subcapa que puede no ser la que desea.

La ventaja de la versión de sustitución de procesos find ... -print0 | xargs -0es mínima: la xargsversión está bien si todo lo que necesita es imprimir una línea o realizar una sola operación en el archivo, pero si necesita realizar varios pasos, la versión de bucle es más fácil.

EDITAR : Aquí hay un buen script de prueba para que pueda tener una idea de la diferencia entre los diferentes intentos de resolver este problema.

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"
sorpigal avatar Aug 12 '2011 11:08 sorpigal

También hay una solución muy simple: confiar en bash globbing

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Tenga en cuenta que no estoy seguro de que este comportamiento sea el predeterminado, pero no veo ninguna configuración especial en mi tienda, por lo que diría que debería ser "seguro" (probado en OSX y Ubuntu).

marchelbling avatar Feb 04 '2014 10:02 marchelbling
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
Karoly Horvath avatar Aug 12 '2011 11:08 Karoly Horvath