index asociativo array bash dictionary hashtable associative-array

asociativo - bash hash array



¿Cómo definir tablas hash en Bash? (16)

¿Cuál es el equivalente de los diccionarios de Python pero en Bash (debería funcionar en OS X y Linux).


Bash 4

Bash 4 soporta de forma nativa esta característica. Asegúrese de que el hashbang de su script sea #!/usr/bin/env bash o #!/bin/bash o cualquier otra cosa que haga referencia a bash y no a sh . Asegúrate de que estás ejecutando tu script, y de que no estás haciendo algo tonto como el sh script lo que provocaría que tu bash hashbang sea ignorado. Esto es algo básico, pero muchos siguen fallando, de ahí la repetición.

Usted declara una matriz asociativa haciendo:

declare -A animals

Puede llenarlo con elementos usando el operador de asignación de matriz normal:

animals=( ["moo"]="cow" ["woof"]="dog")

O fusionarlos:

declare -A animals=( ["moo"]="cow" ["woof"]="dog")

Luego úsalos como arreglos normales. "${animals[@]}" expande los valores, "${!animals[@]}" (note que ! ) expande las teclas. No olvides citarlos:

echo "${animals[moo]}" for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done

Bash 3

Antes de bash 4, no tienes arrays asociativos. No utilice eval para emularlos . Debes evitar la evaluación como la plaga, porque es la plaga de los scripts de shell. La razón más importante es que no desea tratar sus datos como código ejecutable (también hay muchas otras razones).

En primer lugar : considere actualizar a bash 4. En serio. El futuro es ahora , deja de vivir en el pasado y sufre por forzar estúpidos hacks rotos y feos en tu código y cada alma pobre se atasca manteniéndola.

Si tiene alguna excusa tonta por la que " no puede actualizar ", declare es una opción mucho más segura. No evalúa los datos como el código bash como lo hace eval , y como tal no permite la inyección de código arbitrario con tanta facilidad.

Preparemos la respuesta introduciendo los conceptos:

Primero, indirección (seriamente; nunca uses esto a menos que estés mentalmente enfermo o tengas alguna otra mala excusa para escribir hacks).

$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}" cow

En segundo lugar, declare :

$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo" cow

Reunirlos:

# Set a value: declare "array_$index=$value" # Get a value: arrayGet() { local array=$1 index=$2 local i="${array}_$index" printf ''%s'' "${!i}" }

Vamos a usarlo:

$ sound=moo $ animal=cow $ declare "animals_$sound=$animal" $ arrayGet animals "$sound" cow

Nota: declare no se puede poner en una función. Cualquier uso de declare en una función bash convierte la variable que crea local al alcance de esa función, lo que significa que no podemos acceder o modificar matrices globales con ella. (En bash 4 puede usar declare -g para declarar variables globales, pero en bash 4, debería usar matrices asociativas en primer lugar, no este hack).

Resumen

Actualiza a bash 4 y usa declare -A . Si no puedes, considera cambiar por completo a awk antes de hacer trucos feos como se describe anteriormente. Y definitivamente aléjate de los hackers eval .


Solo usa el sistema de archivos

El sistema de archivos es una estructura de árbol que se puede utilizar como un mapa hash. Su tabla hash será un directorio temporal, sus claves serán nombres de archivos y sus valores serán contenidos de archivos. La ventaja es que puede manejar enormes hashmaps y no requiere un shell específico.

Creación hash

hashtable=$(mktemp -d)

Agregar un elemento

echo $value > $hashtable/$key

Leer un elemento

value=$(< $hashtable/$key)

Actuación

Por supuesto, es lento, pero no tan lento. Lo probé en mi máquina, con un SSD y btrfs , y hace alrededor de 3000 elementos de lectura / escritura por segundo .


Antes de bash 4, no hay una buena manera de usar matrices asociativas en bash. Su mejor apuesta es usar un lenguaje interpretado que realmente admita tales cosas, como awk. Por otro lado, bash 4 los apoya.

