Comment définir des tables de hachage dans Bash?

557

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

Sridhar Ratnakumar
la source
4
Demandez à bash d'exécuter un script python / perl ... C'est tellement flexible!
e2-e4
Pensez à utiliser xonsh (c'est sur github).
Oliver

Réponses:

939

Bash 4

Bash 4 prend nativement en charge cette fonctionnalité. Assurez-vous que le hashbang de votre script est #!/usr/bin/env bashou non #!/bin/bash, vous ne finissez pas par l'utiliser sh. Assurez-vous que vous exécutez votre script directement ou exécutez scriptavec bash script. (Non exécution en fait un script Bash Bash ne se produit, et sera vraiment confus!)

Vous déclarez un tableau associatif en faisant:

declare -A animals

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

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

Ou fusionnez-les:

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

Ensuite, utilisez-les comme des tableaux normaux. Utilisation

  • animals['key']='value' pour définir la valeur

  • "${animals[@]}" pour étendre les valeurs

  • "${!animals[@]}"(remarquez le !) pour développer les touches

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. Ne les utilisez pas evalpour les émuler . Évitez evalcomme la peste, car il est le fléau de scripts shell. La raison la plus importante est que evalvos données sont traitées comme du code exécutable (il existe également de nombreuses autres raisons).

Tout d'abord : envisagez la mise à niveau vers bash 4. Cela rendra le processus beaucoup plus facile pour vous.

S'il y a une raison pour laquelle vous ne pouvez pas mettre à niveau, declarec'est une option beaucoup plus sûre. Il n'évalue pas les données comme le fait le code bash 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, l'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

Rassemblez-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: declarene peut pas être mis dans une fonction. Toute utilisation de l' declareintérieur d'une fonction bash transforme la variable qu'elle crée localement en portée de cette fonction, 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:

  • Mettre à niveau vers bash 4 et utiliser declare -Apour les tableaux associatifs.
  • Utilisez l' declareoption si vous ne pouvez pas mettre à niveau.
  • Envisagez d'utiliser à la awkplace et évitez complètement le problème.
lhunath
la source
1
@ Richard: Vraisemblablement, vous n'utilisez pas réellement bash. Votre hashbang est-il sh au lieu de bash, ou invoquez-vous autrement votre code avec sh? Essayez de mettre cela juste avant votre déclaration: echo "$ BASH_VERSION $ POSIXLY_CORRECT", il devrait sortir 4.xet non y.
lhunath
5
Impossible de mettre à niveau: la seule raison pour laquelle j'écris des scripts dans Bash est la portabilité "exécuter n'importe où". Donc, s'appuyer sur une fonctionnalité non universelle de Bash exclut cette approche. Ce qui est dommage car sinon cela aurait été une excellente solution pour moi!
Steve Pitchers
3
Il est dommage que OSX par défaut reste Bash 3 car cela représente le "défaut" pour beaucoup de gens. Je pensais que la peur de ShellShock aurait pu être la poussée dont ils avaient besoin, mais apparemment pas.
ken
13
@ken c'est un problème de licence. Bash sur OSX est bloqué sur la dernière version sous licence non GPLv3.
lhunath
2
... ou sudo port install bash, pour ceux (à bon escient, à mon humble avis) qui ne veulent pas que les répertoires du PATH pour tous les utilisateurs soient accessibles en écriture sans escalade explicite des privilèges par processus.
Charles Duffy
125

Il y a une substitution de paramètres, bien qu'il puisse aussi être non-PC ... comme l'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 meilleure, mais si vous avez besoin d'un hack ... seul un hack fera l'affaire. Vous pouvez rechercher le tableau / hachage avec des techniques similaires.

Bubnoff
la source
5
Je changerais cela pour VALUE=${animal#*:}protéger le cas oùARRAY[$x]="caesar:come:see:conquer"
glenn jackman
2
Il est également utile de mettre des guillemets doubles autour de $ {ARRAY [@]} au cas où il y aurait des espaces dans les clés ou les valeurs, comme dansfor animal in "${ARRAY[@]}"; do
devguydavid
1
Mais l'efficacité n'est-elle pas assez médiocre? Je pense à O (n * m) si vous voulez comparer à une autre liste de clés, au lieu de O (n) avec des hashmaps appropriés (recherche à temps constant, O (1) pour une seule clé).
CodeManX
1
L'idée est moins sur l'efficacité, plus sur la compréhension / capacité de lecture pour ceux qui ont une expérience en perl, python ou même bash 4. Vous permet d'écrire de la même manière.
Bubnoff
1
@CoDEmanX: c'est un hack , une solution intelligente et élégante mais toujours rudimentaire pour aider les pauvres âmes encore coincées en 2007 avec Bash 3.x. Vous ne pouvez pas vous attendre à des "hashmaps appropriés" ou à des considérations d'efficacité dans un code aussi simple.
MestreLion
85

Voici 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" )
aktivb
la source
2
Notez que la valeur peut ne pas contenir d'espaces, sinon vous ajoutez plus d'éléments à la fois
rubo77
6
Upvote pour la syntaxe hashmap ["key"] = "value" que moi aussi j'ai trouvé manquante dans la réponse acceptée par ailleurs fantastique.
thomanski
@ rubo77 key non plus, il ajoute plusieurs clés. Une façon de contourner cela?
Xeverous
25

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

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

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

et alors

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 n'entrent pas en conflit (par exemple, «rcapitals» qui recherche les pays par capitale). Mais, de toute façon, je pense que vous constaterez que tout cela est assez terrible, en termes de performances.

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

Comme je l'ai dit, cela semble terrible, et il semble qu'il devrait être lent et faire toutes sortes d'E / S inutiles, mais en pratique, il est très rapide (le cache disque est génial, n'est-ce pas?), Même pour un hachage très volumineux les tables. Vous devez appliquer vous-même l'unicité des clés, etc. Même si vous ne disposez que de quelques centaines d'entrées, la combinaison fichier de sortie / grep sera beaucoup plus rapide - selon mon expérience, plusieurs fois plus rapide. Il mange également moins de mémoire.

Voici une façon de procéder:

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`
Al P.
la source
1
Génial! vous pouvez même le répéter: pour i dans $ (compgen -A variable capitols); faire hget "$ i" "" fait
zhaorufei
22

Utilisez simplement le système de fichiers

Le système de fichiers est une structure arborescente 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 ne nécessite pas de shell spécifique.

Création de 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 si lent. Je l'ai testé sur ma machine, avec un SSD et btrfs , et il fait environ 3000 éléments en lecture / écriture par seconde .

lovasoa
la source
1
Quelle version de bash prend en charge mkdir -d? (Pas 4.3, sur Ubuntu 14. J'aurais recours à mkdir /run/shm/foo, ou si cela remplissait la RAM mkdir /tmp/foo
,.
1
Peut-être mktemp -détait-ce à la place?
Reid Ellis
2
Curieux de savoir quelle est la différence entre $value=$(< $hashtable/$key) et value=$(< $hashtable/$key)? Merci!
Helin Wang
1
"testé sur ma machine" Cela semble être un excellent moyen de graver un trou dans votre SSD. Toutes les distributions Linux n'utilisent pas tmpfs par défaut.
kirbyfan64sos
Je traite environ 50000 hachages. Perl et PHP font un poil en moins de 1/2 seconde. Noeud en 1 seconde et quelque chose. L'option FS semble lente. Cependant, pouvons-nous nous assurer que les fichiers n'existent qu'en RAM, d'une manière ou d'une autre?
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
DigitalRoss
la source
31
Soupir, cela semble inutilement insultant et c'est quand même inexact. On ne mettrait pas la validation d'entrée, l'échappement ou l'encodage (voir, je le sais en fait) dans les entrailles de la table de hachage, mais plutôt dans un wrapper et le plus tôt possible après l'entrée.
DigitalRoss
@DigitalRoss pouvez-vous expliquer à quoi sert #hash dans eval echo '$ {hash' "$ 1" '# hash}' . pour moi ça me semble comme un commentaire pas plus que ça. #hash a-t-il une signification particulière ici?
Sanjay
@Sanjay ${var#start}supprime le début du texte depuis le début de la valeur stockée dans la variable var .
jpaugh
11

Considérez une solution utilisant la lecture intégrée bash 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 d'ensembles de champs délimités (et pas seulement 2) que souhaité. Nous avons utilisé le | délimiteur car les spécificateurs de plage de ports peuvent nécessiter deux-points, par exemple 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
AsymLabs
la source
2
@CharlieMartin: read est une fonctionnalité très puissante et est sous-utilisée par de nombreux programmeurs bash. Il permet des formes compactes de traitement de liste de type lisp . Par exemple, dans l'exemple ci-dessus, nous pouvons retirer uniquement le premier élément et conserver le reste (c'est-à-dire un concept similaire au premier et reposer dans le lisp) en faisant:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs
6

Je suis d'accord avec @lhunath et d'autres que le tableau associatif est la voie à suivre avec Bash 4. Si vous êtes bloqué sur Bash 3 (OSX, anciennes distributions que vous ne pouvez pas mettre à jour), vous pouvez également utiliser expr, qui devrait être partout, une chaîne et les expressions régulières. Je l'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 expression régulière pour extraire les valeurs

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. Fractionnez la chaîne pour répertorier 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
    }

Vous pouvez maintenant l'utiliser:

$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
marco
la source
5

J'ai vraiment aimé la réponse d'Al P, mais je voulais que l'unicité soit appliquée à moindre coût, alors je suis allé plus loin: utilisez un répertoire. Il existe des limitations évidentes (limites de fichiers de répertoires, noms de fichiers invalides) 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 fonctionne é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 me lancer. A bientôt!

Edit: Ajout de hdestroy ()

Cole Stanfield
la source
3

Deux choses, vous pouvez utiliser la mémoire au lieu de / tmp dans n'importe quel noyau 2.6 en utilisant / dev / shm (Redhat), d'autres distributions peuvent varier. Hget peut également être réimplémenté en utilisant la lecture suivante:

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 d'avoir à lire toutes les entrées. Si votre implémentation peut avoir des clés en double, laissez simplement le retour. Cela économise les frais de lecture et de bifurcation 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 recherchant 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

Lecture / écho:

$ time echo $(hget FD oracle)
3

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

sur de multiples invocations, je n'ai jamais vu moins qu'une amélioration de 50%. Tout cela peut être attribué à la fourche au-dessus de la tête, en raison de l'utilisation de /dev/shm.

jrichard
la source
3

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 des réponses ici ...) intitulé Tables de hachage dans bash :

J'avais auparavant l' habitude cksumde hacher, mais j'ai depuis traduit la chaîne hashCode de Java en bash / zsh natif.

# 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 aucune ne devrait vraiment être utilisée de toute façon. Bash est pour des ponctuels rapides, et de telles choses devraient très rarement impliquer une complexité qui pourrait nécessiter des hachages, sauf peut-être chez vous ~/.bashrcet vos amis.

Adam Katz
la source
Le lien dans la réponse fait peur! Si vous cliquez dessus, vous êtes coincé dans une boucle de redirection. Veuillez mettre à jour.
Rakib
1
@MohammadRakibAmin - Oui, mon site Web est en panne et je doute que je ressuscite mon blog. J'ai mis à jour le lien ci-dessus vers une version archivée. Merci de votre intérêt!
Adam Katz
2

Avant bash 4, il n'y a pas de bon moyen d'utiliser des tableaux associatifs dans bash. Votre meilleur pari est d'utiliser un langage interprété qui prend en charge de telles choses, comme awk. D'un autre côté, bash 4 les prend en charge.

En ce qui concerne les moins bons moyens dans bash 3, voici une référence qui pourrait aider: http://mywiki.wooledge.org/BashFAQ/006

kojiro
la source
2

Solution Bash 3:

En lisant certaines des réponses, j'ai rassemblé une petite fonction rapide que j'aimerais apporter 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[@])
Milan Adamovsky
la source
Je pense que c'est un extrait assez soigné. Il pourrait utiliser un peu de nettoyage (pas grand-chose, cependant). Dans ma version, j'ai renommé «clé» en «paire» et fait que KEY et VALUE soient en minuscules (car j'utilise des majuscules lorsque les variables sont exportées). J'ai également renommé getHashKey en getHashValue et rendu la clé et la valeur locales (parfois, vous voudriez qu'elles ne soient pas locales, cependant). Dans getHashKeys, je n'attribue rien à la valeur. J'utilise le point-virgule pour la séparation, car mes valeurs sont des URL.
0

J'ai également utilisé la méthode bash4 mais je trouve un 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

Je découvre qu'avec bash 4.3.11, l'ajout à une clé existante dans le dict a entraîné l'ajout de la valeur si elle est déjà présente. Ainsi, par exemple, après une répétition, le contenu de la valeur était "checkKOcheckKOallCheckOK" et ce n'était pas bon.

Pas de problème avec bash 4.3.39 où ajouter une clé existante signifie sous-estimer la valeur actuale si elle est déjà présente.

J'ai résolu cela en nettoyant / déclarant le tableau associatif statusCheck avant le cicle:

unset statusCheck; declare -A statusCheck
Alex
la source
-1

Je crée des HashMaps en 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 également jeter un œil à shell_map , qui est une implémentation HashMap réalisée dans bash 3.

Bruno Negrão Zica
la source