Itere sobre una lista de archivos con espacios

Quiero iterar sobre una lista de archivos. Esta lista es el resultado de un comando de find , 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ía 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 maneras factibles de lograr esto.

Si quisiera mantener su versión original, podría hacerlo de la siguiente manera:

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

Esto aún fallará si los nombres de archivo tienen nuevas líneas literales en ellos, pero los espacios no lo romperán.

Sin embargo, jugar con IFS no es necesario. 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 encuentra la syntax < <(command) desconocida, debe leer sobre la sustitución del proceso . La ventaja de esto for file in $(find ...) es que los archivos con espacios, líneas nuevas y otros caracteres se manejan correctamente. Esto funciona porque find con -print0 usará un null (aka \0 ) como terminador para cada nombre de archivo y, a diferencia de la línea nueva, null no es un carácter legal en el nombre de un 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 ciclo while. Es decir, si canalizas hacia arriba while estás arriba, entonces el cuerpo del while está en una subcapa que puede no ser lo que quieres.

La ventaja de la versión de sustitución de proceso sobre find ... -print0 | xargs -0 find ... -print0 | xargs -0 es mínimo: la versión de xargs 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 hacerse una idea de la diferencia entre los diferentes bashs 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: confía 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 shopt, así que iría y diría que debería ser “seguro” (probado en osx y ubuntu).

 find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:" 
 find . -name "fo*" -print0 | xargs -0 ls -l 

Ver man xargs .

Como no está haciendo ningún otro tipo de filtrado con find , puede usar lo siguiente a partir de bash 4.0:

 shopt -s globstar getlist() { for f in **/foo* do echo "File found: $f" # do something useful done } 

**/ coincidirá con cero o más directorios, por lo que el patrón completo coincidirá con foo* en el directorio actual o en cualquier subdirectorio.

Me encantan los bucles y la iteración de matriz, así que creo que agregaré esta respuesta a la mezcla …

También me gustó el estúpido ejemplo de archivo de marchelbling. 🙂

 $ mkdir test $ cd test $ touch "stupid file1" $ touch "stupid file2" $ touch "stupid file 3" 

Dentro del directorio de prueba:

 readarray -t arr <<< "`ls -A1`" 

Esto agrega cada línea de listado de archivos en una matriz de bash llamada arr con cualquier nueva línea final eliminada.

Digamos que queremos dar mejores nombres a estos archivos ...

 for i in ${!arr[@]} do newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/ */_/g'`; mv "${arr[$i]}" "$newname" done 

$ {! arr [@]} se expande a 0 1 2 por lo que "$ {arr [$ i]}" es el elemento i- ésimo de la matriz. Las citas alrededor de las variables son importantes para preservar los espacios.

El resultado es tres archivos renombrados:

 $ ls -1 smarter_file1 smarter_file2 smarter_file_3 

En algunos casos, aquí si solo necesita copiar o mover una lista de archivos, también puede convertir esa lista en awk.
Importante: \"" "\" alrededor del campo $0 (en resumen, sus archivos, una lista de líneas = un archivo).

 find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'