Como definir tabelas de hash no Bash?

Qual é o equivalente dos dictionarys do Python, mas no Bash (deve funcionar no OS X e no Linux).

Bash 4

O Bash 4 suporta esse recurso. Certifique-se de hashbang do seu script é #!/usr/bin/env bash Usr #!/usr/bin/env bash ou #!/bin/bash ou qualquer outra coisa que faz referência bash e não sh . Certifique-se de estar executando o seu script, e não fazendo algo tolo como o sh script que faria com que o hashbang do bash fosse ignorado. Isso é básico, mas muitos continuam fracassando, daí a reiteração.

Você declara um array associativo fazendo:

 declare -A animals 

Você pode preenchê-lo com elementos usando o operador de atribuição de matriz normal:

 animals=( ["moo"]="cow" ["woof"]="dog") 

Ou mesclá-los:

 declare -A animals=( ["moo"]="cow" ["woof"]="dog") 

Em seguida, use-os apenas como matrizes normais. "${animals[@]}" expande os valores, "${!animals[@]}" (observe o ! ) expande as chaves. Não esqueça de citá-los:

 echo "${animals[moo]}" for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done 

Bash 3

Antes do bash 4, você não tem matrizes associativas. Não use eval para imitá-los . Você deve evitar o eval como a praga, porque é a praga do script de shell. A razão mais importante é que você não quer tratar seus dados como código executável (também há muitos outros motivos).

Primeiro e mais importante : considere atualizar para o bash 4. Sério. O futuro é agora , pare de viver no passado e sofra com isso forçando estúpidos hacks quebrados e feios em seu código e toda pobre alma ficou presa em mantê-lo.

Se você tem alguma desculpa boba por que você ” não pode atualizar “, declare é uma opção muito mais segura. Ele não avalia dados como código bash como o eval , e, como tal, não permite a injeção arbitrária de códigos com tanta facilidade.

Vamos preparar a resposta apresentando os conceitos:

Primeiro, indireção (sério; nunca use isso a menos que você esteja mentalmente doente ou tenha alguma outra desculpa ruim para escrever hacks).

 $ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}" cow 

Em segundo lugar, declare :

 $ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo" cow 

Traga-os juntos:

 # Set a value: declare "array_$index=$value" # Get a value: arrayGet() { local array=$1 index=$2 local i="${array}_$index" printf '%s' "${!i}" } 

Vamos usar isso:

 $ sound=moo $ animal=cow $ declare "animals_$sound=$animal" $ arrayGet animals "$sound" cow 

Nota: declare não pode ser colocado em uma function. Qualquer uso de declare dentro de uma function bash transforma a variável que ela cria local no escopo dessa function, o que significa que não podemos acessar ou modificar arrays globais com ela. (No bash 4 você pode usar declare -g para declarar variables ​​globais – mas no bash 4, você deveria estar usando matrizes associativas em primeiro lugar, não este hack).

Resumo

Atualize para o bash 4 e use declare -A . Se você não puder, considere mudar completamente para o awk antes de fazer hacks feios como descrito acima. E definitivamente ficar longe de hackery eval .

Há substituição de parâmetro, embora possa ser não-PC também … como indireção.

 #!/bin/bash # Array pretending to be a Pythonic dictionary 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 printf "%s is an extinct animal which likes to %s\n" "${ARRAY[1]%%:*}" "${ARRAY[1]##*:}" 

A maneira BASH 4 é melhor, claro, mas se você precisar de um hack … apenas um hack fará. Você poderia pesquisar o array / hash com técnicas semelhantes.

