Matrizes associativas em scripts do Shell

Precisamos de um script que simule matrizes associativas ou mapear como estrutura de dados para scripts de shell, qualquer corpo?

Para adicionar à resposta de Irfan , aqui está uma versão mais curta e mais rápida de get() já que não requer iteração sobre o conteúdo do mapa:

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

Outra opção, se a portabilidade não for sua principal preocupação, é usar matrizes associativas que são incorporadas ao shell. Isso deve funcionar no bash 4.0 (disponível agora na maioria das principais distribuições, embora não no OS X, a menos que você mesmo o instale), ksh e zsh:

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

Dependendo do shell, talvez seja necessário fazer um typeset -A newmap vez de declare -A newmap ou, em alguns, pode não ser necessário.

Outro não-bash 4 caminho.

 #!/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" 

Você poderia lançar uma declaração if para procurar lá também. se [[$ var = ~ / blah /]]. como queiras.

Eu acho que você precisa dar um passo atrás e pensar sobre o que um mapa, ou matriz associativa, realmente é. Tudo o que é uma maneira de armazenar um valor para uma determinada chave e obter esse valor de volta de forma rápida e eficiente. Você também pode querer iterar sobre as chaves para recuperar cada par de valores-chave ou excluir chaves e seus valores associados.

Agora, pense em uma estrutura de dados que você usa o tempo todo no script shell, e até mesmo no shell sem escrever um script, que tenha essas propriedades. Perplexo? É o sistema de arquivos.

Realmente, tudo que você precisa para ter um array associativo na programação shell é um diretório temporário. mktemp -d é seu construtor de array associativo:

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

Se você não sente vontade de usar o echo e o cat , pode sempre escrever alguns pequenos invólucros; esses são modelados a partir do Irfan, embora eles apenas exibam o valor em vez de definir variables ​​arbitrárias como $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 : Esta abordagem é realmente um pouco mais rápida que a pesquisa linear usando sed sugerida pelo questionador, bem como mais robusta (permite que chaves e valores contenham -, =, espaço, qnd “: SP:”). O fato de usar o sistema de arquivos não o torna lento; Na verdade, esses arquivos nunca são garantidos para serem gravados no disco, a menos que você chame de sync ; Para arquivos temporários como este com um tempo de vida curto, não é improvável que muitos deles nunca sejam gravados no disco.

Fiz alguns benchmarks do código de Irfan, a modificação de Jerry do código de Irfan e meu código, usando o seguinte programa de driver:

 #!/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 

Os resultados:

     $ hora ./driver.sh irfan 10 5

     0m0.975s reais
     usuário 0m0.280s
     sys 0m0.691s

     $ hora ./driver.sh brian 10 5

     0m0.226s real
     usuário 0m0.057s
     sys 0m0.123s

     $ hora ./driver.sh jerry 10 5

     real 0m0.706s
     usuário 0m0.228s
     sys 0m0.530s

     $ hora ./driver.sh irfan 100 5

     real 0m10.633s
     usuário 0m4.366s
     sys 0m7.127s

     $ hora ./driver.sh brian 100 5

     real 0m1.682s
     usuário 0m0.546s
     sys 0m1.082s

     $ hora ./driver.sh jerry 100 5

     0m9.315s real
     usuário 0m4.565s
     sys 0m5.446s

     $ hora ./driver.sh irfan 10 500

     real 1m46.197s
     usuário 0m44.869s
     sys 1m12.282s

     $ hora ./driver.sh brian 10 500

     real 0m16.003s
     usuário 0m5.135s
     sys 0m10.396s

     $ hora ./driver.sh jerry 10 500

     real 1m24.414s
     usuário 0m39.696s
     sys 0m54.834s

     $ hora ./driver.sh irfan 1000 5

     real 4m25.145s
     usuário 3m17.286s
     sys 1m21.490s

     $ hora ./driver.sh brian 1000 5

     real 0m19.442s
     usuário 0m5.287s
     sys 0m10.751s

     $ hora ./driver.sh jerry 1000 5

     5m29.136s reais
     usuário 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; }' } 

Exemplo:

 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 

O Bash4 suporta isso nativamente. Não use grep ou eval , eles são os mais feios dos hacks.

Para uma resposta detalhada detalhada com o código de exemplo, consulte: https://stackoverflow.com/questions/3467959

Agora respondendo a essa pergunta.

Os scripts a seguir simulam matrizes associativas em scripts de shell. É simples e muito fácil de entender.

Mapa não é nada, mas uma string interminável que possui keyValuePair salva como –name = Irfan – designation = SSE –company = Meu: SP: Own: SP: Company

espaços são substituídos por ‘: SP:’ para valores

 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: acabou de adicionar outro método para buscar todas as chaves.

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

