Impresión de matriz asociativa BASH

17

¿Hay alguna manera de imprimir una matriz completa ([clave] = valor) sin recorrer todos los elementos?

Supongamos que he creado una matriz con algunos elementos:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Puedo imprimir toda la matriz con

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

Sin embargo, parece que bash ya sabe cómo obtener todos los elementos de la matriz de una sola vez, tanto claves ${!array[@]}como valores${array[@]} .

¿Hay alguna manera de hacer que bash imprima esta información sin el bucle?

Editar:
typeset -p arrayhace eso!
Sin embargo, no puedo eliminar tanto el prefijo como el sufijo en una sola sustitución:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

¿Hay una forma más limpia de obtener / imprimir solo la parte clave = valor de la salida?

Dani_l
fuente

Respuestas:

15

Creo que estás preguntando dos cosas diferentes allí.

¿Hay alguna manera de hacer que bash imprima esta información sin el bucle?

Sí, pero no son tan buenos como simplemente usar el bucle.

¿Hay una forma más limpia de obtener / imprimir solo la parte clave = valor de la salida?

Sí, el forbucle. Tiene las ventajas de que no requiere programas externos, es sencillo y facilita el control del formato de salida exacto sin sorpresas.


Cualquier solución que intente manejar la salida de declare -p( typeset -p) tiene que tratar con a) la posibilidad de que las variables contengan paréntesis o corchetes, b) la cita quedeclare -p debe agregar para hacer que su salida sea una entrada válida para el shell.

Por ejemplo, su expansión b="${a##*(}"come algunos de los valores, si alguna clave / valor contiene un paréntesis de apertura. Esto se debe a que usó ##, lo que elimina el prefijo más largo . Lo mismo para c="${b%% )*}". Aunque, por supuesto, podría igualar la plantilla impresa con declaremás precisión, aún le costaría mucho trabajo si no quisiera todas las citas.

Esto no se ve muy bien a menos que lo necesite.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Con el forbucle, es más fácil elegir el formato de salida que desee:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

A partir de ahí, también es simple. cambiar el formato de salida de lo contrario (quite los corchetes alrededor de la clave, coloque todos los pares clave / valor en una sola línea ...). Si necesita citar algo diferente al shell en sí, deberá hacerlo usted mismo, pero al menos tiene los datos sin procesar para trabajar. (Si tiene nuevas líneas en las claves o valores, probablemente necesitará algunas citas).

Con un Bash actual (4.4, creo), también podría usarlo en printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"lugar de printf "%q=%q". Produce un formato citado algo más agradable, pero por supuesto es un poco más trabajo para recordar escribir. (Y cita el caso de la esquina @como clave de matriz, que %qno cita).

Si el bucle for parece demasiado cansado para escribir, guárdelo como una función en algún lugar (sin citar aquí):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

Y luego solo usa eso:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

También funciona con matrices indexadas:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc
ilkkachu
fuente
Tenga en cuenta que la salida de su printf ...%q...variante no es adecuada para reiniciar al shell si la matriz tiene una @clave como% q no la cita y a=([@]=value)es un error de sintaxis bash.
Stéphane Chazelas
@ StéphaneChazelas, al parecer. "${x@Q}"cita eso también, ya que cita todas las cadenas (y se ve mejor). agregó una nota sobre el uso de eso.
ilkkachu
Sí, copiado de mksh. Otro operador de una forma diferente que no se puede combinar con la mayoría de los demás. Nuevamente, vea zshcon sus banderas de expansión variable (que son anteriores a bash por décadas y con las cuales puede elegir el estilo de cotización: $ {(q) var}, $ {(qq) var} ...) para un mejor diseño. bash tiene el mismo problema que mksh en que no cita la cadena vacía (no es un problema aquí ya que de todos modos bash no admite claves vacías). Además, al usar estilos de comillas que no sean comillas simples ( ${var@Q}recurre a $'...'algunos valores) es importante que el código se reintroduzca en la misma configuración regional.
Stéphane Chazelas
@ StéphaneChazelas, ¿creo que te refieres a un valor no establecido, no a una cadena vacía? ( x=; echo "${x@Q}"da '', unset x; echo "${x@Q}"no da nada.) Bash @Qparece preferir $'\n'a una nueva línea literal, lo que en realidad puede ser bueno en algunas situaciones (pero no puedo decir lo que otros prefieren). Por supuesto, tener una opción allí no sería malo.
ilkkachu
Oh sí, lo siento, no me había dado cuenta de eso. Esa es una diferencia de mksh entonces. La $'...'sintaxis es un problema potencial en cosas como LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'qué salidas $'\n<0xa3><0x5c>'y 0x5csolo es una barra invertida, por lo que tendría un problema si esa cita se interpretara en una configuración regional diferente.
Stéphane Chazelas
9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 tenedor

Tal vez esto:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 tenedores

o esto:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Sin tenedor

para ser comparado con

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Comparación de tiempos de ejecución

