¿Cómo se definen las tablas hash en Bash?

¿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 es compatible nativamente con esta característica. Asegúrate de que el hashbang de tu 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 guión y no estés haciendo algo tan tonto como el sh script que podría hacer que tu bash hashbang sea ignorado. Esto es algo básico, pero muchos siguen fallando, de ahí la reiteració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 combínalos:

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

Luego úselos como las matrices normales. "${animals[@]}" expande los valores, "${!animals[@]}" (¡observe el ! ) 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 matrices asociativas. No use eval para emularlos . Debe evitar la evaluación como la peste, porque es la plaga de las secuencias de comandos shell. La razón más importante es que no desea tratar sus datos como código ejecutable (también hay muchos otros motivos).

En primer lugar : simplemente considere actualizar a bash 4. En serio. El futuro es ahora , deja de vivir en el pasado y de sufrir forzando estúpidos hackeos rotos y feos en tu código y cada alma pobre atrapada en mantenerlo.

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

Preparemos la respuesta introduciendo los conceptos:

Primero, indirección (en serio, nunca use esto a menos que esté mentalmente enfermo o tenga alguna otra excusa mala 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 

Tráelos juntos:

 # 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 dentro de una función bash convierte la variable que crea local al scope de esa función, lo que significa que no podemos acceder o modificar las matrices globales con ella. (En bash 4 puedes usar declare -g para declarar variables globales, pero en bash 4, deberías estar usando matrices asociativas en primer lugar, no este truco).

Resumen

Actualiza a bash 4 y usa declare -A . Si no puede, considere cambiar por completo a awk antes de hacer feos hacks como se describe arriba. Y definitivamente mantente al diablo lejos de hackeo eval .

Hay una sustitución de parámetros, aunque también puede ser un PC … como la indirección.

 #!/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]##*:}" 

El modo BASH 4 es mejor, por supuesto, pero si necesitas un truco … solo un truco funcionará. Puede buscar la matriz / hash con técnicas similares.

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 funcionó para mí con bash 4.1.5:

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

Puede modificar aún más la interfaz hput () / hget () para que tenga hashes nombrados 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 entren en conflicto (p. Ej., ‘Rcapitals’ que hace búsqueda de país por ciudad capital). Pero, de cualquier manera, creo que encontrarás que todo esto es bastante terrible, en cuanto a rendimiento.

Si realmente quieres búsqueda rápida de hash, hay un truco terrible y terrible que realmente funciona muy bien. Es esto: escriba su clave / valores en un archivo temporal, uno por línea, luego use ‘grep’ ^ $ key “‘para sacarlos, usando pipes con cut 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 innecesarios, pero en la práctica es muy rápido (la memoria caché de disco es impresionante, ¿no?), Incluso para hash muy grande. mesas. Usted debe imponer la singularidad de la clave usted mismo, etc. Incluso si solo tiene unas pocas entradas, el archivo de salida / combo grep será bastante más rápido, según mi experiencia varias veces más rápido. También come menos memoria.

Aquí hay una manera 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` 
 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 

Considere una solución que utiliza la lectura integrada 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 usado el | delimitador porque los especificadores de rango de puertos 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 

Solo usa el sistema de archivos

El sistema de archivos es una estructura de árbol que se puede usar 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 hasams enormes y no requiere un shell específico.

Creación de Hashtable

hashtable=$(mktemp -d)

Agrega 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 .

Estoy de acuerdo con @lhunath y otros en que la matriz asociativa es el camino a seguir con Bash 4. Si estás atrapado en Bash 3 (OSX, viejas distribuciones que no puedes actualizar) puedes 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 (observe 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 

Realmente me gustó la respuesta de Al P, pero quería que la exclusividad se aplicara de forma barata, así que di un paso más: usar un directorio. Hay algunas limitaciones obvias (límites de archivos de directorio, nombres de archivo no válidos) pero debería funcionar para 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 

Solo pensé en lanzarme. ¡Saludos!

Editar: Añadiendo hdestroy ()

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

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

Además, suponiendo que todas las claves son únicas, el retorno cortocircuita 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 bifurcar grep y awk. El uso de / dev / shm para ambas implementaciones produjo lo siguiente usando time hget en un hash de 3 entradas buscando 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 

Leer / 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 al tenedor por cabeza, debido al uso de /dev/shm .

Bash 3 solución:

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[@]) 

Antes de bash 4 no hay una buena forma de usar matrices asociativas en bash. Su mejor opción es usar un lenguaje interpretado que en realidad tenga soporte para tales cosas, como awk. Por otro lado, bash 4 los admite.

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

Un compañero de trabajo acaba de mencionar este hilo. He implementado tablas hash de forma independiente dentro de bash, y no depende de la versión 4. De una publicación de blog mía 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 un llamado externo para cksum y, por lo tanto, se ralentiza un poco, pero la implementación es muy limpia y utilizable. No es bidireccional, y la forma incorporada es mucho mejor, pero tampoco debería usarse de todos modos. Bash es para one-offs rápidos, y tales cosas rara vez implican complejidad que podría requerir hash, excepto tal vez en su .bashrc y sus amigos.

Para obtener un poco más de rendimiento recuerde que grep tiene una función de detención, para detener cuando encuentra el enésimo partido en este caso n sería 1.

grep –max_count = 1 … o grep -m 1 …

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

Necesitaba actualizar dinámicamente el contenido del conjunto asociativo, así que lo usé de 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 agregar a una clave existente en el dict resultó en agregar el valor si ya estaba presente. Entonces, por ejemplo, después de una repetición, el contenido del valor era “checkKOcheckKOallCheckOK” y esto no era bueno.

No hay problema con bash 4.3.39 donde astackr una clave existente significa substistir el valor actuale si ya está presente.

Resolví esto simplemente limpiando / declarando el array asociativo statusCheck antes del ciclo:

 unset statusCheck; declare -A statusCheck 

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 hecha en bash 3.

    Intereting Posts