¿Cómo puedo escapar del espacio en blanco en una lista de bash loop?

121

Tengo un script de shell bash que recorre todos los directorios secundarios (pero no los archivos) de un determinado directorio. El problema es que algunos de los nombres de directorio contienen espacios.

Aquí están los contenidos de mi directorio de prueba:

$ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

Y el código que recorre los directorios:

for f in `find test/* -type d`; do
  echo $f
done

Aquí está la salida:

prueba / Baltimore
prueba / cereza
colina
prueba / Edison 
prueba / nuevo
York
Ciudad
prueba / Filadelfia

Cherry Hill y la ciudad de Nueva York se tratan como 2 o 3 entradas separadas.

Intenté citar los nombres de archivo, así:

for f in `find test/* -type d | sed -e 's/^/\"/' | sed -e 's/$/\"/'`; do
  echo $f
done

pero fue en vano.

Tiene que haber una manera simple de hacer esto.


Las respuestas a continuación son geniales. Pero para hacer esto más complicado, no siempre quiero usar los directorios que figuran en mi directorio de prueba. A veces quiero pasar los nombres de directorio como parámetros de línea de comandos.

Tomé la sugerencia de Charles de establecer el IFS y se me ocurrió lo siguiente:

dirlist="${@}"
(
  [[ -z "$dirlist" ]] && dirlist=`find test -mindepth 1 -type d` && IFS=$'\n'
  for d in $dirlist; do
    echo $d
  done
)

y esto funciona bien a menos que haya espacios en los argumentos de la línea de comando (incluso si se citan esos argumentos). Por ejemplo, llamar al script de esta manera: test.sh "Cherry Hill" "New York City"produce el siguiente resultado:

Cereza
colina
Nuevo
York
Ciudad
MCS
fuente
re: edit, list="$@"descarta por completo la lista de valores originales, colapsando en una cadena. Por favor, siga las prácticas en mi respuesta exactamente como se indica : tal asignación no se recomienda en ningún lugar; Si desea pasar una lista de argumentos de línea de comandos a un programa, debe recopilarlos en una matriz y expandir esa matriz directamente.
Charles Duffy

Respuestas:

105

Primero, no lo hagas así. El mejor enfoque es usar find -execadecuadamente:

# this is safe
find test -type d -exec echo '{}' +

El otro enfoque seguro es usar la lista terminada en NUL, aunque esto requiere que encuentre soporte -print0:

# this is safe
while IFS= read -r -d '' n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d -print0)

También puede llenar una matriz desde find y pasar esa matriz más tarde:

# this is safe
declare -a myarray
while IFS= read -r -d '' n; do
  myarray+=( "$n" )
done < <(find test -mindepth 1 -type d -print0)
printf '%q\n' "${myarray[@]}" # printf is an example; use it however you want

Si su búsqueda no es compatible -print0, su resultado no es seguro: lo siguiente no se comportará como se desea si existen archivos que contienen nuevas líneas en sus nombres (lo cual, sí, es legal):

# this is unsafe
while IFS= read -r n; do
  printf '%q\n' "$n"
done < <(find test -mindepth 1 -type d)

Si uno no va a usar uno de los anteriores, un tercer enfoque (menos eficiente en términos de tiempo y uso de memoria, ya que lee toda la salida del subproceso antes de dividir las palabras) es usar una IFSvariable que no No contiene el carácter de espacio. Desactive globbing ( set -f) para evitar que las cadenas que contienen caracteres glob como [], *o ?se expandan:

# this is unsafe (but less unsafe than it would be without the following precautions)
(
 IFS=$'\n' # split only on newlines
 set -f    # disable globbing
 for n in $(find test -mindepth 1 -type d); do
   printf '%q\n' "$n"
 done
)

Finalmente, para el caso de los parámetros de la línea de comandos, debería usar matrices si su shell los admite (es decir, es ksh, bash o zsh):

# this is safe
for d in "$@"; do
  printf '%s\n' "$d"
done

mantendrá la separación. Tenga en cuenta que la cita (y el uso de en $@lugar de $*) es importante. Las matrices también se pueden completar de otras maneras, como las expresiones globales:

