Exigimos um script que simula matrizes associativas ou estrutura de dados como um mapa para Shell Scripting, qualquer corpo?
Exigimos um script que simula matrizes associativas ou estrutura de dados como um mapa para Shell Scripting, qualquer corpo?
Respostas:
Para adicionar à resposta de Irfan , aqui está uma versão mais curta e 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' )"
}
Outra opção, se a portabilidade não for sua preocupação principal, é usar matrizes associativas que são integradas ao shell. Isso deve funcionar no bash 4.0 (disponível agora na maioria das principais distros, embora não no OS X, a menos que você mesmo 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, você pode precisar fazer um em typeset -A newmap
vez de declare -A newmap
, ou em alguns pode nem ser necessário.
test -z ${variable+x}
( x
não importa, pode ser qualquer string). Para uma matriz associativa no Bash, você pode fazer o mesmo; usar test -z ${map[key]+x}
.
Outra forma não-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"
Você também pode lançar uma instrução if para pesquisar lá. if [[$ var = ~ / blah /]]. como queiras.
Eu acho que você precisa dar um passo atrás e pensar sobre o que um mapa, ou array associativo, realmente é. Tudo isso é uma maneira de armazenar um valor para uma determinada chave e obter esse valor de volta com rapidez e eficiência. Você também pode querer iterar 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 em scripts de shell, e mesmo apenas no shell sem escrever um script, que tenha essas propriedades. Perplexo? É o sistema de arquivos.
Na verdade, tudo o que você precisa para ter uma matriz associativa na programação do 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 quiser usar echo
e cat
, você pode escrever alguns pequenos invólucros; estes são modelados a partir do Irfan, embora eles apenas gerem 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 é na verdade um pouco mais rápida do 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; esses arquivos nunca têm garantia de serem gravados no disco, a menos que você chamesync
; para arquivos temporários como esse com uma vida útil curta, 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:
$ time ./driver.sh irfan 10 5 0m0.975s reais usuário 0m0.280s sys 0m0.691s $ time ./driver.sh brian 10 5 0m0.226s reais usuário 0m0.057s sys 0m0.123s $ time ./driver.sh jerry 10 5 0m0.706s reais usuário 0m0.228s sys 0m0.530s $ time ./driver.sh irfan 100 5 0m10.633s reais usuário 0m4.366s sys 0m7.127s $ time ./driver.sh brian 100 5 0m1.682s reais usuário 0m0.546s sys 0m1.082s $ time ./driver.sh jerry 100 5 0m9.315s reais usuário 0m4.565s sys 0m5.446s $ time ./driver.sh irfan 10 500 real 1m46.197s usuário 0m44.869s sys 1m12.282s $ time ./driver.sh brian 10 500 0m16.003s reais usuário 0m5.135s sys 0m10.396s $ time ./driver.sh jerry 10 500 real 1m24.414s usuário 0m39.696s sys 0m54.834s $ time ./driver.sh irfan 1000 5 4m25.145s reais usuário 3m17.286s sys 1m21.490s $ time ./driver.sh brian 1000 5 0m19.442s reais usuário 0m5.287s sys 0m10.751s $ time ./driver.sh jerry 1000 5 5m29.136s reais usuário 4m48.926s sys 0m59.336s
Bash4 oferece suporte nativo. Não use grep
ou eval
, eles são os mais feios dos hacks.
Para uma resposta detalhada e detalhada com código de exemplo, consulte: /programming/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 respondendo a esta pergunta.
Os scripts a seguir simulam arrays associativos em scripts de shell. É simples e muito fácil de entender.
Mapa nada mais é do que uma string sem fim que tem keyValuePair salvo como --name = Irfan --designation = SSE --company = Meu: 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: 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"
`
}
eval
inserindo dados como se fossem código bash, e mais: você falha ao citá-los corretamente. Ambos causam muitos bugs e injeção arbitrária de código.
Para o Bash 3, há um caso particular que tem uma solução simples e agradável:
Se você não quiser lidar com muitas variáveis ou se as chaves forem simplesmente identificadores de variáveis inválidos, e se sua matriz tiver a garantia de ter menos de 256 itens , você pode abusar dos valores de retorno da função. Esta solução não requer nenhum subshell, pois o valor está prontamente disponível como uma variável, nem qualquer iteração de modo que o desempenho grite. Também é muito legível, quase como a versão Bash 4.
Esta é 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
, caso contrário, está sujeito a globbing. Muito útil para hashes estáticos / congelados desde o início, mas pode-se escrever um gerador de índice a partir de um hash_keys=()
array.
Cuidado, o padrão é o primeiro, então você pode querer deixar de lado o elemento zero:
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 está incorreto.
Como alternativa, se você deseja manter a indexação baseada em zero, 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, desloque o índice em 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 permitir que os nomes das variáveis funcionem como as chaves de um hashmap.
Por exemplo, se você tem um arquivo de entrada com duas colunas, nome, crédito, como no exemplo abaixo, e deseja somar a renda de cada usuário:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
O comando abaixo somará tudo, usando 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
O resultado será:
map_David=100
map_John=500
map_Mary=150
map_Paul=500
Elaborando essas técnicas, estou desenvolvendo no GitHub uma função que funciona exatamente como um objeto HashMap , shell_map .
Para criar " instâncias de HashMap ", a função shell_map é capaz de criar cópias de si mesma com nomes diferentes. Cada nova cópia de função 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 variáveis globais, no formato $ FUNCNAME_DATA_ $ KEY, onde $ KEY é a chave adicionada ao Mapa. Essas variáveis são variáveis 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!"
Ainda outra forma não-bash-4 (ou seja, bash 3, compatível com Mac):
val_of_key() {
case $1 in
'A1') echo 'aaa';;
'B2') echo 'bbb';;
'C3') echo 'ccc';;
*) echo 'zzz';;
esac
}
for x in 'A1' 'B2' 'C3' 'D4'; do
y=$(val_of_key "$x")
echo "$x => $y"
done
Impressões:
A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz
A função com o case
atua como uma matriz associativa. Infelizmente, ele não pode usar return
, por isso tem de echo
produzir, mas isso não é um problema, a menos que você seja um purista que evita bifurcar subshells.
É uma pena que não tenha visto a pergunta antes - escrevi biblioteca shell-framework 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")"
Descobri que é verdade, como já mencionei, que o método de melhor desempenho é gravar key / vals em um arquivo e, em seguida, usar grep / awk para recuperá-los. Parece 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 memória usando um dos métodos acima (como mostram os benchmarks).
Este é um método rápido e limpo de que 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ê quiser impor um valor único por chave, também pode fazer uma pequena ação grep / sed em hput ().
vários anos atrás, escrevi uma biblioteca de scripts para o bash que suportava matrizes associativas entre outros recursos (registro, 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). Era chamado de shell-framework e hospedado em origo.ethz.ch, mas hoje o recurso está fechado. Se alguém ainda precisar, posso compartilhar com você.
Shell não tem um mapa embutido como estrutura de dados, eu uso string bruta para descrever itens como este:
ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)
ao 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 não parece 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 é para map_get a fim de evitar que ele retorne erros se você solicitar uma chave que não existe, embora o efeito colateral seja que ele também irá ignorar silenciosamente os mapas ausentes, mas se adequou melhor ao meu caso de uso, já que eu apenas queria verificar se há uma chave para pular itens em um loop.
Resposta tardia, mas considere resolver o problema desta forma, usando o bash builtin lido conforme ilustrado no fragmento 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. Usamos o | delimitador porque os especificadores de intervalo de porta podem exigir 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