Tableaux associatifs dans les scripts Shell

nous avions besoin d'un script qui simule des tableaux associatifs ou une structure de données semblable à une carte pour les scripts Shell, n'importe quel corps?

100
demandé sur Sridhar Ratnakumar 2009-03-27 10:37:40

17 réponses

pour ajouter à 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=; key=

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*//" -e 's/:SP:/ /g' )"
}
21
répondu Jerry Penner 2017-05-23 10:31:12

une Autre option, si la portabilité n'est pas votre préoccupation principale, est d'utiliser des tableaux associatifs qui sont intégrés dans la coque. Cela devrait fonctionner dans bash 4.0 (disponible maintenant sur la plupart des distros majeurs, mais pas sur OS X à moins que vous ne 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 la coque, vous pouvez avoir besoin de faire un typeset -A newmap au lieu de declare -A newmap , ou dans certains il peut ne pas être nécessaire du tout.

123
répondu Brian Campbell 2016-01-28 15:15:29

un autre 4 way non-bash.

#!/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 pourriez jeter une déclaration si pour la recherche là-dedans aussi bien. if [[ $var =~ /bla/ ]]. ou que ce soit.

83
répondu Bubnoff 2012-11-15 17:21:12

je pense que vous devez prendre du recul et réfléchir à ce qu'est vraiment une carte, ou un tableau associatif. Tout c'est une façon de stocker une valeur pour une clé donnée, et d'obtenir que la valeur de retour rapidement et efficacement. Vous pouvez également vouloir être en mesure d'itérer sur les clés pour récupérer chaque paire de valeur clé, ou supprimer les 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 script, qui a ces propriétés. Perplexe? C'est le système de fichiers.

Vraiment, vous devez avoir un tableau associatif dans la programmation shell est un répertoire temp. mktemp -d est votre constructeur de tableaux associatifs:

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

si vous n'avez pas envie d'utiliser echo et cat , vous pouvez toujours écrire quelques petits wrappers; ceux-ci sont modélisés à partir D'Irfan, bien qu'ils produisent juste la valeur plutôt que de définir arbitraire variables comme $value :

#!/bin/sh

prefix=$(basename -- ""151910920"")
mapdir=$(mktemp -dt ${prefix})
trap 'rm -r ${mapdir}' EXIT

put() {
  [ "$#" != 3 ] && exit 1
  mapname=; key=; value=
  [ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}"
  echo $value >"${mapdir}/${mapname}/${key}"
}

get() {
  [ "$#" != 2 ] && exit 1
  mapname=; key=
  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 fait un peu plus rapide que la recherche linéaire utilisant sed suggéré par l'auteur de la question, ainsi que plus robuste (il permet des clés et des valeurs pour contenir -, =, espace, qnd": SP:"). Le fait qu'il utilise le système de fichiers ne le rend pas lent; ces fichiers ne sont en fait jamais garantis d'être écrits sur le disque à moins que vous n'appeliez sync ; pour les fichiers temporaires comme celui-ci avec un courte durée de vie, il n'est pas improbable que beaucoup d'entre eux ne sera jamais écrit sur disque.

j'ai fait quelques repères du code D'Irfan, de la modification de Jerry du code D'Irfan, et de mon code, en utilisant le programme de pilote suivant:

#!/bin/sh

mapimpl=
numkeys=
numvals=

. ./${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 résultats:

    $ time ./driver.sh irfan 10 5

    real    0m0.975s
    user    0m0.280s
    sys     0m0.691s

    $ time ./driver.sh brian 10 5

    real    0m0.226s
    user    0m0.057s
    sys     0m0.123s

    $ time ./driver.sh jerry 10 5

    real    0m0.706s
    user    0m0.228s
    sys     0m0.530s

    $ time ./driver.sh irfan 100 5

    real    0m10.633s
    user    0m4.366s
    sys     0m7.127s

    $ time ./driver.sh brian 100 5

    real    0m1.682s
    user    0m0.546s
    sys     0m1.082s

    $ time ./driver.sh jerry 100 5

    real    0m9.315s
    user    0m4.565s
    sys     0m5.446s

    $ time ./driver.sh irfan 10 500

    real    1m46.197s
    user    0m44.869s
    sys     1m12.282s

    $ time ./driver.sh brian 10 500

    real    0m16.003s
    user    0m5.135s
    sys     0m10.396s

    $ time ./driver.sh jerry 10 500

    real    1m24.414s
    user    0m39.696s
    sys     0m54.834s

    $ time ./driver.sh irfan 1000 5

    real    4m25.145s
    user    3m17.286s
    sys     1m21.490s

    $ time ./driver.sh brian 1000 5

    real    0m19.442s
    user    0m5.287s
    sys     0m10.751s

    $ time ./driver.sh jerry 1000 5

    real    5m29.136s
    user    4m48.926s
    sys     0m59.336s

31
répondu Brian Campbell 2017-05-02 19:33:58
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
14
répondu DigitalRoss 2009-09-30 00:12:16
####################################################################
# 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 ""=""
}

# map_get map_name key
# @return value
#
function map_get
{
    alias "" | awk -F"'" '{ print ; }'
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep  | cut -d'=' -f1 | awk -F"" '{print ; }'
}

exemple:

mapName=$(basename "151910920")_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
7
répondu Vadim 2011-06-28 15:28:24

Bash4 supporte cela nativement. Ne pas utiliser grep ou eval , ils sont les plus laids des hacks.

pour une réponse verbeuse détaillée avec Exemple de code, voir: https://stackoverflow.com/questions/3467959

5
répondu lhunath 2017-05-23 11:54:50

répond maintenant à cette question.

les scripts suivants simulent des tableaux associatifs dans des scripts shell. Son simple et très facile à comprendre.

carte est rien mais une chaîne sans fin qui a keyValuePair sauvé comme --nom=Irfan --désignation=ESS --société=Ma:SP:Propre:SP:la Société

les espaces sont remplacés par": SP: "pour les valeurs

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

get() {
    mapName=; key=; 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: vient D'être ajouté une autre méthode pour récupérer toutes les clés.

getKeySet() {
    if [ "$#" != 1 ]; 
    then 
        exit 1; 
    fi

    mapName=; 

    eval map="\"$$mapName\""

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--//g"
          `
}
3
répondu Irfan Zulfiqar 2009-04-14 08:11:06

pour Bash 3, Il y a un cas particulier qui a une solution simple et agréable:

si vous ne voulez pas manipuler beaucoup de variables, ou les clés sont simplement des identificateurs de variables invalides, et votre tableau est garanti d'avoir moins de 256 articles , vous pouvez abuser des valeurs de retour de fonction. Cette solution ne nécessite pas de sous-couche puisque la valeur est facilement disponible en tant que variable, ni aucune itération de sorte que la performance cri. C'est aussi très lisible, presque comme la version Bash 4.

Voici la version la plus basique:

hash_index() {
    case  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 dans case , sinon il est sujet à globbing. Très utile pour les hachages statiques / gelés dès le début, mais on pourrait écrire un générateur d'index à partir d'un tableau hash_keys=() .

attention, il est par défaut au premier, donc vous pouvez vouloir mettre de côté l'élément zeroth:

hash_index() {
    case  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 à base zéro, vous pouvez réserver une autre valeur d'index et vous prémunir contre une clé inexistante, mais elle est moins lisible:

hash_index() {
    case  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 conserver la longueur correcte, le décalage de l'indice par un:

hash_index() {
    case  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))]}
2
répondu Lloeki 2014-03-03 16:15:01

vous pouvez utiliser des noms de variables dynamiques et laisser les noms de variables fonctionner comme les clés d'une hashmap.

par exemple, si vous avez un fichier d'entrée avec deux colonnes, Nom, crédit, comme l'exemple ci-dessous, et 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 additionnera tout, en utilisant des variables dynamiques comme clés, sous la forme de map_ $ {person} :

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

à lire les résultats:

set | grep map

la sortie 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 HashMap Object , shell_map .

afin de créer " HashMap instances "la fonction shell_map est capable de 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 alors utilisé pour créer un namespace pour chaque instance Map.

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

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

#!/bin/bash

shell_map () {
    local METHOD=""

    case $METHOD in
    new)
        local NEW_MAP=""

        # 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/}"
    ;;
    put)
        local KEY=""  
        local VALUE=""

        # declares a variable in the global scope
        eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
    ;;
    get)
        local KEY=""
        local VALUE="${FUNCNAME}_DATA_${KEY}"
        echo "${!VALUE}"
    ;;
    keys)
        declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
    ;;
    name)
        echo $FUNCNAME
    ;;
    contains_key)
        local KEY=""
        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=""
        unset ${FUNCNAME}_DATA_${KEY}
    ;;
    size)
        compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
    ;;
    *)
        echo "unsupported operation ''."
        return 1
    ;;
    esac
}

Utilisation:

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!"
2
répondu Bruno Negrão Zica 2018-05-04 16:51:32

j'ai trouvé vrai, comme déjà mentionné, que la meilleure méthode performante est d'écrire la clé/vals à un fichier, puis utiliser grep/awk pour les récupérer. Cela ressemble à toutes sortes d'IO inutiles, mais le cache disque s'active 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 benchmarks).

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

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

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

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

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 voulez appliquez une valeur unique par clé, vous pouvez aussi faire une petite action grep/sed dans hput().

1
répondu Al P. 2010-02-09 17:19:59

quel dommage que 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 de celui-ci peut être trouvé 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" "$(mapToString "mapName")"
1
répondu Beggy 2011-03-03 09:08:32

Shell n'ont pas de carte intégrée comme la structure de données, j'utilise la chaîne brute pour décrire des éléments comme cela:

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

lors de l'extraction des éléments et de leurs attributs:

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

    echo "${item_name}"
    echo "${item_attr1}"
    echo "${item_attr2}"
done

cela ne semble pas intelligent que la réponse d'autres personnes, mais facile à comprendre pour les nouvelles personnes à shell.

1
répondu coanor 2015-08-05 08:38:09

j'ai modifié la solution de Vadim comme suit:

####################################################################
# 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 ""=""
}

# map_get map_name key
# @return value
#
function map_get {
    if type -p ""
        then
            alias "" | awk -F "'" '{ print ; }';
    fi
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep  | cut -d'=' -f1 | awk -F"" '{print ; }'
}

le changement est à map_get afin de l'empêcher de retourner les erreurs si vous demandez une clé qui n'existe pas, bien que l'effet secondaire est qu'il va également ignorer silencieusement les cartes manquantes, mais il convenait mieux à mon cas d'utilisation puisque je voulais juste vérifier une clé afin de sauter des éléments dans une boucle.

0
répondu Haravikk 2013-04-11 14:22:16

il y a plusieurs années, j'ai écrit une bibliothèque de scripts pour bash qui supporte les tableaux associatifs parmi d'autres fonctionnalités (journalisation, fichiers de configuration, prise en charge étendue de l'argument en ligne de commande, génération d'aide, test d'unité, etc.). La bibliothèque contient un wrapper pour les tableaux associatifs et passe automatiquement au modèle approprié (interne pour bash4 et émuler pour les versions précédentes). Il a été appelé shell-framework et hébergé à origo.ethz.ch mais aujourd'hui la ressource est fermée. Si quelqu'un l'a encore besoin je peux le partager avec vous.

0
répondu Beggy 2013-06-13 12:38:12

Retard de réponse, mais envisager d'aborder le problème de cette façon, 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
0
répondu AsymLabs 2015-09-15 16:58:41

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 '"' 
0
répondu critium 2018-07-13 20:11:25