Quel est l'équivalent de dictionnaires Python mais en Bash (devrait fonctionner sous OS X et Linux).
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
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:
declare -A
pour les tableaux associatifs.declare
si vous ne pouvez pas effectuer de mise à niveau.awk
à la place et évitez le problème.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.
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 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`
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.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
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 .
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
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
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.
Écrivez votre carte sous forme de chaîne (notez le séparateur ',' également au début et à la fin)
animals=",moo:cow,woof:dog,"
Utilisez une regex pour extraire les valeurs
get_animal {
echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
}
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
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 ()
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
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[@])
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.
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
.
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
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.