¿Cómo definir tablas hash en Bash?


Respuestas:


939

Golpe 4

Bash 4 admite de forma nativa esta función. Asegúrate de que el hashbang de tu script sea más #!/usr/bin/env basho menos #!/bin/bashasí que no termines usando sh. Asegúrese de que está ejecutando su script directamente o ejecute scriptcon bash script. (En realidad no la ejecución de un script Bash con Bash no suceda, y será muy confuso!)

Declaras una matriz asociativa haciendo:

declare -A animals

Puede llenarlo con elementos utilizando el operador de asignación de matriz normal. Por ejemplo, si desea tener un mapa de animal[sound(key)] = animal(value):

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

O fusionarlos:

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

Luego úselos como matrices normales. Utilizar

  • animals['key']='value' establecer valor

  • "${animals[@]}" para expandir los valores

  • "${!animals[@]}"(observe el !) para expandir las teclas

No olvides citarlos:

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

Golpe 3

Antes de bash 4, no tienes matrices asociativas. No lo use evalpara emularlos . Evita evalcomo la plaga, porque es la plaga de las secuencias de comandos de shell. La razón más importante es que evaltrata sus datos como código ejecutable (también hay muchas otras razones).

Primero y principal : considere actualizar a bash 4. Esto hará que todo el proceso sea mucho más fácil para usted.

Si hay una razón por la que no puede actualizar, declarees una opción mucho más segura. No evalúa los datos como el código bash como lo evalhace, y como tal no permite la inyección de código arbitrario con tanta facilidad.

Preparemos la respuesta introduciendo los conceptos:

Primero, indirección.

$ 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: declareno se puede poner en una función. Cualquier uso de declaredentro de una función bash convierte la variable que crea local en el 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, puede usar matrices asociativas en primer lugar, evitando esta solución).

Resumen:

  • Actualice a bash 4 y úselo declare -Apara matrices asociativas.
  • Use la declareopción si no puede actualizar.
  • Considere usar en su awklugar y evite el problema por completo.

1
@ Richard: Presumiblemente, en realidad no estás usando bash. ¿Es su hashbang sh en lugar de bash, o está invocando su código con sh? Intente poner esto justo antes de declarar: echo "$ BASH_VERSION $ POSIXLY_CORRECT", debería salir 4.xy no y.
lhunath

55
No se puede actualizar: la única razón por la que escribo scripts en Bash es por la portabilidad de "ejecutar en cualquier lugar". Por lo tanto, confiar en una característica no universal de Bash descarta este enfoque. ¡Qué pena, porque de lo contrario habría sido una excelente solución para mí!
Steve Pitchers

3
Es una pena que OSX siga siendo Bash 3 de forma predeterminada, ya que esto representa el "valor predeterminado" para muchas personas. Pensé que el susto de ShellShock podría haber sido el impulso que necesitaban, pero aparentemente no.
Ken

13
@ken es un problema de licencia. Bash en OSX está atascado en la última compilación sin licencia GPLv3.
lhunath

2
... o sudo port install bash, para aquellos (sabiamente, en mi humilde opinión) que no están dispuestos a crear directorios en la RUTA para que todos los usuarios puedan escribir sin una escalada explícita de privilegios por proceso.
Charles Duffy

125

Hay sustitución de parámetros, aunque también puede ser no PC ... como 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]##*:}"

La forma de BASH 4 es mejor, por supuesto, pero si necesita un hack ... solo un hack lo hará. Puede buscar en la matriz / hash con técnicas similares.


