Comment définir les tables de hachage dans Bash?

ce qui est l'équivalent de dictionnaires Python mais en Bash (devrait fonctionner à travers OS X et Linux).

398
demandé sur codeforester 2009-09-29 22:29:38

16 réponses

Bash 4

Bash 4 supporte cette fonctionnalité. Assurez-vous que le hashbang de votre script est #!/usr/bin/env bash ou #!/bin/bash ou toute autre chose qui fait référence à bash et non sh . Assurez-vous que vous exécutez votre script, et ne pas faire quelque chose de stupide comme sh script qui causerait votre bash hashbang pour être ignoré. C'est des trucs de base, mais autant garder à défaut, d'où la ré-itération.

You déclarer un tableau associatif en faisant:

declare -A animals

vous pouvez le remplir avec des éléments en utilisant l'opérateur normal d'assignation de tableau:

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

ou les fusionner:

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

alors utilisez-les comme des tableaux normaux. "${animals[@]}" étend les valeurs, "${!animals[@]}" (notez le ! ) étend 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. ne pas utiliser eval pour les émuler . Vous devez éviter eval comme la peste, parce que est la peste de shell scripting. La raison la plus importante est que vous ne voulez pas traiter vos données comme du code exécutable (il y a beaucoup d'autres raisons aussi).

tout d'abord et avant tout : il suffit d'envisager la mise à niveau à bash 4. Sérieusement. L'avenir c'est maintenant , arrêter de vivre dans le passé et souffrir en forçant stupide cassé et laid hacks sur votre code et chaque pauvre âme coincé à l'entretenir.

si vous avez une excuse stupide pourquoi vous " ne peut pas mettre à niveau ", declare est une option beaucoup plus sûre. Il n'évalue pas les données comme le code bash comme eval le fait, et en tant que tel il ne permet pas l'injection de code arbitraire tout à fait ainsi facilement.

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

tout d'abord, de façon indirecte (sérieusement; ne l'utilisez jamais à moins d'être malade mentalement ou d'avoir une autre mauvaise excuse pour écrire des piratages).

$ 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

les réunir:

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

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

nous allons l'utiliser:

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

Note: declare ne peut pas être mis dans une fonction. Toute utilisation de declare à l'intérieur d'une fonction bash transforme la variable qu'il crée local à la portée de cette fonction, ce qui signifie que nous ne pouvons pas accéder ou modifier des tableaux globaux avec elle. (Dans bash 4, Vous pouvez utiliser declare-g pour déclarer les variables globales - mais dans bash 4, vous devriez utiliser des tableaux associatifs en premier lieu, pas ce hack.)

résumé

Mise à jour de bash 4 et l'utilisation declare -A . Si vous ne pouvez pas, envisager de passer entièrement à awk avant de faire des piratages laids comme décrit ci-dessus. Et surtout restez loin de eval hackery.

680
répondu lhunath 2016-08-18 18:31:26

il y a une substitution de paramètre, bien qu'elle puisse aussi être un-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]##*:}"

The BASH 4 way est mieux, bien sûr, mais si vous avez besoin d'un piratage ...seulement un hack va faire. Vous pouvez rechercher le tableau/hachage avec des techniques similaires.

94
répondu Bubnoff 2012-05-09 18:11:26

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" )
50
répondu aktivb 2015-07-23 08:36:05

vous pouvez encore modifier l'interface hput()/hget() de sorte que vous avez nommé des hachures comme suit:

