Quel est l'équivalent des dictionnaires Python mais dans Bash (devrait fonctionner sous OS X et Linux).
Quel est l'équivalent des dictionnaires Python mais dans Bash (devrait fonctionner sous OS X et Linux).
Réponses:
Bash 4 prend nativement en charge cette fonctionnalité. Assurez-vous que le hashbang de votre script est #!/usr/bin/env bash
ou non #!/bin/bash
, vous ne finissez pas par l'utiliser sh
. Assurez-vous que vous exécutez votre script directement ou exécutez script
avec 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
Avant bash 4, vous n'avez pas de tableaux associatifs. Ne les utilisez pas eval
pour les émuler . Évitez eval
comme la peste, car il est le fléau de scripts shell. La raison la plus importante est que eval
vos 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, declare
c'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: declare
ne peut pas être mis dans une fonction. Toute utilisation de l' declare
inté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:
declare -A
pour les tableaux associatifs.declare
option si vous ne pouvez pas mettre à niveau.awk
place et évitez complètement le problème.4.x
et non y
.
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.
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.
VALUE=${animal#*:}
protéger le cas oùARRAY[$x]="caesar:come:see:conquer"
for animal in "${ARRAY[@]}"; do
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" )
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`
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.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
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 .
mkdir -d
? (Pas 4.3, sur Ubuntu 14. J'aurais recours à mkdir /run/shm/foo
, ou si cela remplissait la RAM mkdir /tmp/foo
mktemp -d
était-ce à la place?
$value=$(< $hashtable/$key)
et value=$(< $hashtable/$key)
? Merci!
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
${var#start}
supprime le début du texte depuis le début de la valeur stockée dans la variable var .
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
IFS=$'|' read -r first rest <<< "$fields"
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.
É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 expression régulière pour extraire les valeurs
get_animal {
echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
}
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
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 ()
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
.
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 cksum
de 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 ~/.bashrc
et vos amis.
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
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[@])
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
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.