web-dev-qa-db-fra.com

Tableaux associatifs dans les scripts Shell

Nous avions besoin d’un script qui simule des tableaux associatifs ou une structure semblable à Map pour le script shell, n’importe quel corps?

101
Irfan Zulfiqar

Pour ajouter à la réponse d'Irfan , voici une version plus courte et plus rapide de get() puisqu'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' )"
}
21
Jerry Penner

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 distributions majeures, 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 créer un typeset -A newmap au lieu de declare -A newmap ou, dans certains cas, cela ne sera peut-être pas nécessaire du tout.

131
Brian Campbell

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 effectuer une recherche. si [[$ var = ~/blah /]] . ou autre.

81
Bubnoff

Je pense que vous devez prendre du recul et réfléchir à ce qu'est réellement une carte ou un tableau associatif. Il s’agit simplement de stocker une valeur pour une clé donnée et de la récupérer rapidement et efficacement. Vous pouvez également vouloir pouvoir parcourir les clés pour extraire 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 simplement dans le Shell sans écrire de script, qui possède ces propriétés. Stumped? C'est le système de fichiers.

En réalité, tout ce dont vous avez besoin pour avoir un tableau associatif dans la programmation de 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 ne souhaitez pas utiliser echo et cat, vous pouvez toujours écrire quelques petits wrappers; ceux-ci sont calqués sur ceux d'Irfan, bien qu'ils ne produisent que la valeur plutôt que de définir des variables arbitraires telles que $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é bien plus rapide que la recherche linéaire utilisant sed suggérée par le questionneur, ainsi que plus robuste (elle permet aux clés et aux valeurs de contenir -, =, espace, qnd ": SP:" ). Le fait qu'il utilise le système de fichiers ne le ralentit pas; En réalité, il n’est jamais garanti que ces fichiers soient écrits sur le disque, à moins que vous n’appeliez sync; pour les fichiers temporaires de ce type ayant une durée de vie courte, il n’est pas improbable que beaucoup d’entre eux ne soient jamais écrits sur le disque.

J'ai fait quelques points de repère du code d'Irfan, la modification du code d'Irfan par Jerry, et mon code, à l'aide du programme de 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 

 réel 0m0,975s 
 utilisateur 0m0.280s 
 sys 0m0,691s 

 $ time ./driver.sh brian 10 5 

 réel 0m0.226s 
 utilisateur 0m0,057s 
 sys 0m0.123s 

 $ time ./driver.sh jerry 10 5 

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

 $ time ./driver.sh irfan 100 5 

 réel 0m10.633s 
 utilisateur 0m4,366s 
 sys 0m7.127s 

 $ time ./driver.sh brian 100 5 

 real 0m1.682s 
 utilisateur 0m0,546s 
 sys 0m1.082s 

 $ time ./driver.sh jerry 100 5 

 real 0m9.315s 
 utilisateur 0m4,565s 
 sys 0m5.446s 

 $ time ./driver.sh irfan 10 500 

 1m46.197s réel 
 utilisateur 0m44,869s 
 sys 1m12.282s 

 $ time ./driver.sh brian 10 500 

 réel 0m16.003s 
 utilisateur 0m5.135s 
 sys 0m10.396s 

 $ time ./driver.sh jerry 10 500 

 1m24.414s réel 
 utilisateur 0m39,696s 
 sys 0m54.834s 

 $ time ./driver.sh irfan 1000 5 

 4m25.145s réels 
 utilisateur 3m17.286s 
 sys 1m21.490s 

 $ time ./driver.sh brian 1000 5 

 réel 0m19.442s 
 utilisateur 0m5,287s 
 sys 0m10.751s 

 $ time ./driver.sh jerry 1000 5 

 réel 5m29.136s 
 utilisateur 4m48,926s 
 sys 0m59.336s 

32
Brian Campbell
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
15
DigitalRoss

Bash4 le supporte de manière native. Ne pas utiliser grep ou eval, ils sont les plus laids des hacks.

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

6
lhunath
####################################################################
# 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
6
Vadim

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

Si vous ne souhaitez pas gérer un grand nombre de variables ou si les clés sont simplement des identificateurs de variable non valides, et que votre tableau a forcément inférieur à 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 aucune itération, ce qui nuit aux performances. En outre, il est très lisible, presque comme la version Bash 4.

Voici la version la plus basique:

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 dans case, sinon il est sujet à un déplacement. Vraiment utile pour les hachages statiques/figés dès le début, mais on peut écrire un générateur d’index à partir d’un tableau hash_keys=().

Attention, le premier 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

Mise en garde: la longueur est maintenant incorrecte.

Sinon, si vous souhaitez conserver 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 $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 conserver la longueur correcte, offset index by one:

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))]}
2
Lloeki

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

Par exemple, si vous avez un fichier d'entrée à deux colonnes, nom, crédit, comme exemple ci-dessous, et que vous souhaitez additionner le revenu de chaque utilisateur:

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

La commande ci-dessous additionne 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)

Pour 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 exactement comme un objet HashMap Object, Shell_map

Afin de créer "HashMap instances", 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 différente $ FUNCNAME. $ FUNCNAME est ensuite utilisé pour créer un espace de noms pour chaque instance de Map. 

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 variables dynamiques .

Ci-dessous, je vais en mettre une version simplifiée pour que vous puissiez vous en servir 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!"
2

Répondant maintenant à cette question.

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

La carte n'est rien d'autre qu'une chaîne interminable dans laquelle keyValuePair est enregistré en tant que -- name = Irfan --designation = SSE --company = Mon: SP: Propriétaire: SP: Société

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: Vient d'ajouter 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"
          `
}
2
Irfan Zulfiqar

Quel dommage que je n'ai pas vu la question auparavant - j'ai écrit library Shell-framework qui contient entre autres les cartes (tableaux associatifs). La dernière version de celui-ci 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" "$(mapToString "mapName")"
1
Beggy

il y a plusieurs années, j'ai écrit une bibliothèque de scripts pour bash qui prenait en charge des tableaux associatifs, entre autres fonctionnalités (journalisation, fichiers de configuration, support étendu des arguments de ligne de commande, aide à la génération, tests d'unités, 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.

0
Beggy

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
critium

Comme je l'ai déjà mentionné, j'ai trouvé vrai que la méthode la plus performante 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 disque s'installe 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 ().

0
Al P.

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 ça

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

quand extraire les objets et leurs attributs:

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 semble être moins intelligent que la réponse des autres, mais facile à comprendre pour les nouveaux venus chez Shell.

0
coanor