¿Cómo definir tablas hash en Bash?
Cuál es el equivalente de los diccionarios de Python pero en Bash (debería funcionar en OS X y Linux).
16 answers
Bash 4
Bash 4 soporta esta característica de forma nativa. Asegúrate de que el hashbang de tu script sea #!/usr/bin/env bash
o #!/bin/bash
o cualquier otra cosa que haga referencia a bash
y no a sh
. Asegúrese de que está ejecutando su script, y no haciendo algo tonto como sh script
que haría que su bash
hashbang sea ignorado. Esto es algo básico, pero muchos siguen fallando en ello, de ahí la reiteración.
Se declara un array asociativo haciendo:
declare -A animals
Puedes llenarlo con elementos usando el operador de asignación de matriz normal:
animals=( ["moo"]="cow" ["woof"]="dog")
O fusionarlos:
declare -A animals=( ["moo"]="cow" ["woof"]="dog")
Entonces úsalos como los arrays normales. "${animals[@]}"
expande los valores, "${!animals[@]}"
(observe el !
) expande las claves. No se olvide de citarlos:
echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
Bash 3
Antes de bash 4, no tienes matrices asociativas. No utilice eval
para emularlos. Debes evitar eval como la plaga, porque es la plaga de shell scripting. Lo más importante la razón es que no desea tratar sus datos como código ejecutable (también hay muchas otras razones).
Primero y ante todo : Solo considera actualizar a bash 4. Seriamente. El futuro es ahora, deja de vivir en el pasado y sufrir de él forzando estúpidos hackeos rotos y feos en tu código y cada pobre alma atascada manteniéndolo.
Si tienes alguna excusa tonta por la que " no puedes actualizar", declare
es una opción mucho más segura. Se no evalúa los datos como el código bash como lo hace eval
, y como tal no permite la inyección arbitraria de código con tanta facilidad.
Preparemos la respuesta introduciendo los conceptos: {[29]]}
Primero, indirección (en serio; nunca use esto a menos que esté mentalmente enfermo o tenga alguna otra mala excusa para escribir hacks).
$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}"
cow
En segundo lugar, declare
:
$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo"
cow
Reunirlos:
# Set a value:
declare "array_$index=$value"
# Get a value:
arrayGet() {
local array=$1 index=$2
local i="${array}_$index"
printf '%s' "${!i}"
}
Vamos a usarlo:
$ sound=moo
$ animal=cow
$ declare "animals_$sound=$animal"
$ arrayGet animals "$sound"
cow
Nota: declare
no se puede poner en un función. Cualquier uso de declare
dentro de una función bash convierte la variable que crea local en el ámbito de esa función, lo que significa que no podemos acceder ni modificar matrices globales con ella. (En bash 4 puedes usar declare-g para declarar variables globales - pero en bash 4, deberías usar arrays asociativos en primer lugar, no este truco.)
Resumen
Actualice a bash 4 y use declare -A
. Si no puede, considere cambiar completamente a awk
antes de hacer hacks feos como descrito anteriormente. Y definitivamente manténgase alejado de eval
hackery.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-18 18:31:26
Hay sustitución de parámetros, aunque también puede ser un-PC ...como indirecta.
#!/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]##*:}"
La forma de BASH 4 es mejor, por supuesto, pero si necesita un hack ...sólo un hack lo hará. Puedes buscar en el array / hash con técnicas similares.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-09 18:11:26
Esto es lo que estaba buscando aquí:
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
Esto no funcionó para mí con bash 4.1.5:
animals=( ["moo"]="cow" )
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-23 08:36:05
Puede modificar aún más la interfaz hput()/hget() para que haya nombrado los hashes de la siguiente manera:
hput() {
eval "$1""$2"='$3'
}
hget() {
eval echo '${'"$1$2"'#hash}'
}
Y luego
hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
Esto le permite definir otros mapas que no entran en conflicto (por ejemplo, 'rcapitals' que hace búsqueda de país por ciudad capital). Pero, de cualquier manera, creo que encontrarás que todo esto es bastante terrible, en cuanto a rendimiento.
Si realmente quieres una búsqueda rápida de hash, hay un terrible, terrible hack que realmente funciona muy bien. Es esto: escriba su clave / valores a un archivo temporal, uno por línea, luego use 'grep" ^key key "' para sacarlos, usando tuberías con cut o awk o sed o lo que sea para recuperar los valores.
Como dije, suena terrible, y suena como que debería ser lento y hacer todo tipo de IO innecesario, pero en la práctica es muy rápido (caché de disco es impresionante, no es?), incluso para tablas hash muy grandes. Tienes que hacer cumplir la singularidad clave tú mismo, etc. Incluso si solo tiene unos pocos cientos de entradas, el combo archivo de salida / grep es va a ser un poco más rápido, en mi experiencia varias veces más rápido. También come menos memoria.
Aquí hay una manera de hacerlo:
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`
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-29 16:09:52
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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-29 22:45:35
Considere una solución usando bash builtin read como se ilustra en el fragmento de código de un script de firewall de ufw que sigue. Este enfoque tiene la ventaja de utilizar tantos conjuntos de campos delimitados (no solo 2) como se desee. Hemos utilizado el | delimitador porque los especificadores de rango de puerto 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
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-15 17:16:30
Simplemente use el sistema de archivos
El sistema de archivos es una estructura de árbol que se puede usar como un mapa hash. Su tabla hash será un directorio temporal, sus claves serán nombres de archivo y sus valores serán contenidos de archivo. La ventaja es que puede manejar grandes hashmaps, y no requiere un shell específico.
Creación de la tabla hash
hashtable=$(mktemp -d)
Añadir un elemento
echo $value > $hashtable/$key
Lee un elemento
value=$(< $hashtable/$key)
Rendimiento
Por supuesto, su lento, pero no que lento. Lo probé en mi máquina, con un SSD y btrfs, y lo hace alrededor de 3000 elementos de lectura/escritura por segundo.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-06 13:21:19
Estoy de acuerdo con @lhunath y otros en que el array asociativo es el camino a seguir con Bash 4. Si está pegado a Bash 3 (OSX, distribuciones antiguas que no puede actualizar) puede usar también expr, que debería estar en todas partes, una cadena y expresiones regulares. Me gusta especialmente cuando el diccionario no es demasiado grande.
- Elija 2 separadores que no usará en claves y valores (por ejemplo,', 'y':')
-
Escriba su mapa como una cadena (tenga en cuenta el separador', ' también al principio y end)
animals=",moo:cow,woof:dog,"
-
Utilice una expresión regular para extraer los valores
get_animal { echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")" }
-
Divide la cadena para listar los elementos
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 }
Ahora puedes usarlo:
$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-01 15:13:10
Realmente me gustó la respuesta de Al P, pero quería que la unicidad se hiciera cumplir a bajo costo, así que lo llevé un paso más allá: use un directorio. Hay algunas limitaciones obvias (límites de archivos de directorio, nombres de archivos no válidos), pero debería funcionar para la mayoría de los 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
También funciona un poco mejor en mis pruebas.
$ 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
Solo pensé en colaborar. ¡Salud!
Editar: Añadiendo hdestroy ()
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-14 21:19:11
Dos cosas, puede usar memoria en lugar de /tmp en cualquier kernel 2.6 usando /dev/shm (Redhat) otras distribuciones pueden variar. También hget puede ser reimplementado usando read como sigue:
function hget {
while read key idx
do
if [ $key = $2 ]
then
echo $idx
return
fi
done < /dev/shm/hashmap.$1
}
Además, al asumir que todas las claves son únicas, el retorno cortocircuita el bucle de lectura y evita tener que leer todas las entradas. Si su implementación puede tener claves duplicadas, simplemente deje de lado la devolución. Esto ahorra el gasto de leer y bifurcar tanto grep como awk. Usando /dev / shm para ambas implementaciones se obtuvo lo siguiente usando time hget en un hash de 3 entradas buscando la última entrada:
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
Read / echo:
$ time echo $(hget FD oracle)
3
real 0m0.004s
user 0m0.000s
sys 0m0.004s
En múltiples invocaciones nunca vi menos de un 50% de mejora.
Todo esto se puede atribuir a la horquilla sobre la cabeza, debido al uso de /dev/shm
.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-15 17:44:08
Solución Bash 3:
Al leer algunas de las respuestas, armé una pequeña función rápida que me gustaría contribuir para ayudar a otros.
# 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[@])
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-29 15:26:17
Antes de bash 4 no hay una buena manera de usar arrays asociativos en bash. Tu mejor apuesta es usar un lenguaje interpretado que realmente tenga soporte para tales cosas, como awk. Por otro lado, bash 4 los apoya.
En cuanto a menos buenas maneras en bash 3, aquí hay una referencia que podría ayudar: http://mywiki.wooledge.org/BashFAQ/006
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-08-12 12:53:37
Un compañero de trabajo acaba de mencionar este hilo. He implementado tablas hash de forma independiente dentro de bash, y no depende de la versión 4. De un post mío en marzo de 2010 (antes de algunas de las respuestas aquí...) titulado Tablas hash en 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, hace una llamada externa para cksum y, por lo tanto, se ralentiza un poco, pero la implementación es muy limpia y utilizable. No es bidireccional, y la forma incorporada es mucho mejor, pero ninguno de los dos debería usarse realmente Por cierto. Bash es para una sola vez rápida, y tales cosas deberían implicar muy rara vez la complejidad que podría requerir hashes, excepto tal vez en su .bashrc y sus amigos.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-18 00:39:57
Para obtener un poco más de rendimiento recuerde que grep tiene una función stop, para detener cuando encuentra la enésima coincidencia en este caso n sería 1.
Grep max max_count=1 ... o grep-m 1 ...
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-01 15:47:27
También usé la forma bash4, pero me parece un error molesto.
Necesitaba actualizar dinámicamente el contenido del array asociativo, así que usé de esta manera:
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
Me entero de que con bash 4.3.11 añadiendo a una clave existente en el dict resultó en añadir el valor si ya está presente. Así, por ejemplo, después de alguna repetición el contenido del valor era "checkKOcheckKOallCheckOK" y esto no era bueno.
No hay problema con bash 4.3.39 donde appenging una clave existente significa a sustituya el valor actuale si ya está presente.
Resolví esto simplemente limpiando / declarando el array asociativo StatusCheck antes del ciclo:
unset statusCheck; declare -A statusCheck
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-09-04 06:31:09
Creo HashMaps en bash 3 usando variables dinámicas. Expliqué cómo funciona eso en mi respuesta a: Matrices asociativas en scripts de Shell
También puede echar un vistazo en shell_map, que es una implementación de HashMap hecha en bash 3.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:42