web-dev-qa-db-fra.com

Comment définir des tables de hachage dans Bash?

Quel est l'équivalent de dictionnaires Python mais en Bash (devrait fonctionner sous OS X et Linux).

497
Sridhar Ratnakumar

Bash 4

Bash 4 supporte nativement cette fonctionnalité. Assurez-vous que le hashbang de votre script est #!/usr/bin/env bash ou #!/bin/bash afin de ne pas utiliser sh. Assurez-vous que vous exécutez votre script directement ou exécutez script avec bash script. (Ne pas exécuter un script Bash avec Bash se produit et sera réellement déroutant!)

Vous déclarez un tableau associatif en faisant:

declare -A animals

Vous pouvez le remplir avec des éléments en utilisant l'opérateur d'affectation de tableau normal. Par exemple, si vous voulez avoir une carte de animal[sound(key)] = animal(value):

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

Ou les fusionner:

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

Ensuite, utilisez-les comme des tableaux normaux. Utilisez animals['key']='value' pour définir la valeur, "${animals[@]}" pour développer les valeurs et "${!animals[@]}" (remarquez le !) pour développer les clés. N'oubliez pas de les citer:

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

Bash 3

Avant bash 4, vous n’avez pas de tableaux associatifs. N'utilisez pas eval pour les émuler. Évitez eval comme la peste, car elle est la plaie des scripts Shell. La raison la plus importante est que eval traite vos données en tant que code exécutable (il existe de nombreuses autres raisons).

D'abord et avant tout : envisagez de passer à bash 4. Cela facilitera grandement le processus pour vous.

S'il y a une raison pour laquelle vous ne pouvez pas mettre à niveau, declare est une option beaucoup plus sûre. Il n'évalue pas les données en tant que code bash comme eval, et en tant que tel ne permet pas l'injection de code arbitraire aussi facilement.

Préparons la réponse en introduisant les concepts:

Tout d'abord, indirection.

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

Deuxièmement, declare:

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

Réunissez-les:

# Set a value:
declare "array_$index=$value"

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

Utilisons-le:

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

Remarque: declare ne peut être inséré dans une fonction. Toute utilisation de declare dans une fonction bash transforme la variable créée locale en valeur, ce qui signifie que nous ne pouvons pas accéder à ou modifier les tableaux globaux avec elle. (Dans bash 4, vous pouvez utiliser declare -g pour déclarer des variables globales, mais dans bash 4, vous pouvez utiliser des tableaux associatifs en premier lieu, en évitant cette solution de contournement.)

Sommaire:

  • Mettez à niveau vers bash 4 et utilisez declare -A pour les tableaux associatifs.
  • Utilisez l'option declare si vous ne pouvez pas effectuer de mise à niveau.
  • Pensez à utiliser awk à la place et évitez le problème.
831
lhunath

Il y a substitution de paramètres, bien que cela puisse être aussi un-PC ... comme indirection.

#!/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 méthode BASH 4 est bien sûr préférable, mais si vous avez besoin d’un hack, seul un hack suffira. Vous pouvez rechercher le tableau/hachage avec des techniques similaires.

112
Bubnoff

C'est ce que je cherchais ici:

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

Cela n'a pas fonctionné pour moi avec bash 4.1.5:

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

Vous pouvez modifier davantage l'interface hput ()/hget () afin que vous ayez nommé des hachages comme suit:

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

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

puis

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

Cela vous permet de définir d’autres cartes qui ne sont pas en conflit (par exemple, des "récapitulatifs" qui effectuent une recherche de pays par capitale). Mais de toute façon, je pense que vous constaterez que tout cela est plutôt terrible, en termes de performances.

Si vous voulez vraiment une recherche rapide dans le hachage, il existe un piratage terrible qui fonctionne vraiment très bien. Voici ceci: écrivez votre clé/vos valeurs dans un fichier temporaire, une ligne par ligne, puis utilisez 'grep "^ $ key"' pour les extraire, en utilisant des tuyaux avec cut ou awk ou sed ou autre pour récupérer les valeurs.

Comme je l'ai dit, cela semble terrible, et cela devrait être lent et faire toutes sortes d'E/S inutiles, mais en pratique, c'est très rapide (le cache disque est génial, n'est-ce pas?), Même pour de très grands hash les tables. Vous devez imposer vous-même l'unicité des clés, etc. Même si vous n'avez que quelques centaines d'entrées, le combo fichier de sortie/grep va être un peu plus rapide - selon mon expérience plusieurs fois plus rapidement. Il mange aussi moins de mémoire.