En cuanto a las formas menos buenas de bash 3, aquí hay una referencia que podría ayudar: http://mywiki.wooledge.org/BashFAQ/006


Considere una solución que utilice la lectura incorporada de bash como se ilustra en el fragmento de código de un script de firewall ufw que sigue. Este enfoque tiene la ventaja de utilizar tantos conjuntos de campos delimitados (no solo 2) como se desee. Hemos utilizado el | delimitador porque los especificadores de rango de puerto pueden requerir dos puntos, es decir, 6001: 6010 .

#!/usr/bin/env bash readonly connections=( ''192.168.1.4/24|tcp|22'' ''192.168.1.4/24|tcp|53'' ''192.168.1.4/24|tcp|80'' ''192.168.1.4/24|tcp|139'' ''192.168.1.4/24|tcp|443'' ''192.168.1.4/24|tcp|445'' ''192.168.1.4/24|tcp|631'' ''192.168.1.4/24|tcp|5901'' ''192.168.1.4/24|tcp|6566'' ) function set_connections(){ local range proto port for fields in ${connections[@]} do IFS=$''|'' read -r range proto port <<< "$fields" ufw allow from "$range" proto "$proto" to any port "$port" done } set_connections


Creo HashMaps en bash 3 usando variables dinámicas. Expliqué cómo funciona eso en mi respuesta a: Arrays asociativos en scripts de Shell

También puedes echar un vistazo en shell_map , que es una implementación de HashMap realizada en bash 3.


Dos cosas, puede usar la memoria en lugar de / tmp en cualquier kernel 2.6 usando / dev / shm (Redhat) otras distros pueden variar. También se puede volver a implementar hget usando la siguiente lectura:

function hget { while read key idx do if [ $key = $2 ] then echo $idx return fi done < /dev/shm/hashmap.$1 }

Además, al asumir que todas las claves son únicas, el retorno corta el ciclo de lectura y evita tener que leer todas las entradas. Si su implementación puede tener claves duplicadas, simplemente omita la devolución. Esto ahorra el gasto de leer y forking tanto grep como awk. El uso de / dev / shm para ambas implementaciones produjo el siguiente uso del tiempo hget en un hash de 3 entradas en busca de la última entrada:

Grep / Awk:

hget() { grep "^$2 " /dev/shm/hashmap.$1 | awk ''{ print $2 };'' } $ time echo $(hget FD oracle) 3 real 0m0.011s user 0m0.002s sys 0m0.013s

Lectura / eco:

$ time echo $(hget FD oracle) 3 real 0m0.004s user 0m0.000s sys 0m0.004s

en múltiples invocaciones nunca vi menos de un 50% de mejora. Todo esto se puede atribuir a la bifurcación sobre la cabeza, debido al uso de /dev/shm .


Esto es lo que estaba buscando aquí:

