Matrices asociativas en los scripts de Shell
-
22-08-2019 - |
Pregunta
Se requiere un script que simula las matrices Asociativas o Mapa como estructura de datos para la programación en Shell script, cualquier cuerpo?
Solución
Para añadir a de Irfan respuesta , aquí es una versión más corta y más rápida de get()
ya que no requiere iteración sobre el contenido de mapa:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
Otros consejos
Otra opción, si la portabilidad no es su principal preocupación, es el uso de matrices asociativas que se construyen en la cáscara. Esto debería funcionar en bash 4.0 (disponible ahora en la mayoría de distribuciones principales, aunque no en OS X a menos que instale usted mismo), ksh, y zsh:
declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"
echo ${newmap[company]}
echo ${newmap[name]}
En función de la cáscara, es posible que tenga que hacer un typeset -A newmap
en lugar de declare -A newmap
, o en algunos puede no ser necesario en absoluto.
Otro no golpe 4 manera.
#!/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"
Se podría lanzar una sentencia if para la búsqueda en allí también. si [[$ var = ~ / bla /]]. o lo que sea.
Creo que es necesario dar un paso atrás y pensar en lo que es un mapa o matriz asociativa, es en realidad. Todo lo que es es una forma de almacenar un valor para una clave dada, y conseguir que el valor de nuevo de forma rápida y eficiente. También puede querer ser capaz de iterar sobre las claves para recuperar cada par de valores clave, o borrar las claves y sus valores asociados.
Ahora, piensa en una estructura de datos que utiliza todo el tiempo en el shell scripting, e incluso simplemente en la cáscara sin necesidad de escribir un guión, que tiene estas propiedades. Desconcertado? Es el sistema de archivos.
En realidad, todo lo que necesita tener una matriz asociativa en la programación shell es un directorio temporal. mktemp -d
es el constructor de matrices asociativas:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
Si usted no se siente como el uso de echo
y cat
, siempre se puede escribir algunos pequeños envoltorios; éstos se modelan fuera de Irfan de, a pesar de que acaba de emitir el valor en lugar de la definición de variables arbitrarias 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 : Este enfoque es en realidad un poco más rápido que la búsqueda lineal usando sed sugerida por la pregunta, así como más robusta (que permite que las claves y valores que contienen -, =, espacio, qnd ": SP:"). El hecho de que utiliza el sistema de archivos no significa que sea más lento; estos archivos son en realidad nunca se garantiza que se escriben en el disco a menos que llame sync
; para los archivos temporales de este tipo con un tiempo de vida corto, no es improbable que muchos de ellos nunca se escribirá en el disco.
Lo hice un par de puntos de referencia de código de Irfan, la modificación del código de Irfan de Jerry, y mi código, utilizando el siguiente programa controlador:
#!/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
Los 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 apoya esta forma nativa. No utilice grep
o eval
, que son los más feos de los cortes.
Para obtener una respuesta prolijo y detallado con el código de ejemplo, véase: 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; }'
}
Ejemplo:
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
Ahora responder a esta pregunta.
siguientes secuencias de comandos simula matrices asociativas en secuencias de comandos shell. Su sencillo y muy fácil de entender.
Mapa no es más que una cadena sin fin que tiene KeyValuePair guarda como --name = Irfan --designation = SSE --company = Mi: SP: propio: SP: Empresa
espacios se reemplazan con ': 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 acaba de añadir otro método para captar todas las 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, hay un caso particular que tiene una bonita y sencilla solución:
Si usted no desea manejar una gran cantidad de variables, o las teclas son simplemente no válido identificadores de variable, y su matriz está garantizado para tener menos de 256 elementos, puede abuso valores devueltos por la función.Esta solución no requiere cualquier subshell como el valor está disponible como una variable, ni cualquier iteración, de manera que el rendimiento de los gritos.También es muy fácil de leer, casi como el Bash 4 de la versión.
Aquí está el más básico de la versión:
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[$?]}
Recuerde, utilice comillas simples en case
, cosa que es objeto de esta expansión.Realmente útil para la estática/congelados los hash desde el inicio, pero se podría escribir un índice generador de un hash_keys=()
de la matriz.
El reloj, el valor predeterminado es el primero, así que usted puede querer dejar de lado elemento cero:
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
Advertencia:la longitud es ahora incorrecta.
Alternativamente, si desea mantener basado en cero de la indexación, se puede reservar otro, el valor del índice y de la guardia en contra de una inexistente clave, pero es menos legible:
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[$?]}
O bien, para mantener la longitud correcta, offset índice por uno:
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))]}
Puede utilizar nombres de variables dinámicas y dejar que las variables nombres funcionan como las teclas de un HashMap.
Por ejemplo, si usted tiene un archivo de entrada con dos columnas, nombre, crédito, como el ejemplo que sigue, y desea sumar los ingresos de cada usuario:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
El comando abajo resumir todo, el uso de variables dinámicas como claves, en forma de $ mapa _ {persona}
while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)
Para leer los resultados:
set | grep map
La salida será:
map_David=100
map_John=500
map_Mary=150
map_Paul=500
Abundando en estas técnicas, que estoy desarrollando en GitHub una función que funciona como un HashMap Objeto , shell_map .
Con el fin de crear " casos HashMap " función shell_map es capaz de crear copias de sí mismo con nombres diferentes. Cada copia nueva función tendrá una variable $ FUNCNAME diferente. $ FUNCNAME continuación, se utiliza para crear un espacio de nombres para cada instancia mapa.
El mapa de teclas son variables globales, en forma FUNCNAME_DATA_ $ $ CLAVE, donde $ es la tecla añadido al mapa. Estas variables son dinámico las variables .
Fuelle voy a poner una versión simplificada de la misma manera puede utilizar como ejemplo.
#!/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!"
Qué lástima que no vi la pregunta antes - he escrito href="http://shell-framework.origo.ethz.ch/" shell-marco que contiene entre otros los mapas (arrays asociativos). La última versión de la misma se puede encontrar rel="nofollow"> .
Ejemplo:
#!/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")"
He encontrado que es cierto, como ya se ha dicho, que el mejor método de realizar es escribir clave / Vals en un archivo, y luego usar grep / awk para recuperarlos. Suena como todo tipo de IO innecesario, pero caché de disco entra en acción y hace que sea extremadamente eficiente -. Mucho más rápido que tratar de guardarlos en la memoria usando uno de los métodos anteriores (como muestran los puntos de referencia)
Esto es un método rápido, limpio Me gusta:
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 usted quiere hacer cumplir un solo valor por clave, también se puede hacer un poco de grep / acción sed en hput ().
Hace varios años escribí biblioteca de scripts para la fiesta que apoyó matrices asociativas entre otras características (registro, archivos de configuración, soporte extendido para el argumento de la línea de comandos, generar ayuda, la unidad de pruebas, etc.). La biblioteca contiene una envoltura para matrices asociativas y cambia automáticamente a modelo apropiado (interno para bash4 y emular para las versiones anteriores). Se le llamó la cáscara-marco y alojado en origo.ethz.ch pero hoy el recurso está cerrado. Si alguien todavía lo necesita puedo compartirlo con ustedes.
Shell tiene ningún mapa incorporada como estructura de datos, usar cadena de texto para describir artículos como que:
ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)
cuando los elementos de extracto y sus 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
Esto parece no inteligente que la respuesta de otras personas, pero fácil de entender para la gente nueva a Shell.
La adición de otra opción, si está disponible JQ:
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 '"'
he modificado solución de Vadim con lo siguiente:
####################################################################
# 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; }'
}
El cambio es map_get con el fin de evitar que vuelva a errores si solicita una clave que no existe, aunque el efecto secundario es que también pasará por alto en silencio mapas que faltan, pero fue adecuado para mi caso de uso mejor ya que sólo quería comprobar si hay una clave con el fin de omitir elementos en un bucle.
última contestación, pero tenga en cuenta para abordar el problema de esta manera, el uso de la fiesta orden interna leer como se ilustra en el fragmento de código de un script de firewall UFW que sigue. Este enfoque tiene la ventaja de usar tantos conjuntos campo delimitado (no sólo 2) como se desea. Hemos utilizado el | delimitador porque especificadores intervalo de puertos pueden requerir dos puntos, es decir, 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