Iterar sobre una lista de archivos con espacios
Quiero iterar sobre una lista de archivos. Esta lista es el resultado de un find
comando, 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?
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
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 find
with -print0
utilizará 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 while
como se indicó anteriormente, entonces el cuerpo de while
está en una subcapa que puede no ser la que desea.
La ventaja de la versión de sustitución de procesos find ... -print0 | xargs -0
es mínima: la xargs
versió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"
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).
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"