Como la última sintaxis no usa fork, podrían ser más rápidos:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Pero esta afirmación no se mantiene si la matriz se hace grande; Si la reducción de las horquillas es eficiente para procesos pequeños, el uso de herramientas dedicadas es más eficiente para procesos más grandes.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

Observación

Como ambas soluciones ( bifurcadas ) usan alineación , ninguna de ellas funcionará si alguna variable contiene una nueva línea . En este caso, la única forma es un forbucle.

F. Hauri
fuente
Si bien parece inteligente, ambas formas son menos eficientes que a for. Lo que realmente es una lástima.
Satō Katsura
@SatoKatsura Estoy de acuerdo, pero si es más lento, el uso de sintaxis pres más corto ... ¡No estoy seguro de que la prsintaxis se mantenga más lenta, incluso con matrices grandes!
F. Hauri
2
@MiniMax Porque no produce el resultado correcto (mismos elementos, orden incorrecto). Tendría que comprimir arreglos ${!array[@]}y ${array[@]}primero para que eso funcione.
Satō Katsura
1
Ese último fragmento pastees más largo que el forbucle en la pregunta escrita en una línea for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, pero requiere dos subcapas y un programa externo. ¿Cómo es eso más ordenado? La solución prtambién se rompe si hay muchos elementos, ya que intenta paginar la salida. Tendría que usar algo como lo | pr -2t -l"${#array[@]}"que está empezando a ser difícil de recordar en comparación con el bucle simple, y nuevamente, es más largo.
ilkkachu
1
En bash, cmd1 | cmd2significa 2 tenedores, incluso si cmd1 o cmd2 o ambos están integrados.
Stéphane Chazelas
2

Si está buscando un shell con mejor soporte de matriz asociativa, intente zsh.

En zsh(donde se agregaron matrices asociativas en 1998, en comparación con 1993 para ksh93 y 2009 para bash), $varo se ${(v)var}expande a los valores (no vacíos) del hash, ${(k)var}a las teclas (no vacías) (en el mismo orden), y ${(kv)var}a las dos claves y valores.

Para preservar los valores vacíos, como para las matrices, debe citar y usar la @bandera.

Entonces, para imprimir las claves y los valores, es solo cuestión de

printf '%s => %s\n' "${(@kv)var}"

Aunque para tener en cuenta un hash posiblemente vacío, debe hacer:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

También tenga en cuenta que zsh usa una sintaxis de definición de matriz mucho más sensata y útil que ksh93's (copiada por bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

Lo que hace que sea mucho más fácil copiar o fusionar matrices asociativas:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(no puede copiar fácilmente un hash sin un bucle con bash, y tenga en cuenta que bashactualmente no admite claves vacías o clave / valores con bytes NUL).

Consulte también las zshfunciones de compresión de matrices que normalmente necesitará para trabajar con matrices asociativas:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})
Stéphane Chazelas
fuente
1

Dado que la composición tipográfica hace lo que desea, ¿por qué no simplemente editar su salida?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

da

a2 = 2  
a1 = 1  
b1 = bbb 

Dónde

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Detallado, pero es bastante fácil ver cómo funciona el formateo: simplemente ejecute la tubería con progresivamente más comandos sed y tr . Modifíquelos para adaptarse a los gustos bonitos de impresión.

Nadreck
fuente
Ese tipo de canalización fallará en el momento en que algunas de las claves o valores de la matriz contengan cualquiera de los caracteres que está reemplazando, como paréntesis, corchetes o comillas. Y una tubería de sedsy trs no es mucho más simple que un forbucle con printf.
ilkkachu
Además, ¿sabes que trtraducir carácter por carácter, no coincide con las cadenas? tr "]=" " ="cambia "]" a un espacio y un =a un =, independientemente de la posición. Así que probablemente podrías combinar los tres tren uno.
ilkkachu
Muy cierto acerca de algunos de los caracteres no alfanuméricos que engloban esto. Sin embargo todo lo que tiene que hacer frente los sufran un orden de magnitud más complejo y por lo menos legible a menos que haya una muy buena razón para tenerlos en su feed de datos y eso es señalado en la pregunta que supongo que están filtrados antes de llegar aquí. Siempre debe tener su advertencia explícita aunque. Considero que estas tuberías son más simples, por ejemplo, y con fines de depuración, que un globo de impresión que funciona perfectamente o explota en su cara. Aquí puede hacer un cambio simple por elemento, probarlo y luego agregar 1 más.
Nadreck
¡Culpa mía! ¡Tengo mis _tr_s y _sed_s totalmente confundidos! Corregido en la última edición.
Nadreck
1

Una opción más es enumerar todas las variables y grep para la que desee.

set | grep -e '^aa='

Lo uso para depurar. Dudo que sea muy eficiente ya que enumera todas las variables.

Si hacía esto a menudo, podría hacerlo una función como esta:

aap() { set | grep -e "^$1="; }

Lamentablemente, cuando verificamos el rendimiento con el tiempo:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Por lo tanto, si hicieras esto muy a menudo, querrías la versión NO FORKS de @ F.Hauri porque es mucho más rápido.

xer0x
fuente