Bash array con espacios en elementos

150

Estoy tratando de construir una matriz en bash de los nombres de archivo de mi cámara:

FILES=(2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg)

Como puede ver, hay un espacio en el medio de cada nombre de archivo.

Intenté poner cada nombre entre comillas y escapar del espacio con una barra diagonal inversa, ninguno de los cuales funciona.

Cuando intento acceder a los elementos de la matriz, continúa tratando el espacio como el elemento delimitador.

¿Cómo puedo capturar correctamente los nombres de archivo con un espacio dentro del nombre?

abelenky
fuente
¿Has intentado agregar los archivos a la antigua? Al igual que FILES[0] = ...? (Editar: acabo de hacerlo; no funciona. Interesante).
Dan Fego el
POSIX: stackoverflow.com/questions/2936922/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Todas las respuestas aquí se descomponen para mí usando Cygwin. Hace cosas raras si hay espacios en los nombres de archivo, punto. Lo soluciono creando una "matriz" en una lista de archivos de texto de todos los elementos con los que quiero trabajar, e iterando sobre líneas en el archivo: El formateo es muck con los backticks deseados aquí que rodean el comando entre paréntesis: IFS = ""; matriz = ( find . -maxdepth 1 -type f -iname \*.$1 -printf '%f\n'); para el elemento en $ {array [@]}; hacer echo $ elemento; hecho
Alex Hall

Respuestas:

121

Creo que el problema podría deberse en parte a cómo está accediendo a los elementos. Si hago un simple for elem in $FILES, experimento el mismo problema que tú. Sin embargo, si accedo a la matriz a través de sus índices, funciona así si agrego los elementos numéricamente o con escapes:

