Tableaux associatifs dans les scripts Shell

Nous avions besoin d’un script simulant des tableaux associatifs ou une structure de données de type Map pour Shell Scripting, n’importe quel corps?

    Pour append à la réponse d’ Irfan , voici une version plus courte et plus rapide de get() car elle ne nécessite aucune itération sur le contenu de la carte:

     get() { mapName=$1; key=$2 map=${!mapName} value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )" } 

    Une autre option, si la portabilité n’est pas votre principale préoccupation, consiste à utiliser des tableaux associatifs intégrés au shell. Cela devrait fonctionner dans bash 4.0 (disponible maintenant sur la plupart des dissortingbutions principales, mais pas sur OS X, sauf si vous l’installez vous-même), ksh et zsh:

     declare -A newmap newmap[name]="Irfan Zulfiqar" newmap[designation]=SSE newmap[company]="My Own Company" echo ${newmap[company]} echo ${newmap[name]} 

    Selon le shell, vous devrez peut-être faire une typeset -A newmap au lieu de declare -A newmap , ou dans certains cas pas du tout nécessaire.

    Un autre moyen non-bash 4.

     #!/bin/bash # A pretend Python dictionary with bash 3 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 echo -e "${ARRAY[1]%%:*} is an extinct animal which likes to ${ARRAY[1]#*:}\n" 

    Vous pouvez également lancer une instruction if pour y chercher. if [[$ var = ~ / blah /]]. ou peu importe.

    Je pense que vous devez prendre du recul et réfléchir à ce qu’est réellement une carte ou un tableau associatif. Tout cela permet de stocker une valeur pour une clé donnée et de la récupérer rapidement et efficacement. Vous souhaiterez peut-être également pouvoir parcourir les clés pour récupérer chaque paire de valeurs de clé ou supprimer des clés et leurs valeurs associées.

    Maintenant, pensez à une structure de données que vous utilisez tout le temps dans les scripts shell, et même juste dans le shell sans écrire de script, qui possède ces propriétés. Stumped? C’est le système de fichiers.

    En fait, tout ce dont vous avez besoin pour avoir un tableau associatif en programmation shell est un répertoire temporaire. mktemp -d est votre constructeur de tableau associatif:

     prefix=$(basename -- "$0") map=$(mktemp -dt ${prefix}) echo >${map}/key somevalue value=$(cat ${map}/key) 

    Si vous n’avez pas envie d’utiliser l’ echo et le cat , vous pouvez toujours écrire quelques petites enveloppes; ceux-ci sont modélisés à partir de ceux d’Irfan, bien qu’ils ne fassent que générer la valeur plutôt que de définir des variables arbitraires comme $value :

     #!/bin/sh prefix=$(basename -- "$0") mapdir=$(mktemp -dt ${prefix}) trap 'rm -r ${mapdir}' EXIT put() { [ "$#" != 3 ] && exit 1 mapname=$1; key=$2; value=$3 [ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}" echo $value >"${mapdir}/${mapname}/${key}" } get() { [ "$#" != 2 ] && exit 1 mapname=$1; key=$2 cat "${mapdir}/${mapname}/${key}" } put "newMap" "name" "Irfan Zulfiqar" put "newMap" "designation" "SSE" put "newMap" "company" "My Own Company" value=$(get "newMap" "company") echo $value value=$(get "newMap" "name") echo $value 

    edit : Cette approche est en réalité un peu plus rapide que la recherche linéaire en utilisant sed suggérée par le questionneur, ainsi que plus robuste (elle permet aux clés et aux valeurs de contenir -, =, space, qnd “: SP:”). Le fait qu’il utilise le système de fichiers ne le rend pas lent; ces fichiers ne sont en réalité jamais garantis être écrits sur le disque, sauf si vous appelez sync ; pour les fichiers temporaires comme celui-ci avec une courte durée de vie, il n’est pas improbable que beaucoup d’entre eux ne soient jamais écrits sur le disque.

    J’ai fait quelques tests du code d’Irfan, la modification du code de Irfan par Jerry et mon code, en utilisant le programme pilote suivant:

     #!/bin/sh mapimpl=$1 numkeys=$2 numvals=$3 . ./${mapimpl}.sh #/ < - fix broken stack overflow syntax highlighting for (( i = 0 ; $i < $numkeys ; i += 1 )) do for (( j = 0 ; $j < $numvals ; j += 1 )) do put "newMap" "key$i" "value$j" get "newMap" "key$i" done done 

    Les resultats:

         $ time ./driver.sh irfan 10 5
    
         vrai 0m0.975s
         utilisateur 0m0.280s
         sys 0m0.691s
    
         $ time ./driver.sh brian 10 5
    
         vrai 0m0.226s
         utilisateur 0m0.057s
         sys 0m0.123s
    
         $ time ./driver.sh jerry 10 5
    
         vrai 0m0.706s
         utilisateur 0m0.228s
         sys 0m0.530s
    
         $ time ./driver.sh irfan 100 5
    
         vrai 0m10.633s
         utilisateur 0m4.366s
         sys 0m7.127s
    
         $ time ./driver.sh brian 100 5
    
         vrai 0m1.682s
         utilisateur 0m0.546s
         sys 0m1.082s
    
         $ time ./driver.sh jerry 100 5
    
         vrai 0m9.315s
         utilisateur 0m4.565s
         sys 0m5.446s
    
         $ time ./driver.sh irfan 10 500
    
         vrai 1m46.197s
         utilisateur 0m44.869s
         sys 1m12.282s
    
         $ time ./driver.sh brian 10 500
    
         vrai 0m16.003s
         utilisateur 0m5.135s
         sys 0m10.396s
    
         $ time ./driver.sh jerry 10 500
    
         vrai 1m24.414s
         utilisateur 0m39.696s
         sys 0m54.834s
    
         $ time ./driver.sh irfan 1000 5
    
         vrai 4m25.145s
         utilisateur 3m17.286s
         sys 1m21.490s
    
         $ time ./driver.sh brian 1000 5
    
         vrai 0m19.442s
         utilisateur 0m5.287s
         sys 0m10.751s
    
         $ time ./driver.sh jerry 1000 5
    
         vrai 5m29.136s
         utilisateur 4m48.926s
         sys 0m59.336s
    
    
     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 
     #################################################################### # Bash v3 does not support associative arrays # and we cannot use ksh since all generic scripts are on bash # Usage: map_put map_name key value # function map_put { alias "${1}$2"="$3" } # map_get map_name key # @return value # function map_get { alias "${1}$2" | awk -F"'" '{ print $2; }' } # map_keys map_name # @return map keys # function map_keys { alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }' } 

    Exemple:

     mapName=$(basename $0)_map_ map_put $mapName "name" "Irfan Zulfiqar" map_put $mapName "designation" "SSE" for key in $(map_keys $mapName) do echo "$key = $(map_get $mapName $key) done 

    Bash4 supporte cela en natif. N’utilisez pas grep ou eval , ils sont les plus moche des hacks.

    Pour une réponse détaillée et détaillée avec un exemple de code, voir: https://stackoverflow.com/questions/3467959

    Maintenant, répondez à cette question.

    Les scripts suivants simulent des tableaux associatifs dans des scripts shell. C’est simple et très facile à comprendre.

    Map n’est rien d’autre qu’une chaîne sans fin sur laquelle keyValuePair est enregistré sous la forme –name = Irfan –designation = SSE –company = My: SP: Own: SP: Company

    les espaces sont remplacés par ‘: SP:’ pour les valeurs

     put() { if [ "$#" != 3 ]; then exit 1; fi mapName=$1; key=$2; value=`echo $3 | sed -e "s/ /:SP:/g"` eval map="\"\$$mapName\"" map="`echo "$map" | sed -e "s/--$key=[^ ]*//g"` --$key=$value" eval $mapName="\"$map\"" } get() { mapName=$1; key=$2; valueFound="false" eval map=\$$mapName for keyValuePair in ${map}; do case "$keyValuePair" in --$key=*) value=`echo "$keyValuePair" | sed -e 's/^[^=]*=//'` valueFound="true" esac if [ "$valueFound" == "true" ]; then break; fi done value=`echo $value | sed -e "s/:SP:/ /g"` } put "newMap" "name" "Irfan Zulfiqar" put "newMap" "designation" "SSE" put "newMap" "company" "My Own Company" get "newMap" "company" echo $value get "newMap" "name" echo $value 

    edit: J’ai ajouté une autre méthode pour récupérer toutes les clés.

     getKeySet() { if [ "$#" != 1 ]; then exit 1; fi mapName=$1; eval map="\"\$$mapName\"" keySet=` echo $map | sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g" ` } 

    Pour Bash 3, il existe un cas particulier qui offre une solution simple et agréable:

    Si vous ne voulez pas manipuler beaucoup de variables, ou si les clés sont simplement des identificateurs de variable non valides, et que votre masortingce est garantie avec moins de 256 éléments , vous pouvez abuser des valeurs de retour de fonction. Cette solution ne nécessite aucun sous-shell car la valeur est facilement disponible en tant que variable, ni en tant qu’itération, de sorte que les performances cèdent. C’est aussi très lisible, presque comme la version Bash 4.

    Voici la version la plus simple:

     hash_index() { case $1 in 'foo') return 0;; 'bar') return 1;; 'baz') return 2;; esac } hash_vals=("foo_val" "bar_val" "baz_val"); hash_index "foo" echo ${hash_vals[$?]} 

    Rappelez-vous, utilisez des guillemets simples au case , sinon, il est sujet à la globalisation. Vraiment utile pour les hachages statiques / gelés depuis le début, mais on pourrait écrire un générateur d’index à partir d’un hash_keys=() .

    Attention, le premier paramètre par défaut, vous pouvez donc mettre de côté l’élément zeroth:

     hash_index() { case $1 in 'foo') return 1;; 'bar') return 2;; 'baz') return 3;; esac } hash_vals=("", # sort of like returning null/nil for a non existent key "foo_val" "bar_val" "baz_val"); hash_index "foo" || echo ${hash_vals[$?]} # It can't get more readable than this 

    Avertissement: la longueur est maintenant incorrecte.

    Alternativement, si vous voulez garder l’indexation basée sur zéro, vous pouvez réserver une autre valeur d’index et vous prémunir contre une clé inexistante, mais c’est moins lisible:

     hash_index() { case $1 in 'foo') return 0;; 'bar') return 1;; 'baz') return 2;; *) return 255;; esac } hash_vals=("foo_val" "bar_val" "baz_val"); hash_index "foo" [[ $? -ne 255 ]] && echo ${hash_vals[$?]} 

    Ou, pour que la longueur rest correcte, décaler l’index de un:

     hash_index() { case $1 in 'foo') return 1;; 'bar') return 2;; 'baz') return 3;; esac } hash_vals=("foo_val" "bar_val" "baz_val"); hash_index "foo" || echo ${hash_vals[$(($? - 1))]} 

    Vous pouvez utiliser des noms de variables dynamics et laisser les noms de variables fonctionner comme les clés d’un hashmap.

    Par exemple, si vous avez un fichier d’entrée avec deux colonnes, name, credit, comme l’exemple ci-dessous, et que vous voulez additionner le revenu de chaque utilisateur:

     Mary 100 John 200 Mary 50 John 300 Paul 100 Paul 400 David 100 

    La commande ci-dessous résumera tout, en utilisant des variables dynamics comme clés, sous la forme de map _ $ {person} :

     while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log) 

    Pour lire les résultats:

     set | grep map 

    Le résultat sera:

     map_David=100 map_John=500 map_Mary=150 map_Paul=500 

    En développant ces techniques, je développe sur GitHub une fonction qui fonctionne comme un object HashMap , shell_map .

    Pour créer des " instances HashMap ", la fonction shell_map peut créer des copies de lui-même sous différents noms. Chaque nouvelle copie de fonction aura une variable $ FUNCNAME différente. $ FUNCNAME est ensuite utilisé pour créer un espace de noms pour chaque instance de la carte.

    Les clés de la carte sont des variables globales, sous la forme $ FUNCNAME_DATA_ $ KEY, où $ KEY est la clé ajoutée à la carte. Ces variables sont des variables dynamics .

    Ci-dessous, je vais en mettre une version simplifiée pour que vous puissiez l'utiliser comme exemple.

     #!/bin/bash shell_map () { local METHOD="$1" case $METHOD in new) local NEW_MAP="$2" # loads shell_map function declaration test -n "$(declare -f shell_map)" || return # declares in the Global Scope a copy of shell_map, under a new name. eval "${_/shell_map/$2}" ;; put) local KEY="$2" local VALUE="$3" # declares a variable in the global scope eval ${FUNCNAME}_DATA_${KEY}='$VALUE' ;; get) local KEY="$2" local VALUE="${FUNCNAME}_DATA_${KEY}" echo "${!VALUE}" ;; keys) declare | grep -Po "(?< =${FUNCNAME}_DATA_)\w+((?=\=))" ;; name) echo $FUNCNAME ;; contains_key) local KEY="$2" compgen -v ${FUNCNAME}_DATA_${KEY} > /dev/null && return 0 || return 1 ;; clear_all) while read var; do unset $var done < <(compgen -v ${FUNCNAME}_DATA_) ;; remove) local KEY="$2" unset ${FUNCNAME}_DATA_${KEY} ;; size) compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l ;; *) echo "unsupported operation '$1'." return 1 ;; esac } 

    Usage:

     shell_map new credit credit put Mary 100 credit put John 200 for customer in `credit keys`; do value=`credit get $customer` echo "customer $customer has $value" done credit contains_key "Mary" && echo "Mary has credit!" 

    J’ai trouvé vrai, comme déjà mentionné, que la meilleure méthode consiste à écrire des clés / valeurs dans un fichier, puis à utiliser grep / awk pour les récupérer. Cela ressemble à toutes sortes d’E / S inutiles, mais le cache de disque se déclenche et le rend extrêmement efficace – beaucoup plus rapide que d’essayer de les stocker en mémoire en utilisant l’une des méthodes ci-dessus (comme le montrent les tests).

    Voici une méthode rapide et propre que j’aime bien:

     hinit() { rm -f /tmp/hashmap.$1 } hput() { echo "$2 $3" >> /tmp/hashmap.$1 } hget() { grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };' } hinit capitols hput capitols France Paris hput capitols Netherlands Amsterdam hput capitols Spain Madrid echo `hget capitols France` and `hget capitols Netherlands` and `hget capitols Spain` 

    Si vous souhaitez appliquer une valeur unique par clé, vous pouvez également effectuer une petite action grep / sed dans hput ().

    Quel dommage je n’ai pas vu la question avant – j’ai écrit la bibliothèque shell-framework qui contient entre autres les cartes (tableaux associatifs). La dernière version peut être trouvée ici .

    Exemple:

     #!/bin/bash #include map library shF_PATH_TO_LIB="/usr/lib/shell-framework" source "${shF_PATH_TO_LIB}/map" #simple example get/put putMapValue "mapName" "mapKey1" "map Value 2" echo "mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")" #redefine old value to new putMapValue "mapName" "mapKey1" "map Value 1" echo "after change mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")" #add two new pairs key/values and print all keys putMapValue "mapName" "mapKey2" "map Value 2" putMapValue "mapName" "mapKey3" "map Value 3" echo -e "mapName keys are \n$(getMapKeys "mapName")" #create new map putMapValue "subMapName" "subMapKey1" "sub map Value 1" putMapValue "subMapName" "subMapKey2" "sub map Value 2" #and put it in mapName under key "mapKey4" putMapValue "mapName" "mapKey4" "subMapName" #check if under two key were placed maps echo "is map mapName[mapKey3]? - $(if isMap "$(getMapValue "mapName" "mapKey3")" ; then echo Yes; else echo No; fi)" echo "is map mapName[mapKey4]? - $(if isMap "$(getMapValue "mapName" "mapKey4")" ; then echo Yes; else echo No; fi)" #print map with sub maps printf "%s\n" "$(mapToSsortingng "mapName")" 

    Shell n’a pas de carte intégrée comme la structure de données, j’utilise une chaîne brute pour décrire des éléments comme celui-ci:

     ARRAY=( "item_A|attr1|attr2|attr3" "item_B|attr1|attr2|attr3" "..." ) 

    quand extraire des éléments et ses atsortingbuts:

     for item in "${ARRAY[@]}" do item_name=$(echo "${item}"|awk -F "|" '{print $1}') item_attr1=$(echo "${item}"|awk -F "|" '{print $2}') item_attr2=$(echo "${item}"|awk -F "|" '{print $3}') echo "${item_name}" echo "${item_attr1}" echo "${item_attr2}" done 

    Cela ne semble pas intelligent que la réponse des autres, mais facile à comprendre pour les nouveaux utilisateurs.

    J’ai modifié la solution de Vadim avec les éléments suivants:

     #################################################################### # Bash v3 does not support associative arrays # and we cannot use ksh since all generic scripts are on bash # Usage: map_put map_name key value # function map_put { alias "${1}$2"="$3" } # map_get map_name key # @return value # function map_get { if type -p "${1}$2" then alias "${1}$2" | awk -F "'" '{ print $2; }'; fi } # map_keys map_name # @return map keys # function map_keys { alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }' } 

    Le changement est à map_get afin de l’empêcher de renvoyer des erreurs si vous demandez une clé qui n’existe pas, bien que l’effet secondaire soit qu’il ignore également les cartes manquantes, mais il convient mieux à mon cas d’utilisation voulait vérifier une clé afin de sauter des éléments dans une boucle.

    Il y a plusieurs années, j’ai écrit une bibliothèque de scripts pour bash qui supportait entre autres les tableaux associatifs (journalisation, fichiers de configuration, prise en charge étendue des arguments de ligne de commande, génération d’aide, tests unitaires, etc.). La bibliothèque contient un wrapper pour les tableaux associatifs et bascule automatiquement vers le modèle approprié (interne pour bash4 et émuler pour les versions précédentes). Il s’appelait shell-framework et était hébergé sur origo.ethz.ch mais aujourd’hui la ressource est fermée. Si quelqu’un en a encore besoin, je peux le partager avec vous.

    Réponse tardive, mais envisagez d’aborder le problème de cette manière, en utilisant la lecture intégrée bash, comme illustré dans l’extrait de code d’un script de pare-feu uf qui suit. Cette approche a l’avantage d’utiliser autant d’ensembles de champs délimités (pas seulement 2) que l’on souhaite. Nous avons utilisé le | délimiteur car les spécificateurs de plage de ports peuvent nécessiter un 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 

    Ajouter une autre option, si jq est disponible:

     export NAMES="{ \"Mary\":\"100\", \"John\":\"200\", \"Mary\":\"50\", \"John\":\"300\", \"Paul\":\"100\", \"Paul\":\"400\", \"David\":\"100\" }" export NAME=David echo $NAMES | jq --arg v "$NAME" '.[$v]' | tr -d '"'