55
Cambiaría eso para VALUE=${animal#*:}proteger el caso dondeARRAY[$x]="caesar:come:see:conquer"
Glenn Jackman

2
Es también útil para poner comillas dobles alrededor del $ {MATRIZ [@]} en caso de que haya espacios en las claves o valores, como enfor animal in "${ARRAY[@]}"; do
devguydavid

1
¿Pero no es la eficiencia bastante pobre? Estoy pensando en O (n * m) si desea comparar con otra lista de claves, en lugar de O (n) con hashmaps adecuados (búsqueda de tiempo constante, O (1) para una sola clave).
CodeManX

1
La idea es menos sobre eficiencia, más sobre comprensión / capacidad de lectura para aquellos con experiencia en perl, python o incluso bash 4. Le permite escribir de manera similar.
Bubnoff

1
@CoDEmanX: este es un truco , una solución inteligente y elegante pero aún rudimentaria para ayudar a las almas pobres que aún se estancaron en 2007 con Bash 3.x. No puede esperar "hashmaps adecuados" o consideraciones de eficiencia en un código tan simple.
MestreLion

85

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

2
Tenga en cuenta que el valor puede no contener espacios; de lo contrario, agregará más elementos a la vez
rubo77

66
Vota a favor de la sintaxis de hashmap ["key"] = "value" que también encontré faltante en la respuesta aceptada que de otro modo sería fantástica.
thomanski

@ clave Rubo77 tampoco, agrega varias claves. ¿Alguna forma de solucionar esto?
Xeverous

25

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 entren en conflicto (p. Ej., 'Rcapitals' que realiza búsquedas de país por ciudad capital). Pero, de cualquier manera, creo que encontrarás que todo esto es bastante terrible, en cuanto al rendimiento.

Si realmente quieres una búsqueda rápida de hash, hay un truco terrible que funciona realmente bien. Es esto: escriba su clave / valores en un archivo temporal, uno por línea, luego use 'grep "^ $ key"' para sacarlos, utilizando 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é del disco es increíble, ¿no?), Incluso para hash muy grandes mesas. Tienes que imponer la unicidad de la clave tú mismo, etc. Incluso si solo tienes unos cientos de entradas, el combo de archivo de salida / 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`

1
¡Excelente! incluso puede iterarlo: para i en $ (compgen -A capitols variables); do hget "$ i" "" hecho
zhaorufei

22

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 enormes hashmaps y no requiere un shell específico.

Creación de tabla 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 .


1
¿Qué versión de bash soporta mkdir -d? (No 4.3, en Ubuntu 14. Recurriría mkdir /run/shm/foo, o si eso llenara RAM mkdir /tmp/foo
,.

1
Tal vez mktemp -dse entiende en su lugar?
Reid Ellis

2
¿Curioso cuál es la diferencia entre $value=$(< $hashtable/$key)y value=$(< $hashtable/$key)? ¡Gracias!
Helin Wang

1
"lo probé en mi máquina" Esto suena como una excelente manera de hacer un agujero a través de su SSD. No todas las distribuciones de Linux usan tmpfs por defecto.
kirbyfan64sos

Estoy procesando unos 50000 hashes. Perl y PHP lo hacen menos de 1/2 segundo. Nodo en 1 segundo y algo. La opción FS suena lenta. Sin embargo, ¿podemos asegurarnos de que los archivos solo existan en RAM, de alguna manera?
Rolf

14
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

31
Suspiro, eso parece innecesariamente insultante y de todos modos es inexacto. Uno no pondría la validación de entrada, el escape o la codificación (ver, en realidad lo sé) en las entrañas de la tabla hash, sino más bien en un contenedor y tan pronto como sea posible después de la entrada.
DigitalRoss

@DigitalRoss, ¿puede explicar cuál es el uso de #hash en eval echo '$ {hash' "$ 1" '# hash}' . para mí me parece un comentario no más que eso. ¿#hash tiene algún significado especial aquí?
Sanjay

@Sanjay ${var#start}elimina el texto que comienza desde el principio del valor almacenado en la variable var .
jpaugh

11

Considere una solución usando el bash builtin read como se ilustra dentro del 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

2
@CharlieMartin: la lectura es una característica muy poderosa y muchos programadores de bash la subutilizan. Permite formas compactas de procesamiento de listas tipo lisp . Por ejemplo, en el ejemplo anterior podemos quitar solo el primer elemento y retener el resto (es decir, un concepto similar al primero y descansar en lisp) haciendo:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs

6

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 (tenga en cuenta el separador ',' también al principio y al final)

    animals=",moo:cow,woof:dog,"
  3. Use 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

5

Realmente me gustó la respuesta de Al P, pero quería que la unicidad se aplicara de manera barata, así que lo llevé un paso más allá: use 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. ¡Salud!

Editar: Agregar hdestroy ()


3

Dos cosas, puedes usar memoria en lugar de / tmp en cualquier kernel 2.6 usando / dev / shm (Redhat) otras distribuciones pueden variar. También se puede volver a implementar hget 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, 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 el retorno. Esto ahorra el gasto de leer y bifurcar grep y awk. El uso de / dev / shm para ambas implementaciones arrojó 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 invocaciones múltiples nunca vi menos de una mejora del 50%. Todo esto se puede atribuir al tenedor sobre la cabeza, debido al uso de /dev/shm.


3

Un compañero de trabajo acaba de mencionar este hilo. Implementé independientemente tablas hash dentro de bash, y no depende de la versión 4. De una publicación mía en mi blog en marzo de 2010 (antes de algunas de las respuestas aquí ...) titulada Tablas hash en bash :

Me previamente acostumbrado cksuma picadillo pero desde entonces han traducido hashCode cadena de Java a los nativos bash / zsh.

# Here's the hashing function
ht() {
  local h=0 i
  for (( i=0; i < ${#1}; i++ )); do
    let "h=( (h<<5) - h ) + $(printf %d \'${1:$i:1})"
    let "h |= h"
  done
  printf "$h"
}

# 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
echo ${#myhash[@]} # "2" - there are two values (note, zsh doesn't count right)

No es bidireccional, y la forma incorporada es mucho mejor, pero tampoco debería usarse. Bash es para casos únicos rápidos, y tales cosas rara vez deben involucrar complejidad que pueda requerir hashes, excepto tal vez en usted ~/.bashrcy sus amigos.


¡El enlace en la respuesta da miedo! Si hace clic en él, está atrapado en un bucle de redirección. Por favor actualice.
Rakib

1
@MohammadRakibAmin - Sí, mi sitio web no funciona y dudo que resucite mi blog. He actualizado el enlace anterior a una versión archivada. ¡Gracias por tu interés!
Adam Katz

2

Antes de bash 4, no hay una buena manera de usar matrices asociativas en bash. Su mejor opción es utilizar un lenguaje interpretado que realmente tenga soporte para tales cosas, como awk. Por otra parte, golpe del 4 hace apoyarlos.

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


2

Solución Bash 3:

Al leer algunas de las respuestas, reuní 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[@])

Creo que este es un fragmento bastante bueno. Podría usar un poco de limpieza (aunque no mucho). En mi versión, he cambiado el nombre de 'clave' a 'par' e hice KEY y VALUE en minúsculas (porque uso mayúsculas cuando se exportan variables). También cambié el nombre de getHashKey por getHashValue e hice que la clave y el valor fueran locales (aunque a veces desearía que no fueran locales). En getHashKeys, no asigno nada al valor. Uso punto y coma para la separación, ya que mis valores son URL.

0

También usé la forma bash4 pero encuentro un error molesto.

Necesitaba actualizar dinámicamente el contenido de la matriz asociativa, así que lo utilicé 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 está 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 donde agregar una clave existente significa subestimar el valor real si ya está presente.

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

unset statusCheck; declare -A statusCheck

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.