¿Cuál es el equivalente de los diccionarios de Python pero en Bash (debería funcionar en OS X y Linux).
¿Cuál es el equivalente de los diccionarios de Python pero en Bash (debería funcionar en OS X y Linux).
Respuestas:
Bash 4 admite de forma nativa esta función. Asegúrate de que el hashbang de tu script sea más #!/usr/bin/env bash
o menos #!/bin/bash
así que no termines usando sh
. Asegúrese de que está ejecutando su script directamente o ejecute script
con bash script
. (En realidad no la ejecución de un script Bash con Bash no suceda, y será muy confuso!)
Declaras una matriz asociativa haciendo:
declare -A animals
Puede llenarlo con elementos utilizando el operador de asignación de matriz normal. Por ejemplo, si desea tener un mapa de animal[sound(key)] = animal(value)
:
animals=( ["moo"]="cow" ["woof"]="dog")
O fusionarlos:
declare -A animals=( ["moo"]="cow" ["woof"]="dog")
Luego úselos como matrices normales. Utilizar
animals['key']='value'
establecer valor
"${animals[@]}"
para expandir los valores
"${!animals[@]}"
(observe el !
) para expandir las teclas
No olvides citarlos:
echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
Antes de bash 4, no tienes matrices asociativas. No lo use eval
para emularlos . Evita eval
como la plaga, porque es la plaga de las secuencias de comandos de shell. La razón más importante es que eval
trata sus datos como código ejecutable (también hay muchas otras razones).
Primero y principal : considere actualizar a bash 4. Esto hará que todo el proceso sea mucho más fácil para usted.
Si hay una razón por la que no puede actualizar, declare
es una opción mucho más segura. No evalúa los datos como el código bash como lo eval
hace, y como tal no permite la inyección de código arbitrario con tanta facilidad.
Preparemos la respuesta introduciendo los conceptos:
Primero, indirección.
$ 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 una función. Cualquier uso de declare
dentro de una función bash convierte la variable que crea local en el alcance de esa función, lo que significa que no podemos acceder o modificar matrices globales con ella. (En bash 4 puede usar declare -g para declarar variables globales, pero en bash 4, puede usar matrices asociativas en primer lugar, evitando esta solución).
Resumen:
declare -A
para matrices asociativas.declare
opción si no puede actualizar.awk
lugar y evite el problema por completo.4.x
y no y
.
sudo port install bash
, para aquellos (sabiamente, en mi humilde opinión) que no están dispuestos a crear directorios en la RUTA para que todos los usuarios puedan escribir sin una escalada explícita de privilegios por proceso.
Hay sustitución de parámetros, aunque también puede ser no PC ... como indirección.
#!/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 ... solo un hack lo hará. Puede buscar en la matriz / hash con técnicas similares.
VALUE=${animal#*:}
proteger el caso dondeARRAY[$x]="caesar:come:see:conquer"
for animal in "${ARRAY[@]}"; do
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" )
Puede modificar aún más la interfaz hput () / hget () para que haya nombrado hashes de la siguiente manera:
hput() {
eval "$1""$2"='$3'
}
hget() {
eval echo '${'"$1$2"'#hash}'
}
y entonces
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 entren en conflicto (p. Ej., 'Rcapitals' que realiza búsquedas de país por ciudad capital). Pero, de cualquier manera, creo que encontrarás que todo esto es bastante terrible, en cuanto al rendimiento.
Si realmente quieres una búsqueda rápida de hash, hay un truco terrible que funciona realmente bien. Es esto: escriba su clave / valores en un archivo temporal, uno por línea, luego use 'grep "^ $ key"' para sacarlos, utilizando tuberías con corte o awk o sed o lo que sea para recuperar los valores.
Como dije, suena terrible, y parece que debería ser lento y hacer todo tipo de IO innecesarias, pero en la práctica es muy rápido (el caché del disco es increíble, ¿no?), Incluso para hash muy grandes mesas. Tienes que imponer la unicidad de la clave tú mismo, etc. Incluso si solo tienes unos cientos de entradas, el combo de archivo de salida / grep será bastante más rápido, en mi experiencia varias veces más rápido. También come menos memoria.
Aquí hay una forma 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`
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 archivos y sus valores serán contenidos de archivos. La ventaja es que puede manejar enormes hashmaps y no requiere un shell específico.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
Por supuesto, es lento, pero no tan lento. Lo probé en mi máquina, con un SSD y btrfs , y hace alrededor de 3000 elementos de lectura / escritura por segundo .
mkdir -d
? (No 4.3, en Ubuntu 14. Recurriría mkdir /run/shm/foo
, o si eso llenara RAM mkdir /tmp/foo
mktemp -d
se entiende en su lugar?
$value=$(< $hashtable/$key)
y value=$(< $hashtable/$key)
? ¡Gracias!
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
${var#start}
elimina el texto que comienza desde el principio del valor almacenado en la variable var .
Considere una solución usando el bash builtin read como se ilustra dentro del 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
IFS=$'|' read -r first rest <<< "$fields"
Estoy de acuerdo con @lhunath y otros en que la matriz asociativa es el camino a seguir con Bash 4. Si está atascado en 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.
Escriba su mapa como una cadena (tenga en cuenta el separador ',' también al principio y al final)
animals=",moo:cow,woof:dog,"
Use una expresión regular para extraer los valores
get_animal {
echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
}
Dividir la cadena para enumerar 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
Realmente me gustó la respuesta de Al P, pero quería que la unicidad se aplicara de manera barata, así que lo llevé un paso más allá: use un directorio. Hay algunas limitaciones obvias (límites de archivos de directorio, nombres de archivo 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 lanzarme. ¡Salud!
Editar: Agregar hdestroy ()
Dos cosas, puedes usar memoria en lugar de / tmp en cualquier kernel 2.6 usando / dev / shm (Redhat) otras distribuciones pueden variar. También se puede volver a implementar hget 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, suponiendo que todas las claves son únicas, el retorno cortocircuita el ciclo de lectura y evita tener que leer todas las entradas. Si su implementación puede tener claves duplicadas, simplemente omita el retorno. Esto ahorra el gasto de leer y bifurcar grep y awk. El uso de / dev / shm para ambas implementaciones arrojó 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
Leer / eco:
$ time echo $(hget FD oracle)
3
real 0m0.004s
user 0m0.000s
sys 0m0.004s
en invocaciones múltiples nunca vi menos de una mejora del 50%. Todo esto se puede atribuir al tenedor sobre la cabeza, debido al uso de /dev/shm
.
Un compañero de trabajo acaba de mencionar este hilo. Implementé independientemente tablas hash dentro de bash, y no depende de la versión 4. De una publicación mía en mi blog en marzo de 2010 (antes de algunas de las respuestas aquí ...) titulada Tablas hash en bash :
Me previamente acostumbrado cksum
a picadillo pero desde entonces han traducido hashCode cadena de Java a los nativos bash / zsh.
# Here's the hashing function
ht() {
local h=0 i
for (( i=0; i < ${#1}; i++ )); do
let "h=( (h<<5) - h ) + $(printf %d \'${1:$i:1})"
let "h |= h"
done
printf "$h"
}
# 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
echo ${#myhash[@]} # "2" - there are two values (note, zsh doesn't count right)
No es bidireccional, y la forma incorporada es mucho mejor, pero tampoco debería usarse. Bash es para casos únicos rápidos, y tales cosas rara vez deben involucrar complejidad que pueda requerir hashes, excepto tal vez en usted ~/.bashrc
y sus amigos.
Antes de bash 4, no hay una buena manera de usar matrices asociativas en bash. Su mejor opción es utilizar un lenguaje interpretado que realmente tenga soporte para tales cosas, como awk. Por otra parte, golpe del 4 hace apoyarlos.
En cuanto a las formas menos buenas en bash 3, aquí hay una referencia que podría ayudar: http://mywiki.wooledge.org/BashFAQ/006
Solución Bash 3:
Al leer algunas de las respuestas, reuní 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[@])
También usé la forma bash4 pero encuentro un error molesto.
Necesitaba actualizar dinámicamente el contenido de la matriz asociativa, así que lo utilicé 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
Descubrí que con bash 4.3.11 agregar a una clave existente en el dict resultó en agregar el valor si ya está presente. Entonces, 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 agregar una clave existente significa subestimar el valor real si ya está presente.
Resolví esto simplemente limpiando / declarando la matriz asociativa statusCheck antes del ciclo:
unset statusCheck; declare -A statusCheck
Creo HashMaps en bash 3 usando variables dinámicas. Le 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.