# this is safe
entries=( test/* )
for d in "${entries[@]}"; do
  printf '%s\n' "$d"
done
Charles Duffy
fuente
1
no sabía sobre ese sabor '+' para -exec. dulce
Johannes Schaub - litb
1
Parece que también puede, como xargs, solo poner los argumentos al final del comando dado: / eso me molesta a veces
Johannes Schaub - litb
Creo que -exec [name] {} + es una extensión GNU y 4.4-BSD. (Al menos, no aparece en Solaris 8, y tampoco creo que estuviera en AIX 4.3.) Supongo que el resto de nosotros podríamos estar atascados con tuberías a xargs ...
Michael Ratanapintha
2
Nunca he visto la sintaxis $ '\ n' antes. ¿Cómo funciona? (Pensé que IFS = '\ n' o IFS = "\ n" funcionarían, pero tampoco lo hace.)
MCS
1
@crosstalk definitivamente está en Solaris 10, lo acabo de usar.
Nick
26
find . -type d | while read file; do echo $file; done

Sin embargo, no funciona si el nombre del archivo contiene nuevas líneas. Lo anterior es la única solución que conozco cuando realmente desea tener el nombre del directorio en una variable. Si solo quieres ejecutar algún comando, usa xargs.

find . -type d -print0 | xargs -0 echo 'The directory is: '
Johannes Schaub - litb
fuente
No hay necesidad de xargs, consulte find -exec ... {} +
Charles Duffy el
44
@Charles: para grandes cantidades de archivos, xargs es mucho más eficiente: solo genera un proceso. La opción -exec bifurca un nuevo proceso para cada archivo, que puede ser un orden de magnitud más lento.
Adam Rosenfield
1
Me gustan más los xargs. Esencialmente, estos dos parecen hacer lo mismo, mientras que xargs tiene más opciones, como correr en paralelo
Johannes Schaub - litb
2
Adam, no, ese '+' agregará tantos nombres de archivos como sea posible y luego se ejecutará. pero no tendrá funciones tan ordenadas como ejecutarse en paralelo :)
Johannes Schaub - litb
2
Tenga en cuenta que si desea hacer algo con los nombres de archivo, tendrá que citarlos. Por ejemplo:find . -type d | while read file; do ls "$file"; done
David Moles
23

Aquí hay una solución simple que maneja pestañas y / o espacios en blanco en el nombre del archivo. Si tiene que lidiar con otros caracteres extraños en el nombre del archivo como líneas nuevas, elija otra respuesta.

El directorio de prueba

ls -F test
Baltimore/  Cherry Hill/  Edison/  New York City/  Philadelphia/  cities.txt

El código para ir a los directorios.

find test -type d | while read f ; do
  echo "$f"
done

El nombre del archivo debe ser entrecomillado ( "$f") si se usa como argumento. Sin comillas, los espacios actúan como separador de argumentos y se dan múltiples argumentos al comando invocado.

Y la salida:

test/Baltimore
test/Cherry Hill
test/Edison
test/New York City
test/Philadelphia
cbliard
fuente
gracias, esto funcionó para el alias que estaba creando para enumerar cuánto espacio está usando cada directorio en la carpeta actual, se estaba ahogando en algunos directorios con espacios en la encarnación anterior. Esto funciona en zsh, pero algunas de las otras respuestas no:alias duc='ls -d * | while read D; do du -sh "$D"; done;'
Ted Naleid
2
Si está utilizando zsh, también puede hacer esto:alias duc='du -sh *(/)'
cbliard
@cbliard Esto todavía tiene errores. Intente ejecutarlo con un nombre de archivo con, digamos, una secuencia de tabulación o múltiples espacios; notarás que cambia cualquiera de esos a un solo espacio, porque no estás citando en tu eco. Y luego está el caso de los nombres de archivo que contienen nuevas líneas ...
Charles Duffy
@CharlesDuffy Lo intenté con secuencias de pestañas y múltiples espacios. Funciona con comillas. También probé con líneas nuevas y no funciona en absoluto. Actualicé la respuesta en consecuencia. Gracias por señalar esto.
cbliard
1
@cbliard Derecha: agregar comillas a su comando echo era lo que estaba buscando. En cuanto a las nuevas líneas, puede hacer que funcione utilizando find -print0y IFS='' read -r -d '' f.
Charles Duffy
7

Esto es extremadamente complicado en Unix estándar, y la mayoría de las soluciones funcionan con nuevas líneas o algún otro personaje. Sin embargo, si está utilizando el conjunto de herramientas GNU, puede explotar la findopción -print0y utilizarla xargscon la opción correspondiente -0(menos cero). Hay dos caracteres que no pueden aparecer en un nombre de archivo simple; esos son barra oblicua y NUL '\ 0'. Obviamente, la barra diagonal aparece en los nombres de ruta, por lo que la solución GNU de usar un NUL '\ 0' para marcar el final del nombre es ingeniosa y a prueba de tontos.

Jonathan Leffler
fuente
4

¿Por qué no simplemente poner

IFS='\n'

en frente del comando? Esto cambia el separador de campo de <Espacio> <Pestaña> <Nueva línea> a solo <Nueva línea>

oshunluvr
fuente
4
find . -print0|while read -d $'\0' file; do echo "$file"; done
Freakus
fuente
1
-d $'\0'es exactamente lo mismo que -d ''- porque bash usa cadenas terminadas en NUL, el primer carácter de una cadena vacía es un NUL, y por la misma razón, los NUL no pueden representarse dentro de las cadenas C en absoluto.
Charles Duffy
4

yo suelo

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for f in $( find "$1" -type d ! -path "$1" )
do
  echo $f
done
IFS=$SAVEIFS

¿No sería eso suficiente?
Idea tomada de http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html

murpel
fuente
gran consejo: es muy útil para las opciones de un osascript de línea de comandos (OS X AppleScript), donde los espacios dividen un argumento en múltiples parámetros donde solo se pretende uno
tim
No, no es suficiente. Es ineficiente (debido al uso innecesario de $(echo ...)), no maneja correctamente los nombres de archivo con expresiones globales, no maneja correctamente los nombres de archivo que contienen $'\b'o $ '\ n' caracteres, y además convierte varias ejecuciones de espacios en blanco en caracteres de espacios en blanco únicos en el lado de salida debido a una cita incorrecta.
Charles Duffy
4

No almacene listas como cadenas; guárdelos como matrices para evitar toda esta confusión del delimitador. Aquí hay un script de ejemplo que funcionará en todos los subdirectorios de prueba o en la lista provista en su línea de comando:

#!/bin/bash
if [ $# -eq 0 ]; then
        # if no args supplies, build a list of subdirs of test/
        dirlist=() # start with empty list
        for f in test/*; do # for each item in test/ ...
                if [ -d "$f" ]; then # if it's a subdir...
                        dirlist=("${dirlist[@]}" "$f") # add it to the list
                fi
        done
else
        # if args were supplied, copy the list of args into dirlist
        dirlist=("$@")
fi
# now loop through dirlist, operating on each one
for dir in "${dirlist[@]}"; do
        printf "Directory: %s\n" "$dir"
done

Ahora intentemos esto en un directorio de prueba con una curva o dos:

$ ls -F test
Baltimore/
Cherry Hill/
Edison/
New York City/
Philadelphia/
this is a dirname with quotes, lfs, escapes: "\''?'?\e\n\d/
this is a file, not a directory
$ ./test.sh 
Directory: test/Baltimore
Directory: test/Cherry Hill
Directory: test/Edison
Directory: test/New York City
Directory: test/Philadelphia
Directory: test/this is a dirname with quotes, lfs, escapes: "\''
'
\e\n\d
$ ./test.sh "Cherry Hill" "New York City"
Directory: Cherry Hill
Directory: New York City
Gordon Davisson
fuente
1
Mirando hacia atrás en esto, en realidad había una solución con POSIX sh: podría reutilizar la "$@"matriz y agregarla con set -- "$@" "$f".
Charles Duffy
4

Puede usar IFS (separador de campo interno) temporalmente usando:

OLD_IFS=$IFS     # Stores Default IFS
IFS=$'\n'        # Set it to line break
for f in `find test/* -type d`; do
    echo $f
done

$IFS=$OLD_IFS

asombroso
fuente
Por favor proporcione una explicación.
Steve K
IFS especificó cuál es el símbolo separador, luego el nombre del archivo con espacio en blanco no se truncaría.
Increíble allí el
$ IFS = $ OLD_IFS al final debería ser: IFS = $ OLD_IFS
Michel Donais
3

ps si solo se trata de espacio en la entrada, entonces algunas comillas dobles funcionaron sin problemas para mí ...

read artist;

find "/mnt/2tb_USB_hard_disc/p_music/$artist" -type f -name *.mp3 -exec mpg123 '{}' \;
duro pero no
fuente
2

Para agregar a lo que dijo Jonathan : use la -print0opción findjunto con xargslo siguiente:

find test/* -type d -print0 | xargs -0 command

Eso ejecutará el comando commandcon los argumentos adecuados; los directorios con espacios en ellos se citarán correctamente (es decir, se pasarán como un argumento).

Adam Rosenfield
fuente
1
#!/bin/bash

dirtys=()

for folder in *
do    
 if [ -d "$folder" ]; then    
    dirtys=("${dirtys[@]}" "$folder")    
 fi    
done    

for dir in "${dirtys[@]}"    
do    
   for file in "$dir"/\*.mov   # <== *.mov
   do    
       #dir_e=`echo "$dir" | sed 's/[[:space:]]/\\\ /g'`   -- This line will replace each space into '\ '   
       out=`echo "$file" | sed 's/\(.*\)\/\(.*\)/\2/'`     # These two line code can be written in one line using multiple sed commands.    
       out=`echo "$out" | sed 's/[[:space:]]/_/g'`    
       #echo "ffmpeg -i $out_e -sameq -vcodec msmpeg4v2 -acodec pcm_u8 $dir_e/${out/%mov/avi}"    
       `ffmpeg -i "$file" -sameq -vcodec msmpeg4v2 -acodec pcm_u8 "$dir"/${out/%mov/avi}`    
   done    
done

El código anterior convertirá los archivos .mov a .avi. Los archivos .mov están en diferentes carpetas y los nombres de las carpetas también tienen espacios en blanco . Mi script anterior convertirá los archivos .mov a archivos .avi en la misma carpeta. No sé si les ayuda a los pueblos.

Caso:

[sony@localhost shell_tutorial]$ ls
Chapter 01 - Introduction  Chapter 02 - Your First Shell Script
[sony@localhost shell_tutorial]$ cd Chapter\ 01\ -\ Introduction/
[sony@localhost Chapter 01 - Introduction]$ ls
0101 - About this Course.mov   0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ ./above_script
 ... successfully executed.
[sony@localhost Chapter 01 - Introduction]$ ls
0101_-_About_this_Course.avi  0102_-_Course_Structure.avi
0101 - About this Course.mov  0102 - Course Structure.mov
[sony@localhost Chapter 01 - Introduction]$ CHEERS!

¡Salud!

Sony George
fuente
echo "$name" | ...no funciona si namees así -n, y cómo se comporta con nombres con secuencias de escape de barra diagonal inversa depende de su implementación: POSIX hace que el comportamiento echoen ese caso sea explícitamente indefinido (mientras que POSIX extendido con XSI hace que la expansión de secuencias de escape de barra diagonal inversa sea un comportamiento definido de forma estándar y sistemas GNU - incluyendo fiesta - sin POSIXLY_CORRECT=1ruptura del estándar POSIX mediante la implementación -e(mientras que la especificación requiere echo -epara imprimir -e.) en la salida printf '%s\n' "$name" | ...es más seguro.
Charles Duffy
1

También tenía que tratar con espacios en blanco en los nombres de ruta. Lo que finalmente hice fue usar una recursión y for item in /path/*:

function recursedir {
    local item
    for item in "${1%/}"/*
    do
        if [ -d "$item" ]
        then
            recursedir "$item"
        else
            command
        fi
    done
}
Gilles 'SO- deja de ser malvado'
fuente
1
No use la functionpalabra clave: hace que su código sea incompatible con POSIX sh, pero no tiene otro propósito útil. Puede definir una función con recursedir() {, agregando los dos elementos parentales y eliminando la palabra clave de función, y esto será compatible con todos los shells compatibles con POSIX.
Charles Duffy
1

Convierta la lista de archivos en una matriz Bash. Utiliza el enfoque de Matt McClure para devolver una matriz de una función Bash: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html El resultado es un camino para convertir cualquier entrada de varias líneas en una matriz Bash.

#!/bin/bash

# This is the command where we want to convert the output to an array.
# Output is: fileSize fileNameIncludingPath
multiLineCommand="find . -mindepth 1 -printf '%s %p\\n'"

# This eval converts the multi-line output of multiLineCommand to a
# Bash array. To convert stdin, remove: < <(eval "$multiLineCommand" )
eval "declare -a myArray=`( arr=(); while read -r line; do arr[${#arr[@]}]="$line"; done; declare -p arr | sed -e 's/^declare -a arr=//' ) < <(eval "$multiLineCommand" )`"

for f in "${myArray[@]}"
do
   echo "Element: $f"
done

Este enfoque parece funcionar incluso cuando hay caracteres incorrectos, y es una forma general de convertir cualquier entrada a una matriz Bash. La desventaja es que si la entrada es larga, podría exceder los límites de tamaño de la línea de comandos de Bash o utilizar grandes cantidades de memoria.

Los enfoques en los que el ciclo que finalmente está funcionando en la lista también tiene la lista distribuida tienen la desventaja de que leer stdin no es fácil (como pedirle entrada al usuario), y el ciclo es un proceso nuevo, por lo que puede preguntarse por qué las variables que establezca dentro del bucle no estarán disponibles después de que finalice el bucle.

También no me gusta configurar IFS, puede estropear otro código.

Steve Zobell
fuente
Si usa IFS='' read, en la misma línea, la configuración de IFS está presente solo para el comando de lectura y no se escapa. No hay razón para que no le guste configurar IFS de esta manera.
Charles Duffy
1

Bueno, veo demasiadas respuestas complicadas. No quiero pasar la salida de la utilidad find o escribir un bucle, porque find tiene la opción "exec" para esto.

Mi problema era que quería mover todos los archivos con extensión dbf a la carpeta actual y algunos de ellos contenían espacios en blanco.

Lo abordé así:

 find . -name \*.dbf -print0 -exec mv '{}'  . ';'

Me parece muy simple

Tebe
fuente
0

Acabo de descubrir que hay algunas similitudes entre mi pregunta y la tuya. Aparentemente si quieres pasar argumentos a comandos

test.sh "Cherry Hill" "New York City"

para imprimirlos en orden

for SOME_ARG in "$@"
do
    echo "$SOME_ARG";
done;

observe que $ @ está rodeado de comillas dobles, algunas notas aquí

Jeffrey04
fuente
0

Necesitaba el mismo concepto para comprimir secuencialmente varios directorios o archivos de una carpeta determinada. He resuelto usar awk para analizar la lista de ls y evitar el problema del espacio en blanco en el nombre.

source="/xxx/xxx"
dest="/yyy/yyy"

n_max=`ls . | wc -l`

echo "Loop over items..."
i=1
while [ $i -le $n_max ];do
item=`ls . | awk 'NR=='$i'' `
echo "File selected for compression: $item"
tar -cvzf $dest/"$item".tar.gz "$item"
i=$(( i + 1 ))
done
echo "Done!!!"

¿Qué piensas?

Hìr0
fuente
Creo que esto no funcionará correctamente si los nombres de archivo tienen nuevas líneas en ellos. Quizás deberías probarlo.
user000001
0
find Downloads -type f | while read file; do printf "%q\n" "$file"; done
Johan Kasselman
fuente
-3

Para mí esto funciona, y es bastante "limpio":

for f in "$(find ./test -type d)" ; do
  echo "$f"
done
AndrzejP
fuente
44
Pero esto es peor. Las comillas dobles alrededor de la búsqueda hacen que todos los nombres de ruta se concatenen como una sola cadena. Cambie el eco a un ls para ver el problema.
NVRAM
-4

Acabo de tener un problema de variante simple ... Convertir archivos de .flv mecanografiado a .mp3 (bostezar).

for file in read `find . *.flv`; do ffmpeg -i ${file} -acodec copy ${file}.mp3;done

encuentre de forma recursiva todos los archivos flash de usuario de Macintosh y conviértalos en audio (copia, sin transcodificación) ... es como en el momento anterior, señalando que la lectura en lugar de solo 'for file in ' se escapará.

Mark Washeim
fuente
2
El readdespués ines una palabra más en la lista sobre la que estás iterando. Lo que has publicado es una versión ligeramente rota de lo que tenía el autor de la pregunta, que no funciona. Es posible que haya tenido la intención de publicar algo diferente, pero probablemente esté cubierto por otras respuestas aquí de todos modos.
Gilles 'SO- deja de ser malvado'