declare -A hashmap hashmap["key"]="value" hashmap["key2"]="value2" echo "${hashmap["key"]}" for key in ${!hashmap[@]}; do echo $key; done for value in ${hashmap[@]}; do echo $value; done echo hashmap has ${#hashmap[@]} elements

Esto no me funcionó con bash 4.1.5:

animals=( ["moo"]="cow" )


Estoy de acuerdo con @lhunath y otros en que la matriz asociativa es el camino a seguir con Bash 4. Si está atascado en Bash 3 (OSX, distribuciones antiguas que no puede actualizar), puede usar también expr, que debería estar en todas partes, una cadena y expresiones regulares. Me gusta especialmente cuando el diccionario no es demasiado grande.

  1. Elija 2 separadores que no usará en claves y valores (por ejemplo, '','' y '':'')
  2. Escriba su mapa como una cadena (note el separador '','' también al principio y al final)

    animals=",moo:cow,woof:dog,"

  3. Usa una expresión regular para extraer los valores.

    get_animal { echo "$(expr "$animals" : ".*,$1:/([^,]*/),.*")" }

  4. Dividir la cadena para enumerar los elementos

    get_animal_items { arr=$(echo "${animals:1:${#animals}-2}" | tr "," "/n") for i in $arr do value="${i##*:}" key="${i%%:*}" echo "${value} likes to $key" done }

Ahora puedes usarlo:

$ animal = get_animal "moo" cow $ get_animal_items cow likes to moo dog likes to woof


Hay sustitución de parámetros, aunque también puede ser un-PC ... como direccionamiento indirecto.

#!/bin/bash # Array pretending to be a Pythonic dictionary ARRAY=( "cow:moo" "dinosaur:roar" "bird:chirp" "bash:rock" ) for animal in "${ARRAY[@]}" ; do KEY="${animal%%:*}" VALUE="${animal##*:}" printf "%s likes to %s./n" "$KEY" "$VALUE" done printf "%s is an extinct animal which likes to %s/n" "${ARRAY[1]%%:*}" "${ARRAY[1]##*:}"

La forma de BASH 4 es mejor, por supuesto, pero si necesitas un hack ... solo lo hará un hack. Puedes buscar el array / hash con técnicas similares.


Para obtener un poco más de rendimiento, recuerde que grep tiene una función de parada, que se detiene cuando encuentra la enésima coincidencia en este caso n sería 1.

grep --max_count = 1 ... o grep -m 1 ...


Puede modificar aún más la interfaz hput () / hget () para que haya nombrado hashes de la siguiente manera:

hput() { eval "$1""$2"=''$3'' } hget() { eval echo ''${''"$1$2"''#hash}'' }

y entonces

hput capitals France Paris hput capitals Netherlands Amsterdam hput capitals Spain Madrid echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`

Esto le permite definir otros mapas que no están en conflicto (p. Ej., ''Rcapitals'' que realiza una búsqueda de país por ciudad capital). Pero, de cualquier manera, creo que encontrará que todo esto es bastante terrible, en cuanto a rendimiento.

Si realmente quieres una búsqueda rápida de hash, hay un terrible, terrible hack que realmente funciona muy bien. Es esto: escriba su clave / valores en un archivo temporal, una por línea, luego use ''grep "^ $ clave"'' para eliminarlos, usando tuberías con corte o awk o sed o lo que sea para recuperar los valores.

Como dije, suena terrible, y parece que debería ser lento y hacer todo tipo de IO innecesarias, pero en la práctica es muy rápido (el caché de disco es impresionante, ¿no?), Incluso para hash muy grande mesas. Tiene que imponer la singularidad de la clave, etc. Incluso si solo tiene unos pocos cientos de entradas, el combo de salida de archivo / grep será bastante más rápido, en mi experiencia varias veces más rápido. También come menos memoria.

Aquí hay una forma de hacerlo:

hinit() { rm -f /tmp/hashmap.$1 } hput() { echo "$2 $3" >> /tmp/hashmap.$1 } hget() { grep "^$2 " /tmp/hashmap.$1 | awk ''{ print $2 };'' } hinit capitals hput capitals France Paris hput capitals Netherlands Amsterdam hput capitals Spain Madrid echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`


Realmente me gustó la respuesta de Al P, pero quería que la singularidad se aplicara a bajo costo, así que fui un paso más allá: usar un directorio. Existen algunas limitaciones obvias (límites de archivos de directorio, nombres de archivos no válidos) pero debería funcionar en la mayoría de los casos.

hinit() { rm -rf /tmp/hashmap.$1 mkdir -p /tmp/hashmap.$1 } hput() { printf "$3" > /tmp/hashmap.$1/$2 } hget() { cat /tmp/hashmap.$1/$2 } hkeys() { ls -1 /tmp/hashmap.$1 } hdestroy() { rm -rf /tmp/hashmap.$1 } hinit ids for (( i = 0; i < 10000; i++ )); do hput ids "key$i" "value$i" done for (( i = 0; i < 10000; i++ )); do printf ''%s/n'' $(hget ids "key$i") > /dev/null done hdestroy ids

También funciona un poco mejor en mis pruebas.

$ time bash hash.sh real 0m46.500s user 0m16.767s sys 0m51.473s $ time bash dirhash.sh real 0m35.875s user 0m8.002s sys 0m24.666s

Sólo pensé que me gustaría colaborar. ¡Salud!

Edición: Añadiendo hdestroy ()


Solución Bash 3:

Al leer algunas de las respuestas, armé una pequeña función rápida que me gustaría contribuir y que podría ayudar a otros.

# Define a hash like this MYHASH=("firstName:Milan" "lastName:Adamovsky") # Function to get value by key getHashKey() { declare -a hash=("${!1}") local key local lookup=$2 for key in "${hash[@]}" ; do KEY=${key%%:*} VALUE=${key#*:} if [[ $KEY == $lookup ]] then echo $VALUE fi done } # Function to get a list of all keys getHashKeys() { declare -a hash=("${!1}") local KEY local VALUE local key local lookup=$2 for key in "${hash[@]}" ; do KEY=${key%%:*} VALUE=${key#*:} keys+="${KEY} " done echo $keys } # Here we want to get the value of ''lastName'' echo $(getHashKey MYHASH[@] "lastName") # Here we want to get all keys echo $(getHashKeys MYHASH[@])


También utilicé el modo bash4 pero encuentro un error molesto.

Necesitaba actualizar dinámicamente el contenido de la matriz asociativa, así que utilicé esta manera:

for instanceId in $instanceList do aws cloudwatch describe-alarms --output json --alarm-name-prefix $instanceId| jq ''.["MetricAlarms"][].StateValue''| xargs | grep -E ''ALARM|INSUFFICIENT_DATA'' [ $? -eq 0 ] && statusCheck+=([$instanceId]="checkKO") || statusCheck+=([$instanceId]="allCheckOk" done

Descubrí que con bash 4.3.11 anexar una clave existente en el dict dio como resultado anexar el valor si ya estaba presente. Entonces, por ejemplo, después de alguna repetición, el contenido del valor era "checkKOcheckKOallCheckOK" y esto no era bueno.

No hay problema con bash 4.3.39 cuando agregar una clave existente significa subsistir el valor real si ya está presente.

Resolví esto simplemente limpiando / declarando la matriz asociativa statusCheck antes del ciclo:

unset statusCheck; declare -A statusCheck


Un compañero de trabajo acaba de mencionar este hilo. He implementado de forma independiente tablas hash dentro de bash, y no depende de la versión 4. De una publicación de mi blog en marzo de 2010 (antes de algunas de las respuestas aquí ...) titulada Hash tables in bash :

# Here''s the hashing function ht() { local ht=`echo "$*" |cksum`; echo "${ht//[!0-9]}"; } # Example: myhash[`ht foo bar`]="a value" myhash[`ht baz baf`]="b value" echo ${myhash[`ht baz baf`]} # "b value" echo ${myhash[@]} # "a value b value" though perhaps reversed

Claro, hace una llamada externa para cksum y por lo tanto es un poco lento, pero la implementación es muy limpia y utilizable. No es bidireccional, y la forma integrada es mucho mejor, pero ninguna debería usarse de todos modos. Bash es para una sola vez rápida, y esas cosas rara vez involucran complejidad que puede requerir hashes, excepto quizás en tu .bashrc y amigos.


hput () { eval hash"$1"=''$2'' } hget () { eval echo ''${hash''"$1"''#hash}'' } hput France Paris hput Netherlands Amsterdam hput Spain Madrid echo `hget France` and `hget Netherlands` and `hget Spain`

$ sh hash.sh Paris and Amsterdam and Madrid