Para o Bash 3, há um caso particular que possui uma solução simples e interessante:

Se você não quiser lidar com muitas variables, ou se as chaves forem simplesmente identificadores de variables ​​inválidos, e sua matriz tiver menos de 256 itens , você poderá abusar dos valores de retorno da function. Essa solução não requer nenhum subshell, pois o valor está prontamente disponível como uma variável, nem qualquer iteração para que o desempenho grite. Também é muito legível, quase como a versão do Bash 4.

Aqui está a versão mais básica:

 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[$?]} 

Lembre-se, use aspas simples, case contrário, está sujeito a globbing. Realmente útil para hashes estáticos / congelados desde o início, mas pode-se escrever um gerador de índice a partir de uma hash_keys=() .

Cuidado, o padrão é o primeiro, então você pode querer anular o elemento 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 

Advertência: o comprimento está agora incorreto.

Alternativamente, se você quiser manter a indexação baseada em zero, você pode reservar outro valor de índice e se proteger contra uma chave inexistente, mas é menos legível:

 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, para manter o comprimento correto, o índice de deslocamento por um:

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

Você pode usar nomes dynamics de variables ​​e deixar que os nomes das variables ​​funcionem como as chaves de um hashmap.

Por exemplo, se você tiver um arquivo de input com duas colunas, nome, crédito, como o exemplo abaixo e quiser sumr a receita de cada usuário:

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

O comando abaixo resumirá tudo, usando variables ​​dinâmicas como chaves, na forma de map _ $ {person} :

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

Para ler os resultados:

 set | grep map 

A saída será:

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

Elaborando essas técnicas, estou desenvolvendo no GitHub uma function que funciona como um object HashMap , shell_map .

Para criar " instâncias do HashMap ", a function shell_map pode criar cópias de si mesmo sob nomes diferentes. Cada nova cópia de function terá uma variável $ FUNCNAME diferente. $ FUNCNAME então é usado para criar um namespace para cada instância do Mapa.

As chaves do mapa são variables ​​globais, no formato $ FUNCNAME_DATA_ $ KEY, em que $ KEY é a chave adicionada ao Mapa. Essas variables ​​são variables ​​dinâmicas .

Abaixo, colocarei uma versão simplificada para que você possa usar como exemplo.

 #!/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 } 

Uso:

 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!" 

Eu achei verdadeiro, como já mencionado, que o método de melhor desempenho é escrever key / vals em um arquivo, e então usar o grep / awk para recuperá-los. Soa como todo tipo de IO desnecessário, mas o cache de disco entra em ação e o torna extremamente eficiente – muito mais rápido do que tentar armazená-los na memory usando um dos methods acima (como mostram os benchmarks).

Aqui está um método rápido e limpo que eu gosto:

 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` 

Se você quisesse aplicar valor único por chave, você também poderia fazer uma pequena ação grep / sed em hput ().

Pena que eu não tenha visto a pergunta antes – eu escrevi o shell-framework da biblioteca que contém, entre outros, os mapas (matrizes associativas). A última versão pode ser encontrada aqui .

Exemplo:

 #!/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")" 

Shell não tem um mapa embutido como estrutura de dados, eu uso string raw para descrever itens como esse:

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

quando extrair itens e seus atributos:

 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 

Isso parece não ser inteligente do que a resposta de outras pessoas, mas é fácil de entender para que novas pessoas descubram.

Modifiquei a solução de Vadim com o seguinte:

 #################################################################### # 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; }' } 

A mudança é map_get para evitar que ele retorne erros se você solicitar uma chave que não existe, mas o efeito colateral é que ela também silenciosamente ignorará os mapas ausentes, mas se adequou melhor ao meu caso de uso, já que eu queria verificar uma chave para pular itens em um loop.

Há vários anos, escrevi a biblioteca de scripts para o bash, que suportava matrizes associativas, entre outros resources (criação de log, arquivos de configuração, suporte estendido para argumento de linha de comando, geração de ajuda, teste de unidade, etc.). A biblioteca contém um wrapper para matrizes associativas e alterna automaticamente para o modelo apropriado (interno para bash4 e emular para versões anteriores). Foi chamado shell-framework e hospedado em origo.ethz.ch, mas hoje o recurso está fechado. Se alguém ainda precisar, posso compartilhar com você.

Resposta tardia, mas considere abordar o problema dessa maneira, usando o bash builtin lido conforme ilustrado no snippet de código de um script de firewall ufw a seguir. Essa abordagem tem a vantagem de usar tantos conjuntos de campos delimitados (não apenas 2) quantos forem desejados. Nós usamos o | delimitador porque os especificadores de intervalo de porta podem requerer dois pontos, ou seja, 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 

Adicionando outra opção, se jq estiver disponível:

 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 '"'