Conjuntos asociativos en scripts de Shell

Requerimos un script que simule matrices asociativas o una estructura de datos Map like para Shell Scripting, ¿cualquier cuerpo?

Para agregar a la respuesta de Irfan , aquí hay una versión más corta y más rápida de get() ya que no requiere iteración sobre el contenido del mapa:

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

Otra opción, si la portabilidad no es su principal preocupación, es utilizar matrices asociativas que están integradas en el shell. Esto debería funcionar en bash 4.0 (disponible ahora en la mayoría de las distribuciones principales, aunque no en OS X a menos que usted mismo lo instale), ksh y zsh:

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

Dependiendo del shell, es posible que necesite hacer un tipo de typeset -A newmap lugar de declare -A newmap , o en algunos puede no ser necesario en absoluto.

Otra forma de no 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" 

También podría arrojar una instrucción if para buscar allí. if [[$ var = ~ / blah /]]. o lo que sea.

Creo que es necesario dar un paso atrás y pensar qué es realmente un mapa o una matriz asociativa. Todo lo que hay es una manera de almacenar un valor para una clave dada, y recuperar ese valor de manera rápida y eficiente. Es posible que también desee poder iterar sobre las claves para recuperar cada par de valores clave, o eliminar claves y sus valores asociados.

Ahora, piense en una estructura de datos que use todo el tiempo en las secuencias de comandos del shell, e incluso solo en el shell sin escribir un script, que tenga estas propiedades. ¿Sorprendido? Es el sistema de archivos.

En realidad, todo lo que necesita para tener una matriz asociativa en la progtwigción de shell es un directorio temporal. mktemp -d es tu constructor de matriz asociativa:

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

Si no tiene ganas de usar el echo y el cat , siempre puede escribir algunos pequeños envoltorios; estos se modelan a partir de los de Irfan, aunque solo generan el valor en lugar de establecer 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 bastante más rápido que la búsqueda lineal utilizando sed sugerido por el interrogador, y también más robusto (permite que las claves y los valores contengan -, =, espacio, qnd “: SP:”). El hecho de que use el sistema de archivos no lo hace lento; en realidad, nunca se garantiza que estos archivos se escriban en el disco a menos que llame a sync ; para archivos temporales como este con una vida útil corta, no es improbable que muchos de ellos nunca se escriban en el disco.

Hice algunos puntos de referencia del código de Irfan, la modificación de Jerry del código de Irfan, y mi código, utilizando el siguiente progtwig de 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

     0m0.975s reales
     usuario 0m0.280s
     sys 0m0.691s

     $ time ./driver.sh brian 10 5

     0m0.226s reales
     usuario 0m0.057s
     sys 0m0.123s

     $ time ./driver.sh jerry 10 5

     real 0m0.706s
     usuario 0m0.228s
     sys 0m0.530s

     $ time ./driver.sh irfan 100 5

     0m10.633s reales
     usuario 0m4.366s
     sys 0m7.127s

     $ time ./driver.sh brian 100 5

     0m1.682s reales
     usuario 0m0.546s
     sys 0m1.082s

     $ time ./driver.sh jerry 100 5

     0m9.315s reales
     usuario 0m4.565s
     sys 0m5.446s

     $ tiempo ./driver.sh irfan 10 500

     1m46.197s reales
     usuario 0m44.869s
     sys 1m12.282s

     $ time ./driver.sh brian 10 500

     0m16.003s reales
     usuario 0m5.135s
     sys 0m10.396s

     $ time ./driver.sh jerry 10 500

     1m24.414s reales
     usuario 0m39.696s
     sys 0m54.834s

     $ tiempo ./driver.sh irfan 1000 5

     4m25.145s reales
     usuario 3m17.286s
     sys 1m21.490s

     $ time ./driver.sh brian 1000 5

     0m19.442s reales
     usuario 0m5.287s
     sys 0m10.751s

     $ time ./driver.sh jerry 1000 5

     5m29.136s reales
     usuario 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; }' } 

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 

Bash4 es compatible de forma nativa. No uses grep o eval , son los más feos de los hacks.

Para obtener una respuesta detallada y detallada con código de ejemplo, consulte: https://stackoverflow.com/questions/3467959

Ahora respondiendo esta pregunta.

Los siguientes scripts simulan matrices asociativas en scripts de shell. Es simple y muy fácil de entender.

El mapa no es más que una cadena sin fin que tiene keyValuePair guardado como –name = Irfan –designation = SSE –company = Mi: SP: Own: SP: Company