hput() {
    eval """"=''
}

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

et 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 n'entrent pas en conflit (par exemple, 'rcapitals' qui fait une recherche par pays par capitale). Mais, de toute façon, je pense que vous trouverez que tout cela est assez terrible, la performance-sage.

si vous voulez vraiment une recherche rapide de hash, il y a un terrible, terrible un piratage qui fonctionne très bien. C'est ceci: écrivez votre clé/valeurs dans un fichier temporaire, une par ligne, puis utilisez 'grep' ^$key '' pour les sortir, en utilisant des pipes avec cut ou awk ou sed ou n'importe quoi pour récupérer les valeurs.

comme je l'ai dit, ça a l'air terrible, et on dirait que ça devrait être lent et faire toutes sortes d'IO inutiles, mais en pratique c'est très rapide (la cache disque est géniale, n'est-ce pas?), même pour de très grandes tables de hachage. Vous devez faire respecter l'unicité clé vous-même, etc. Même si vous n'avez que quelques centaines d'entrées, le fichier de sortie/grep combo va être un peu plus rapide - en mon expérience plusieurs fois plus rapide. Il mange aussi moins de mémoire.

Voici une façon de le faire:

hinit() {
    rm -f /tmp/hashmap.
}

hput() {
    echo " " >> /tmp/hashmap.
}

hget() {
    grep "^ " /tmp/hashmap. | awk '{ print  };'
}

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
répondu Al P. 2013-08-29 16:09:52
hput () {
  eval hash""=''
}

hget () {
  eval echo '${hash'""'#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
répondu DigitalRoss 2009-09-29 22:45:35

Envisager une solution en utilisant le bash builtin lire comme illustré dans l'extrait de code à partir d'un ufw script de pare-feu qui suit. Cette approche a l'avantage d'utiliser autant d'ensembles de champs délimités (pas seulement 2) que désiré. Nous avons utilisé le délimiteur | parce que les spécificateurs de port range peuvent nécessiter deux points, c'est-à-dire 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
répondu AsymLabs 2015-09-15 17:16:30

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 fichier, et vos valeurs seront des contenus de fichier. L'avantage est qu'il peut gérer des hashmaps énormes, et ne nécessite pas un shell spécifique.

Hashtable création

hashtable=$(mktemp -d)

Ajouter un élément

echo $value > $hashtable/$key

lire un élément

value=$(< $hashtable/$key)

Performance

bien sûr, son lent, mais pas que lent. Je l'ai testé sur ma machine, avec un SSD et btrfs , et il fait autour de 3000 élément lire/écrire par seconde .

8
répondu lovasoa 2017-11-06 13:21:19

je suis d'accord avec @lhunath et d'autres que le tableau associatif est la voie à suivre avec Bash 4. Si vous êtes collé à Bash 3 (OSX, Vieux distros que vous ne pouvez pas mettre à jour) vous pouvez utiliser aussi expr, qui devrait être partout, une chaîne de caractères et des expressions régulières. Je l'aime surtout quand le dictionnaire n'est pas trop grand.

  1. choisir 2 séparateurs que vous n'utiliserez pas dans les clés et les valeurs (par exemple ',' et ':' )
  2. écrivez votre carte chaîne (noter le séparateur ',' aussi au début et à la fin)

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

    get_animal {
        echo "$(expr "$animals" : ".*,:\([^,]*\),.*")"
    }
    
  4. fendre la chaîne de caractères pour lister les articles

    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
répondu marco 2014-07-01 15:13:10

j'ai vraiment aimé la réponse de Al P, mais je voulais que l'unicité soit imposée à moindre coût, donc je l'ai fait un pas de plus - utiliser un répertoire. Il y a des limites évidentes (limites des fichiers répertoires, noms de fichiers invalides) mais cela devrait fonctionner dans la plupart des cas.

hinit() {
    rm -rf /tmp/hashmap.
    mkdir -p /tmp/hashmap.
}

hput() {
    printf "" > /tmp/hashmap./
}

hget() {
    cat /tmp/hashmap./
}

hkeys() {
    ls -1 /tmp/hashmap.
}

hdestroy() {
    rm -rf /tmp/hashmap.
}

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 aussi 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

j'ai pensé que je me lancerais. Acclamations!

Edit: Ajout de hdestroy()

5
répondu Cole Stanfield 2012-03-14 21:19:11

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 distros peuvent varier. Aussi hget peut être réimplémenté en utilisant lire comme suit:

function hget {

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

de plus, en présumant 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 dupliquées, alors il suffit de laisser de côté 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:

Grep/ Awk:

hget() {
    grep "^ " /dev/shm/hashmap. | awk '{ print  };'
}

$ 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 invocations multiples, Je n'ai jamais vu moins qu'une amélioration de 50%. Tout cela peut être attribué à la fourche sur la tête, en raison de l'utilisation de /dev/shm .

2
répondu jrichard 2011-04-15 17:44:08

Bash 3 solution:

en lisant certaines des réponses que j'ai mis en place une petite fonction rapide, je voudrais contribuer en arrière 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=

  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=

  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
répondu Milan Adamovsky 2013-08-29 15:26:17

Avant bash 4 il n'y a pas de bonne façon d'utiliser des tableaux associatifs dans bash. Votre meilleur pari est d'utiliser un langage interprété qui a réellement de soutien pour de telles choses, comme awk. D'autre part, bash 4 fait les soutenir.

comme pour moins bonnes façons dans bash 3, Voici une référence qui pourrait aider: http://mywiki.wooledge.org/BashFAQ/006

1
répondu kojiro 2010-08-12 12:53:37

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'un de mes billets de blog en mars 2010 (avant quelques réponses ici...) intitulée tables de hachage 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

bien sûr, il fait un appel externe pour cksum et est donc quelque peu ralenti, mais la mise en œuvre est très propre et utilisable. Ce n'est pas bidirectionnel, et la manière intégrée est beaucoup mieux, mais ne devrait vraiment être utilisé de toute façon. Bash est pour les one-offs rapides, et de telles choses devraient assez rarement impliquer la complexité qui pourrait exiger des hachures, sauf peut-être dans votre .bashrc et amis.

1
répondu Adam Katz 2012-10-18 00:39:57

pour obtenir un peu plus de performances rappelez-vous que grep a une fonction stop, pour s'arrêter quand il trouve la nème correspondance dans ce cas n serait 1.

grep --max_count=1 ... ou grep - m 1 ...

0
répondu bozon 2010-04-01 15:47:27

j'ai aussi utilisé la méthode bash4 mais j'ai trouvé et bug ennuyeux.

j'avais besoin de mettre à jour dynamiquement le contenu du tableau associatif, donc j'ai utilisé cette façon:

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 en ajoutant à une clé existante dans le dict a conduit à ajouter la valeur si déjà présent. 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ù l'appenging d'une clé existante signifie substiturer la valeur actuelle si déjà présent.

j'ai résolu ce nettoyage/déclaration juste le tableau associatif statusCheck avant la glace:

unset statusCheck; declare -A statusCheck
0
répondu Alex 2015-09-04 06:31:09

je crée des Hachmaps 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 dans shell_map , qui est une implémentation HashMap faite dans bash 3.

0
répondu Bruno Negrão Zica 2017-05-23 12:10:42