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).
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.
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.
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" )
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`
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
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
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 .
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.
- choisir 2 séparateurs que vous n'utiliserez pas dans les clés et les valeurs (par exemple ',' et ':' )
-
écrivez votre carte chaîne (noter le séparateur ',' aussi au début et à la fin)
animals=",moo:cow,woof:dog,"
-
utiliser un regex pour extraire les valeurs
get_animal { echo "$(expr "$animals" : ".*,:\([^,]*\),.*")" }
-
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
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()
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
.
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[@])
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
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.
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 ...
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
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.