Voici une façon de le faire:

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`
24
Al P.

Il suffit d'utiliser le système de fichiers

Le système de fichiers est une arborescence qui peut être utilisée comme une carte de hachage. Votre table de hachage sera un répertoire temporaire, vos clés seront des noms de fichiers et vos valeurs seront le contenu du fichier. L'avantage est qu'il peut gérer d'énormes hashmaps et qu'il ne nécessite pas de shell spécifique.

Création d'une table de hachage

hashtable=$(mktemp -d)

Ajouter un élément

echo $value > $hashtable/$key

Lire un élément

value=$(< $hashtable/$key)

Performance

Bien sûr, c'est lent, mais pas que lent. Je l'ai testé sur ma machine, avec un SSD et btrfs , et il fonctionne autour de 3 000 éléments en lecture/écriture par seconde .

16
lovasoa
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
14
DigitalRoss

Considérez une solution utilisant la commande intégrée bash read, comme illustré dans l'extrait de code d'un script de pare-feu ufw qui suit. Cette approche présente l’avantage d’utiliser autant de jeux de champs délimités (pas seulement 2) que souhaité. Nous avons utilisé le délimiteur car les spécificateurs de plage de ports peuvent nécessiter un signe deux-points, c'est-à-dire 6001: 601.

#!/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
11
AsymLabs

Je suis d'accord avec @lhunath et d'autres que le tableau associatif est le chemin à parcourir avec Bash 4. Si vous êtes bloqué sur Bash 3 (OSX, anciennes distributions que vous ne pouvez pas mettre à jour), vous pouvez aussi utiliser expr, qui devrait être partout, une chaîne et des expressions régulières. J'aime surtout quand le dictionnaire n'est pas trop gros.

  1. Choisissez 2 séparateurs que vous n'utiliserez pas dans les clés et les valeurs (par exemple, ',' et ':')
  2. Écrivez votre carte sous forme de chaîne (notez le séparateur ',' également au début et à la fin)

    animals=",moo:cow,woof:dog,"
    
  3. Utilisez une regex pour extraire les valeurs

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
    
  4. Fractionner la chaîne pour lister les éléments

    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
    }
    

Maintenant, vous pouvez l'utiliser:

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

J'ai bien aimé la réponse de Al P mais je voulais que l'unicité soit appliquée à moindre coût, alors je l'ai poussée plus loin: utiliser un répertoire. Il existe des limitations évidentes (limites des fichiers de répertoire, noms de fichiers non valides), mais cela devrait fonctionner dans la plupart des cas.

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

Il effectue également un peu mieux dans mes tests.

$ 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

Je pensais juste que je participerais.

Edit: Ajout de hdestroy ()

5
Cole Stanfield

Avant Bash 4, il n’existait pas de bonne façon d’utiliser des tableaux associatifs dans bash. Votre meilleur choix est d'utiliser un langage interprété qui prend en charge de telles choses, comme awk. Par contre, bash 4 ne les supporte pas .

En ce qui concerne moins de bonnes manières dans bash 3, voici une référence qui pourrait aider: http://mywiki.wooledge.org/BashFAQ/006

2
kojiro

Solution Bash 3:

En lisant certaines des réponses, j’ai rassemblé une petite fonction rapide que je souhaiterais contribuer en retour et qui pourrait aider les autres.

# 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
Milan Adamovsky

Un collègue vient de mentionner ce fil. J'ai indépendamment implémenté des tables de hachage dans bash, et cela ne dépend pas de la version 4. D'après un de mes articles de blog en mars 2010 (avant certaines réponses ici ...) intitulé Tables de hachage dans bash :

J'ai précédemment utilisé cksum pour hacher mais ai depuis traduit la chaîne de caractères hashCode de Java en natif 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)

Ce n'est pas bidirectionnel, et la méthode intégrée est bien meilleure, mais ni l'une ni l'autre ne devrait vraiment être utilisée de toute façon. Bash est destiné aux parties ponctuelles rapides, et de telles choses devraient très rarement impliquer une complexité qui pourrait nécessiter un hachage, sauf peut-être dans votre ~/.bashrc et vos amis.

2
Adam Katz

Deux choses, vous pouvez utiliser la mémoire à la place de/tmp dans n’importe quel noyau 2.6 en utilisant/dev/shm (Redhat), d’autres distributions pouvant varier. Aussi, hget peut être réimplémenté en utilisant ce qui suit:

function hget {

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

De plus, en supposant que toutes les clés sont uniques, le retour court-circuite la boucle de lecture et évite de devoir lire toutes les entrées. Si votre implémentation peut avoir des clés en double, laissez simplement de côté le retour. Cela évite les dépenses liées à la lecture et à la corrélation entre grep et awk. L'utilisation de/dev/shm pour les deux implémentations a donné les résultats suivants en utilisant time hget sur un hachage à 3 entrées cherchant la dernière entrée:

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

Lire/echo:

$ time echo $(hget FD Oracle)
3

real    0m0.004s
user    0m0.000s
sys     0m0.004s

sur plusieurs invocations, je n'ai jamais vu moins d'une amélioration de 50%. Tout cela peut être attribué à la fourchette, en raison de l'utilisation de /dev/shm.

2
jrichard

J'ai aussi utilisé la méthode bash4 mais je trouve et bug ennuyeux.

J'avais besoin de mettre à jour dynamiquement le contenu du tableau associatif, j'ai donc utilisé cette méthode:

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

J'ai découvert qu'avec bash 4.3.11, l'ajout d'une clé existante dans le dict entraînait l'ajout de la valeur si elle était déjà présente. Ainsi, par exemple, après quelques répétitions, le contenu de la valeur était "checkKOcheckKOallCheckOK" et ce n’était pas bon.

Pas de problème avec bash 4.3.39 où apposer une clé existante signifie de remplacer la valeur actuelle si elle est déjà présente.

J'ai résolu ce problème en nettoyant/déclarant le tableau associatif statusCheck avant le cycle:

unset statusCheck; declare -A statusCheck
0
Alex

Je crée HashMaps dans Bash 3 en utilisant des variables dynamiques. J'ai expliqué comment cela fonctionne dans ma réponse à: Tableaux associatifs dans les scripts Shell

Vous pouvez aussi jeter un oeil à Shell_map, qui est une implémentation de HashMap réalisée dans bash 3.

0