Isso é o que eu estava procurando aqui:

 declare -A hashmap hashmap["key"]="value" hashmap["key2"]="value2" echo "${hashmap["key"]}" for key in ${!hashmap[@]}; do echo $key; done for value in ${hashmap[@]}; do echo $value; done echo hashmap has ${#hashmap[@]} elements 

Isso não funcionou para mim com o bash 4.1.5:

 animals=( ["moo"]="cow" ) 

Você pode modificar ainda mais a interface hput () / hget () para que você tenha nomeado hashes da seguinte forma:

 hput() { eval "$1""$2"='$3' } hget() { eval echo '${'"$1$2"'#hash}' } 

e depois

 hput capitals France Paris hput capitals Netherlands Amsterdam hput capitals Spain Madrid echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain` 

Isso permite que você defina outros mapas que não entrem em conflito (por exemplo, ‘rcapitals’ que faz a pesquisa de país por cidade capital). Mas, de qualquer forma, acho que você verá que tudo isso é muito terrível, em termos de desempenho.

Se você realmente quer uma rápida busca por hash, há um terrível e terrível hack que realmente funciona muito bem. É isto: escreva sua chave / valores em um arquivo temporário, um por linha, então use ‘grep’ ^ $ key ” para pegá-los, usando pipes com cut ou awk ou sed ou o que for para recuperar os valores.

Como eu disse, parece terrível, e parece que deve ser lento e fazer todos os tipos de IO desnecessários, mas na prática é muito rápido (cache de disco é incrível, não é?), Mesmo para hash muito grande tabelas. Você tem que impor a sua própria unicidade, etc. Mesmo se você tiver apenas algumas centenas de inputs, o combo file / grep de saída será um pouco mais rápido – na minha experiência, várias vezes mais rápido. Também come menos memory.

Aqui está uma maneira de fazer isso:

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

Considere uma solução usando o bash builtin read conforme ilustrado no snippet de código de um script de firewall ufw que segue. 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 

Basta usar o sistema de arquivos

O sistema de arquivos é uma estrutura em tree que pode ser usada como um mapa hash. Sua tabela de hash será um diretório temporário, suas chaves serão nomes de arquivos e seus valores serão de arquivo. A vantagem é que ele pode lidar com enormes hashmaps e não requer um shell específico.

Criação de Hashtable

hashtable=$(mktemp -d)

Adicione um elemento

echo $value > $hashtable/$key

Leia um elemento

value=$(< $hashtable/$key)

atuação

Claro, é lento, mas não tão lento. Eu testei na minha máquina, com um SSD e btrfs , e ele faz cerca de 3000 elementos de leitura / gravação por segundo .

Eu concordo com o @lhunath e outros que o array associativo é o caminho a seguir com o Bash 4. Se você está preso ao Bash 3 (OSX, distribuições antigas que você não pode atualizar) você pode usar também expr, que deve estar em qualquer lugar, uma string e expressões regulares. Eu gosto especialmente quando o dictionary não é muito grande.

  1. Escolha dois separadores que você não usará em chaves e valores (por exemplo, ‘,’ e ‘:’)
  2. Escreva seu mapa como uma string (observe o separador ‘,’ também no início e no fim)

     animals=",moo:cow,woof:dog," 
  3. Use um regex para extrair os valores

     get_animal { echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")" } 
  4. Divida a string para listar os itens

     get_animal_items { arr=$(echo "${animals:1:${#animals}-2}" | tr "," "\n") for i in $arr do value="${i##*:}" key="${i%%:*}" echo "${value} likes to $key" done } 

Agora você pode usá-lo:

 $ animal = get_animal "moo" cow $ get_animal_items cow likes to moo dog likes to woof 

Eu realmente gostei da resposta de Al P, mas queria exclusividade reforçada mais barato, então eu levei um passo adiante – use um diretório. Existem algumas limitações óbvias (limites de arquivos de diretórios, nomes de arquivos inválidos), mas isso deve funcionar na maioria dos casos.

 hinit() { rm -rf /tmp/hashmap.$1 mkdir -p /tmp/hashmap.$1 } hput() { printf "$3" > /tmp/hashmap.$1/$2 } hget() { cat /tmp/hashmap.$1/$2 } hkeys() { ls -1 /tmp/hashmap.$1 } hdestroy() { rm -rf /tmp/hashmap.$1 } hinit ids for (( i = 0; i < 10000; i++ )); do hput ids "key$i" "value$i" done for (( i = 0; i < 10000; i++ )); do printf '%s\n' $(hget ids "key$i") > /dev/null done hdestroy ids 

Ele também executa um pouco melhor nos meus testes.

 $ time bash hash.sh real 0m46.500s user 0m16.767s sys 0m51.473s $ time bash dirhash.sh real 0m35.875s user 0m8.002s sys 0m24.666s 

Apenas pensei que eu participaria. Felicidades!

Editar: Adicionando hdestroy ()

Duas coisas, você pode usar memory em vez de / tmp em qualquer kernel 2.6 usando / dev / shm (Redhat) outras distros podem variar. Também o hget pode ser reimplementado usando o seguinte comando:

 function hget { while read key idx do if [ $key = $2 ] then echo $idx return fi done < /dev/shm/hashmap.$1 } 

Além disso, assumindo que todas as chaves são exclusivas, o retorno produz um curto-circuito no loop de leitura e evita a leitura de todas as inputs. Se a sua implementação puder ter chaves duplicadas, simplesmente deixe de fora o retorno. Isso economiza a despesa de ler e bifurcar grep e awk. Usando / dev / shm para ambas as implementações, obteve-se o seguinte usando o tempo hget em um hash de 3 inputs pesquisando a última input:

Grep / Awk:

 hget() { grep "^$2 " /dev/shm/hashmap.$1 | awk '{ print $2 };' } $ time echo $(hget FD oracle) 3 real 0m0.011s user 0m0.002s sys 0m0.013s 

Leia / echo:

 $ time echo $(hget FD oracle) 3 real 0m0.004s user 0m0.000s sys 0m0.004s 

em várias invocações eu nunca vi menos do que uma melhoria de 50%. Isso tudo pode ser atribuído ao fork over, devido ao uso de /dev/shm .

Bash 3 solução:

Ao ler algumas das respostas, eu coloquei uma rápida pequena function e gostaria de contribuir de volta para ajudar os outros.

 # Define a hash like this MYHASH=("firstName:Milan" "lastName:Adamovsky") # Function to get value by key getHashKey() { declare -a hash=("${!1}") local key local lookup=$2 for key in "${hash[@]}" ; do KEY=${key%%:*} VALUE=${key#*:} if [[ $KEY == $lookup ]] then echo $VALUE fi done } # Function to get a list of all keys getHashKeys() { declare -a hash=("${!1}") local KEY local VALUE local key local lookup=$2 for key in "${hash[@]}" ; do KEY=${key%%:*} VALUE=${key#*:} keys+="${KEY} " done echo $keys } # Here we want to get the value of 'lastName' echo $(getHashKey MYHASH[@] "lastName") # Here we want to get all keys echo $(getHashKeys MYHASH[@]) 

Antes do bash 4, não há uma boa maneira de usar matrizes associativas no bash. Sua melhor aposta é usar uma linguagem interpretada que realmente tenha suporte para essas coisas, como o awk. Por outro lado, o bash 4 suporta-os.

Quanto a menos boas maneiras no bash 3, aqui está uma referência que pode ajudar: http://mywiki.wooledge.org/BashFAQ/006

Um colega de trabalho acabou de mencionar este tópico. Eu implementei independentemente tabelas de hash no bash, e não é dependente da versão 4. De um post meu em março de 2010 (antes de algumas das respostas aqui …) intitulado Hash tables in bash :

 # Here's the hashing function ht() { local ht=`echo "$*" |cksum`; echo "${ht//[!0-9]}"; } # Example: myhash[`ht foo bar`]="a value" myhash[`ht baz baf`]="b value" echo ${myhash[`ht baz baf`]} # "b value" echo ${myhash[@]} # "a value b value" though perhaps reversed 

Claro, faz uma chamada externa para cksum e, portanto, é um pouco mais lento, mas a implementação é muito limpa e utilizável. Não é bidirecional, e a maneira interna é muito melhor, mas nem deve ser usada de qualquer maneira. Bash é para one-offs rápidos, e tais coisas raramente devem envolver complexidade que possa exigir hashes, exceto talvez em seu .bashrc e amigos.

Para obter um pouco mais de desempenho, lembre-se de que o grep tem uma function de parada, para parar quando encontrar a enésima coincidência, neste caso n seria 1.

grep –max_count = 1 … ou grep -m 1 …

Eu também usei o bash4, mas eu acho um bug irritante.

Eu precisava atualizar dinamicamente o conteúdo da matriz associativa, então usei desta forma:

 for instanceId in $instanceList do aws cloudwatch describe-alarms --output json --alarm-name-prefix $instanceId| jq '.["MetricAlarms"][].StateValue'| xargs | grep -E 'ALARM|INSUFFICIENT_DATA' [ $? -eq 0 ] && statusCheck+=([$instanceId]="checkKO") || statusCheck+=([$instanceId]="allCheckOk" done 

Descobri que, com o bash 4.3.11 anexando a uma chave existente no dict, resultou em acrescentar o valor se já estiver presente. Por exemplo, após algumas repetições, o conteúdo do valor era “checkKOcheckKOallCheckOK” e isso não era bom.

Nenhum problema com o bash 4.3.39, em que a opção de append uma chave existente significa substistir o valor atual se já estiver presente.

Eu resolvi isso apenas limpando / declarando o array associativo statusCheck antes do ciclo:

 unset statusCheck; declare -A statusCheck 

Eu crio HashMaps no bash 3 usando variables ​​dinâmicas. Expliquei como isso funciona na minha resposta a: Matrizes associativas em scripts do Shell

Você também pode dar uma olhada no shell_map , que é uma implementação do HashMap feita no bash 3.