Pergunta

Nós necessário um script que simula Matrizes associativas ou Mapa como estrutura de dados para Shell Scripting, qualquer corpo?

Foi útil?

Solução

Para adicionar de Irfan resposta , aqui é uma versão mais curta e mais rápida de get() uma vez 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' )"
}

Outras dicas

Outra opção, se a portabilidade não é a sua principal preocupação, é usar arrays associativos que estão embutidos no shell. Isso deve funcionar no bash 4.0 (já disponível na maioria das grandes distros, embora não no OS X a menos que você instalá-lo sozinho), 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, você pode precisar de fazer um typeset -A newmap vez de declare -A newmap, ou em alguns, pode não ser necessário em tudo.

Outra não-festa de 4 vias.

#!/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 jogar uma instrução if para realizar buscas em lá também. if [[$ var = ~ / blah /]]. ou o que quer.

Eu acho que você precisa dar um passo para trá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 rápida e eficiente. Você também pode querer ser capaz de interagir sobre as chaves para recuperar cada par de chaves de valor, ou as chaves de exclusão e seus valores associados.

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

Realmente, tudo que você precisa ter uma matriz associativa em programação de shell é um diretório temporário. mktemp -d é o seu construtor de matriz associativa:

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

Se você não se sente como o uso de echo e cat, você sempre pode escrever alguns pequenos invólucros; estes queridos são modelados fora de Irfan de, embora apenas saída o valor em vez de definir variáveis ??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

Editar : Esta abordagem é realmente um pouco mais rápido do que a busca linear usando sed sugerido pela pergunta, bem como mais robusto (permite chaves e valores para conter -, =, espaço, qnd ": SP:"). O fato de que ele usa o sistema de arquivos não torná-lo lento; esses arquivos são, na verdade, não garantido para ser escrito para o disco a menos que você chamar sync; para arquivos temporários como este com um tempo de vida curto, não é improvável que muitos deles nunca serão gravados no disco.

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

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

    $ 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

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

Bash4 suporta isso nativamente. Não use grep ou eval, eles são o mais feio de hacks.

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

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

Agora responder a esta questão.

seguintes scripts Simula arrays associativos em shell scripts. É simples e muito fácil de entender.

Mapa é nada, mas uma seqüência interminável que tem KeyValuePair salvo como --name = Irfan --designation = SSE --company = My: SP: Próprio: SP: Empresa

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

Editar: Apenas acrescentou outro método para buscar todas as teclas

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

    mapName=$1; 

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

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
          `
}

Para Bash 3, não é um caso particular que tem uma agradável e simples solução:

Se você não quer lidar com uma série de variáveis, ou as teclas são simplesmente identificadores de variáveis ??inválida, e sua matriz está garantido para ter menos de 256 itens , você pode abusar valores de retorno de função. Esta solução não requer qualquer subshell como o valor está prontamente disponível como uma variável, nem qualquer iteração de modo que os gritos de desempenho. Também é muito legível, quase como a versão 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, usar aspas simples em case, então é sujeito a englobamento. Realmente útil para estáticos / hashes congelados desde o início, mas pode-se escrever um gerador de índice de uma matriz hash_keys=().

Watch out, o padrão é o primeiro, então você pode querer definir elemento de lado 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 agora é incorreta

Como alternativa, se você quiser manter a indexação baseada em zero, você pode reservar um outro valor do í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, índice compensado 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 de variáveis ??dinâmicas e deixar as variáveis ??nomes funcionam como as teclas de um hashmap.

Por exemplo, se você tem um arquivo de entrada com duas colunas, nome, crédito, como o exemplo abaixo, e você deseja somar a renda de cada usuário:

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

O abaixo comando irá resumir tudo, utilizando variáveis ??dinâmicas como chaves, na forma de Mapa _ $ {pessoa} :

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 sobre estas técnicas, eu estou desenvolvendo no GitHub uma função que funciona como um HashMap objeto , shell_map .

A fim de criar " casos HashMap " função shell_map é capaz criar cópias de si mesmo sob nomes diferentes. Cada nova cópia função terá uma variável $ funcname diferente. $ Funcname em seguida, é usado para criar um espaço de nomes para cada instância do Mapa.

As chaves de mapa são variáveis ??globais, na forma $ FUNCNAME_DATA_ $ KEY, onde $ KEY é a chave adicionado ao Mapa. Essas variáveis ??são variáveis ??.

Abaixo eu vou colocar uma versão simplificada do mesmo modo que você pode 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!"

Que pena eu não vi a pergunta antes - Eu já escreveu biblioteca shell-framework , que contém, entre outros, os mapas (arrays associativos). A última versão dele 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")"

Eu achei verdade, como já mencionado, que o método de melhor desempenho é escrever chave / vals para um arquivo, e depois usar grep / awk para recuperá-los. Parece que todos os tipos de IO desnecessário, mas de cache de disco entra em ação e faz com que seja extremamente eficiente -. Muito mais rápido do que tentar armazená-los na memória usando um dos métodos acima (como os benchmarks mostram)

Aqui está uma rápida, método 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 fazer valer de valor único por chave, você poderia também fazer um / ação sed pouco grep em hput ().

vários anos atrás eu escrevi biblioteca de script para a festança que apoiou arrays associativos entre outras funcionalidades (logging, arquivos de configuração, suporte estendido para o argumento de linha de comando, geram ajuda, testes unitários, etc). A biblioteca contém um invólucro para matrizes de associação e comuta automaticamente para o modelo (interno para bash4 e imitar para versões anteriores) apropriado. Foi chamado shell-estrutura e hospedado no origo.ethz.ch mas hoje o recurso é fechada. Se alguém ainda precisa dele eu posso compartilhar com você.

Shell não têm built-in mapa como estrutura de dados, eu uso corda crua para descrever itens como o seguinte:

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

quando os itens extrair 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

Esta parece ser não inteligente do que resposta de outras pessoas, mas fácil de entender para novas pessoas que desembolsar.

Adicionando uma outra opção, se jq está 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 '"' 

Eu modifiquei 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, a fim de impedi-lo de retornar erros se você solicitar uma chave que não existe, embora o efeito colateral é que ele também irá ignorar silenciosamente mapas em falta, mas adequados meu caso de uso de melhor desde que eu só queria verificar se há uma chave, a fim de ignorar itens em um loop.

resposta tardia, mas considere abordar o problema desta forma, usando o builtin festa ler , como ilustrado dentro do trecho de código a partir de um script de firewall UFW que se segue. Esta abordagem tem a vantagem de usar tantos conjuntos campo delimitado (não apenas 2) como são desejados. Temos utilizado o | delimitador porque especificadores intervalo de portas pode exigir dois pontos, ou seja, 60.01. 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
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top