los espacios se reemplazan 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: acaba de agregar otro método para recuperar todas las claves.

 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 solución agradable y simple:

Si no desea manejar muchas variables, o las claves son simplemente identificadores de variable no válidos, y su matriz tiene garantizado tener menos de 256 elementos , puede abusar de los valores de devolución de funciones. Esta solución no requiere ninguna subcapa, ya que el valor está fácilmente disponible como una variable, ni ninguna iteración, por lo que el rendimiento se alarga. También es muy legible, casi como la versión Bash 4.

Aquí está la versión más 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[$?]} 

Recuerde, use comillas simples en el case , de lo contrario está sujeto a globbing. Realmente útil para hashes estáticos / congelados desde el principio, pero se podría escribir un generador de índices a partir de una hash_keys=() .

Tenga cuidado, por defecto es el primero, por lo que es posible que desee dejar de lado 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 

Advertencia: la longitud ahora es incorrecta.

Alternativamente, si desea mantener la indexación basada en cero, puede reservar otro valor de índice y protegerse contra una clave inexistente, 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, desplace el índice en 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 usar nombres de variables dinámicas y dejar que los nombres de las variables funcionen como las claves de un hashmap.

Por ejemplo, si tiene un archivo de entrada con dos columnas, nombre, crédito, como se muestra en el siguiente ejemplo, y desea sumr los ingresos de cada usuario:

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

El comando a continuación sumrá todo, utilizando 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 

El resultado será:

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

Al elaborar sobre estas técnicas, estoy desarrollando en GitHub una función que funciona como un objeto HashMap , shell_map .

Para crear " instancias HashMap ", la función shell_map puede crear copias de sí misma con diferentes nombres. Cada nueva copia de función tendrá una variable $ FUNCNAME diferente. $ FUNCNAME luego se usa para crear un espacio de nombres para cada instancia de Map.

Las claves del mapa son variables globales, en la forma $ FUNCNAME_DATA_ $ KEY, donde $ KEY es la clave que se agrega al Mapa. Estas variables son variables dinámicas .

A continuación, le pondré una versión simplificada para que pueda usarlo 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!" 

He encontrado que es cierto, como ya se mencionó, que el mejor método es escribir claves / valores en un archivo, y luego usar grep / awk para recuperarlos. Suena como todo tipo de IO innecesarios, pero la memoria caché de disco se activa y la hace extremadamente eficiente, mucho más rápido que tratar de almacenarlos en la memoria utilizando uno de los métodos anteriores (como se muestra en las pruebas comparativas).

Aquí hay un método rápido y limpio que 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 quería aplicar un solo valor por clave, también podría hacer una pequeña acción grep / sed en hput ().

Qué lástima que no haya visto la pregunta antes: escribí el framework de shell de la biblioteca que contiene, entre otros, los mapas (matrices asociativas). La última versión de la misma se puede encontrar aquí .

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

Shell no tiene un mapa incorporado como la estructura de datos, uso una cadena sin formato para describir elementos como ese:

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

cuando se extraen elementos 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 ser inteligente que la respuesta de otras personas, pero fácil de entender para las nuevas personas.

Modifiqué la 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 para evitar que devuelva errores si solicita una clave que no existe, aunque el efecto secundario es que también ignorará silenciosamente los mapas faltantes, pero se adecuó mejor a mi caso de uso ya que solo quería verificar una clave para saltar elementos en un bucle.

Hace varios años escribí una biblioteca de scripts para bash que admitía matrices asociativas entre otras características (registro, archivos de configuración, soporte extendido para el argumento de la línea de comando, generar ayuda, pruebas de unidades, etc.). La biblioteca contiene un contenedor para matrices asociativas y cambia automáticamente al modelo apropiado (interno para bash4 y emulado para versiones anteriores). Se llamó shell-framework y se alojó en origo.ethz.ch, pero hoy el recurso está cerrado. Si alguien aún lo necesita, puedo compartirlo contigo.

Última respuesta, pero considere abordar el problema de esta manera, utilizando la lectura integrada de bash como se ilustra en el fragmento de código de un script de firewall ufw que sigue. Este enfoque tiene la ventaja de utilizar tantos conjuntos de campos delimitados (no solo 2) como se desee. Hemos usado el | delimitador porque los especificadores de rango de puertos pueden requerir dos puntos, es decir, 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 

Agregando otra opción, si jq está disponible:

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