¿Cómo definir 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).

Author: codeforester, 2009-09-29

16 answers

Bash 4

Bash 4 soporta esta característica de forma nativa. 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úrese de que está ejecutando su script, y no haciendo algo tonto como sh script que haría que su bash hashbang sea ignorado. Esto es algo básico, pero muchos siguen fallando en ello, de ahí la reiteración.

Se declara un array asociativo haciendo:

declare -A animals

Puedes 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")

Entonces úsalos como los arrays normales. "${animals[@]}" expande los valores, "${!animals[@]}" (observe el !) expande las claves. No se olvide de 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 utilice eval para emularlos. Debes evitar eval como la plaga, porque es la plaga de shell scripting. Lo más importante la razón es que no desea tratar sus datos como código ejecutable (también hay muchas otras razones).

Primero y ante todo : Solo considera actualizar a bash 4. Seriamente. El futuro es ahora, deja de vivir en el pasado y sufrir de él forzando estúpidos hackeos rotos y feos en tu código y cada pobre alma atascada manteniéndolo.

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

Preparemos la respuesta introduciendo los conceptos: {[29]]}

Primero, indirección (en serio; nunca use esto a menos que esté mentalmente enfermo o tenga 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 un función. Cualquier uso de declare dentro de una función bash convierte la variable que crea local en el ámbito de esa función, lo que significa que no podemos acceder ni modificar matrices globales con ella. (En bash 4 puedes usar declare-g para declarar variables globales - pero en bash 4, deberías usar arrays asociativos en primer lugar, no este truco.)

Resumen

Actualice a bash 4 y use declare -A. Si no puede, considere cambiar completamente a awk antes de hacer hacks feos como descrito anteriormente. Y definitivamente manténgase alejado de eval hackery.

 680
Author: lhunath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-18 18:31:26

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

#!/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 necesita un hack ...sólo un hack lo hará. Puedes buscar en el array / hash con técnicas similares.

 94
Author: Bubnoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-09 18:11:26

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" )
 50
Author: aktivb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-23 08:36:05

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

hput() {
    eval "$1""$2"='$3'
}

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

Y luego

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 entran en conflicto (por ejemplo, '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 una búsqueda rápida de hash, hay un terrible, terrible hack que realmente funciona muy bien. Es esto: escriba su clave / valores a un archivo temporal, uno por línea, luego use 'grep" ^key key "' para sacarlos, usando tuberías con cut o awk o sed o lo que sea para recuperar los valores.

Como dije, suena terrible, y suena como que debería ser lento y hacer todo tipo de IO innecesario, pero en la práctica es muy rápido (caché de disco es impresionante, no es?), incluso para tablas hash muy grandes. Tienes que hacer cumplir la singularidad clave tú mismo, etc. Incluso si solo tiene unos pocos cientos de entradas, el combo archivo de salida / grep es va a ser un poco más rápido, en 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`
 22
Author: Al P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-29 16:09:52
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
 13
Author: DigitalRoss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-29 22:45:35

Considere una solución usando bash builtin read como se ilustra en el fragmento de código de un script de firewall de 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
 9
Author: AsymLabs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-15 17:16:30

Simplemente use 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 archivo y sus valores serán contenidos de archivo. La ventaja es que puede manejar grandes hashmaps, y no requiere un shell específico.

Creación de la tabla hash

hashtable=$(mktemp -d)

Añadir un elemento

echo $value > $hashtable/$key

Lee un elemento

value=$(< $hashtable/$key)

Rendimiento

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

 8
Author: lovasoa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-06 13:21:19

Estoy de acuerdo con @lhunath y otros en que el array asociativo es el camino a seguir con Bash 4. Si está pegado a 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 (tenga en cuenta el separador', ' también al principio y end)

    animals=",moo:cow,woof:dog,"
    
  3. Utilice una expresión regular para extraer los valores

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
    
  4. Divide la cadena para listar 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
 6
Author: marco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-01 15:13:10

Realmente me gustó la respuesta de Al P, pero quería que la unicidad se hiciera cumplir a bajo costo, así que lo llevé un paso más allá: use un directorio. Hay algunas limitaciones obvias (límites de archivos de directorio, nombres de archivos 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 colaborar. ¡Salud!

Editar: Añadiendo hdestroy ()

 5
Author: Cole Stanfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-14 21:19:11

Dos cosas, puede usar memoria en lugar de /tmp en cualquier kernel 2.6 usando /dev/shm (Redhat) otras distribuciones pueden variar. También hget puede ser reimplementado usando read como sigue:

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 cortocircuita el bucle de lectura y evita tener que leer todas las entradas. Si su implementación puede tener claves duplicadas, simplemente deje de lado la devolución. Esto ahorra el gasto de leer y bifurcar tanto grep como awk. Usando /dev / shm para ambas implementaciones se obtuvo 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

Read / echo:

$ 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 horquilla sobre la cabeza, debido al uso de /dev/shm.

 2
Author: jrichard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-15 17:44:08

Solución Bash 3:

Al leer algunas de las respuestas, armé una pequeña función rápida que me gustaría contribuir para 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[@])
 2
Author: Milan Adamovsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-29 15:26:17

Antes de bash 4 no hay una buena manera de usar arrays asociativos en bash. Tu mejor apuesta es usar un lenguaje interpretado que realmente tenga soporte para tales cosas, como awk. Por otro lado, bash 4 los apoya.

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

 1
Author: kojiro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-12 12:53:37

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 un post mío en marzo de 2010 (antes de algunas de las respuestas aquí...) titulado Tablas hash en 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, se ralentiza un poco, pero la implementación es muy limpia y utilizable. No es bidireccional, y la forma incorporada es mucho mejor, pero ninguno de los dos debería usarse realmente Por cierto. Bash es para una sola vez rápida, y tales cosas deberían implicar muy rara vez la complejidad que podría requerir hashes, excepto tal vez en su .bashrc y sus amigos.

 1
Author: Adam Katz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-18 00:39:57

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

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

 0
Author: bozon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-01 15:47:27

También usé la forma bash4, pero me parece un error molesto.

Necesitaba actualizar dinámicamente el contenido del array asociativo, así que 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

Me entero de que con bash 4.3.11 añadiendo a una clave existente en el dict resultó en añadir el valor si ya está presente. Así, 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 donde appenging una clave existente significa a sustituya el valor actuale si ya está presente.

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

unset statusCheck; declare -A statusCheck
 0
Author: Alex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-04 06:31:09

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

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

 0
Author: Bruno Negrão Zica,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:42