for ((i = 0; i < ${#FILES[@]}; i++))
do
    echo "${FILES[$i]}"
done

Cualquiera de estas declaraciones de $FILESdebería funcionar:

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

o

FILES=("2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg")

o

FILES[0]="2011-09-04 21.43.02.jpg"
FILES[1]="2011-09-05 10.23.14.jpg"
FILES[2]="2011-09-09 12.31.16.jpg"
FILES[3]="2011-09-11 08.43.12.jpg"
Dan Fego
fuente
66
Tenga en cuenta que debe usar comillas dobles cuando usa los elementos de la matriz (por ejemplo echo "${FILES[$i]}"). No importa echo, pero lo será para cualquier cosa que lo use como nombre de archivo.
Gordon Davisson el
26
No es necesario recorrer los índices cuando puede recorrer los elementos con for f in "${FILES[@]}".
Mark Edgar
10
@MarkEdgar Estoy experimentando problemas con for f en $ {FILES [@]} cuando los miembros de la matriz tienen espacios. Parece que toda la matriz se reinterpreta nuevamente, con los espacios escupiendo a los miembros existentes en dos o más elementos. Parece que los "" son muy importantes
Michael Shaw
1
¿Qué hace el #símbolo sharp ( ) en la for ((i = 0; i < ${#FILES[@]}; i++))declaración?
Michal Vician
44
Respondí esto hace seis años, pero creo que es para contar el número de elementos en la matriz ARCHIVOS.
Dan Fego
91

Debe haber algo mal con la forma en que accede a los elementos de la matriz. Así es como se hace:

for elem in "${files[@]}"
...

Desde la página de manual de bash :

Se puede hacer referencia a cualquier elemento de una matriz usando $ {name [subíndice]}. ... Si el subíndice es @ o *, la palabra se expande a todos los miembros de nombre. Estos subíndices difieren solo cuando la palabra aparece entre comillas dobles. Si la palabra está entre comillas dobles, $ {name [*]} se expande a una sola palabra con el valor de cada miembro de la matriz separado por el primer carácter de la variable especial IFS, y $ {name [@]} expande cada elemento de nombre a una palabra separada .

Por supuesto, también debe usar comillas dobles al acceder a un solo miembro

cp "${files[0]}" /tmp
usuario123444555621
fuente
3
La solución más limpia y elegante de este grupo, aunque debería reiterar que cada elemento definido en la matriz debe ser citado.
rebelde
Si bien la respuesta de Dan Fego es efectiva, esta es la forma más idiomática de manejar espacios en los elementos.
Daniel Zhang
3
Viniendo de otros lenguajes de programación, la terminología de ese extracto es realmente difícil de entender. Además, la sintaxis es desconcertante. Estaría extremadamente agradecido si pudieras profundizar un poco más. Particularmenteexpands to a single word with the value of each array member separated by the first character of the IFS special variable
CL22
1
Sí, estoy de acuerdo en que las comillas dobles lo resuelven y esto es mejor que otras soluciones. Para explicar más, la mayoría de los demás simplemente carecen de comillas dobles. Obtuviste lo correcto: for elem in "${files[@]}"mientras lo hayan hecho for elem in ${files[@]}, por lo que los espacios confunden la expansión y los intentos que se ejecutan en las palabras individuales.
arntg
Esto no me funciona en macOS 10.14.4, que usa "GNU bash, versión 3.2.57 (1) -release (x86_64-apple-darwin18)". Tal vez un error en la versión anterior de bash?
Mark Ribau
43

Debe usar IFS para detener el espacio como delimitador de elementos.

FILES=("2011-09-04 21.43.02.jpg"
       "2011-09-05 10.23.14.jpg"
       "2011-09-09 12.31.16.jpg"
       "2011-09-11 08.43.12.jpg")
IFS=""
for jpg in ${FILES[*]}
do
    echo "${jpg}"
done

Si quieres separar en base a. entonces simplemente haga IFS = "." Espero que te ayude :)

Khushneet
fuente
3
Tuve que mover el IFS = "" antes de la asignación de matriz, pero esta es la respuesta correcta.
robar
Estoy usando varias matrices para analizar la información y tendré el efecto de que IFS = "" solo funcione en una de ellas. Una vez que uso IFS = "", todas las demás matrices dejan de analizarse en consecuencia. ¿Alguna pista sobre esto?
Paulo Pedroso
Paulo, mira otra respuesta aquí que puede ser mejor para tu caso: stackoverflow.com/a/9089186/1041319 . No he probado IFS = "", y parece que lo resuelve con elegancia, pero su ejemplo muestra por qué uno puede encontrar problemas en algunos casos. Es posible establecer el IFS = "" en una sola línea, pero aún puede ser más confuso que la otra solución.
arntg
También me funcionó en bash. Gracias @Khushneet Estuve buscando durante media hora ...
csonuryilmaz
Genial, solo responde en esta página que funcionó. Pero también tuve que mover el IFS="" antes de la construcción de la matriz .
pkamb
13

Estoy de acuerdo con los demás en que es probable que el problema sea cómo accedes a los elementos. Citar los nombres de archivo en la asignación de matriz es correcto:

FILES=(
  "2011-09-04 21.43.02.jpg"
  "2011-09-05 10.23.14.jpg"
  "2011-09-09 12.31.16.jpg"
  "2011-09-11 08.43.12.jpg"
)

for f in "${FILES[@]}"
do
  echo "$f"
done

El uso de comillas dobles alrededor de cualquier matriz del formulario "${FILES[@]}"divide la matriz en una palabra por elemento de matriz. No hace ninguna división de palabras más allá de eso.

El uso "${FILES[*]}"también tiene un significado especial, pero une los elementos de la matriz con el primer carácter de $ IFS, lo que da como resultado una palabra, que probablemente no sea lo que desea.

Usando un desnudo ${array[@]}o ${array[*]}sujetos el resultado de esa expansión para una mayor división de palabras, por lo que terminará con palabras divididas en espacios (y cualquier otra cosa $IFS) en lugar de una palabra por elemento de matriz.

Usar un estilo C para el bucle también está bien y evita preocuparse por la división de palabras si no lo tiene claro:

for (( i = 0; i < ${#FILES[@]}; i++ ))
do
  echo "${FILES[$i]}"
done
Dean Hall
fuente
3

Escapando de las obras.

#!/bin/bash

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

echo ${FILES[0]}
echo ${FILES[1]}
echo ${FILES[2]}
echo ${FILES[3]}

Salida:

$ ./test.sh
2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg

Citar las cadenas también produce la misma salida.

Chris Seymour
fuente
3

Si tuviera su matriz así: #! / Bin / bash

Unix[0]='Debian'
Unix[1]="Red Hat"
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${Unix[@]});
    do echo $i;
done

Obtendrías:

Debian
Red
Hat
Ubuntu
Suse

No sé por qué, pero el bucle divide los espacios y los coloca como un elemento individual, incluso si lo rodea con comillas.

Para evitar esto, en lugar de llamar a los elementos de la matriz, llama a los índices, que toman la cadena completa que está entre comillas. ¡Debe estar entre comillas!

#!/bin/bash

Unix[0]='Debian'
Unix[1]='Red Hat'
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${!Unix[@]});
    do echo ${Unix[$i]};
done

Entonces obtendrás:

Debian
Red Hat
Ubuntu
Suse
Jonni2016aa
fuente
2

No es exactamente una respuesta al problema de cita / escape de la pregunta original, pero probablemente algo que realmente hubiera sido más útil para la operación:

unset FILES
for f in 2011-*.jpg; do FILES+=("$f"); done
echo "${FILES[@]}"

Donde, por supuesto, la expresión tendría que adoptarse según el requisito específico (por ejemplo, *.jpgpara todos o 2001-09-11*.jpgsolo para las imágenes de un día determinado).

TNT
fuente
0

Otra solución es usar un ciclo "while" en lugar de un ciclo "for":

index=0
while [ ${index} -lt ${#Array[@]} ]
  do
     echo ${Array[${index}]}
     index=$(( $index + 1 ))
  done
Javier Salas
fuente
0

Si no está atascado en el uso bash, el manejo diferente de espacios en los nombres de archivos es uno de los beneficios de la concha de pescado . Considere un directorio que contiene dos archivos: "a b.txt" y "b c.txt". Aquí hay una suposición razonable al procesar una lista de archivos generados a partir de otro comando bash, pero falla debido a los espacios en los nombres de archivo que experimentó:

# bash
$ for f in $(ls *.txt); { echo $f; }
a
b.txt
b
c.txt

Con fish, la sintaxis es casi idéntica, pero el resultado es lo que cabría esperar:

# fish
for f in (ls *.txt); echo $f; end
a b.txt
b c.txt

Funciona de manera diferente porque fish divide la salida de comandos en nuevas líneas, no espacios.

Si tiene un caso en el que desea dividir en espacios en lugar de líneas nuevas, fishtiene una sintaxis muy legible para eso:

for f in (ls *.txt | string split " "); echo $f; end
Mark Stosberg
fuente
0

Solía ​​restablecer el valor de IFS y la reversión cuando terminaba.

# backup IFS value
O_IFS=$IFS

# reset IFS value
IFS=""

FILES=(
"2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg"
)

for file in ${FILES[@]}; do
    echo ${file}
done

# rollback IFS value
IFS=${O_IFS}

Posible salida del bucle:

2011-09-04 21.43.02.jpg

2011-09-05 10.23.14.jpg

2011-09-09 12.31.16.jpg

2011-09-11 08.43.12.jpg

Madan Sapkota
fuente