Come definire le tabelle hash in Bash?

Qual è l’equivalente dei dizionari Python ma in Bash (dovrebbe funzionare su OS X e Linux).

Bash 4

Bash 4 supporta nativamente questa funzione. Assicurati che l’hashbang dello script sia #!/usr/bin/env bash Usr #!/usr/bin/env bash o #!/bin/bash o qualsiasi altra cosa che bash riferimento a bash e non a sh . Assicurati di eseguire il tuo script e di non fare qualcosa di sciocco come sh script che farebbe ignorare l’hashbang di bash . Questa è roba di base, ma molti continuano a fallire, da qui la reiterazione.

Si dichiara un array associativo facendo:

 declare -A animals 

Puoi riempirlo con elementi usando il normale operatore di assegnazione della matrice:

 animals=( ["moo"]="cow" ["woof"]="dog") 

O uniscili:

 declare -A animals=( ["moo"]="cow" ["woof"]="dog") 

Quindi usali come normali array. "${animals[@]}" espande i valori, "${!animals[@]}" (noti il ! ) espande le chiavi. Non dimenticare di citarli:

 echo "${animals[moo]}" for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done 

Bash 3

Prima di bash 4, non hai array associativi. Non usare eval per emularli . Devi evitare la valutazione come la peste, perché è la piaga dello scripting di shell. Il motivo più importante è che non vuoi trattare i tuoi dati come codice eseguibile (ci sono anche molti altri motivi).

Prima di tutto : considera l’aggiornamento a bash 4. Seriamente. Il futuro è adesso , smetti di vivere nel passato e soffri nel farlo forzando stupidi e rotti brutti hack sul tuo codice e ogni povera anima bloccata a mantenerla.

Se hai qualche scusa sciocca perché ” non puoi aggiornare “, declare è un’opzione molto più sicura. Non valuta i dati come fa il codice eval come eval , e come tale non consente l’iniezione arbitraria di codice abbastanza facilmente.

Prepariamo la risposta introducendo i concetti:

Innanzitutto, indiretta (seriamente, non usare mai questo a meno che tu non sia malato di mente o abbia qualche altra scusa negativa per scrivere hack).

 $ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}" cow 

In secondo luogo, declare :

 $ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo" cow 

Uniscili insieme:

 # Set a value: declare "array_$index=$value" # Get a value: arrayGet() { local array=$1 index=$2 local i="${array}_$index" printf '%s' "${!i}" } 

Usiamolo:

 $ sound=moo $ animal=cow $ declare "animals_$sound=$animal" $ arrayGet animals "$sound" cow 

Nota: declare non può essere inserito in una funzione. Qualsiasi uso di declare all’interno di una funzione bash trasforma la variabile che crea localmente nello scope di quella funzione, nel senso che non possiamo accedere o modificare array globali con esso. (In bash 4 puoi usare declare -g per dichiarare le variabili globali – ma in bash 4, dovresti usare gli array associativi in ​​primo luogo, non questo hack.)

Sommario

Esegui l’upgrade a bash 4 e utilizza declare -A . Se non puoi, awk a passare completamente a awk prima di fare brutti hack come descritto sopra. E sicuramente stare alla larga dallo spettacolo di hacker.

C’è la sostituzione dei parametri, anche se potrebbe anche non essere PC … come l’indirezione.

 #!/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]##*:}" 

Il modo BASH 4 è certamente migliore, ma se hai bisogno di un trucco … lo farà solo un trucco. È ansible cercare l’array / hash con tecniche simili.

Questo è quello che stavo cercando qui:

 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 

Questo non ha funzionato per me con bash 4.1.5:

 animals=( ["moo"]="cow" ) 

È ansible modificare ulteriormente l’interfaccia hput () / hget () in modo da avere hash denominati come segue:

 hput() { eval "$1""$2"='$3' } hget() { eval echo '${'"$1$2"'#hash}' } 

e poi

 hput capitals France Paris hput capitals Netherlands Amsterdam hput capitals Spain Madrid echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain` 

Questo ti consente di definire altre mappe che non entrano in conflitto (ad esempio, “rcapitals” che ricerca il paese per capitale). Ma, in entrambi i casi, penso che scoprirai che tutto ciò è piuttosto terribile, dal punto di vista delle prestazioni.

Se vuoi davvero una rapida ricerca hash, c’è un attacco terribile e terribile che funziona davvero molto bene. È questo: scrivi la tua chiave / i valori in un file temporaneo, uno per riga, quindi usa “grep” ^ $ key “” per estrarli, usando pipe con cut o awk o sed o qualsiasi altra cosa per recuperare i valori.

Come ho detto, sembra terribile, e sembra che dovrebbe essere lento e fare ogni sorta di IO non necessario, ma in pratica è molto veloce (la cache del disco è fantastica, vero?), Anche per hash molto grandi tabelle. Devi forzare te stesso l’unicità della chiave, ecc. Anche se hai solo poche centinaia di voci, il file di output / grep combo sarà un po ‘più veloce – nella mia esperienza molte volte più veloce. Mangia anche meno memoria.

Ecco un modo per farlo:

 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` 
 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 

Si consideri una soluzione che utilizza il built-in di bash come illustrato nello snippet di codice da uno script ufw firewall che segue. Questo approccio ha il vantaggio di utilizzare tanti set di campi delimitati (non solo 2) come desiderato. Abbiamo usato il | delimitatore perché gli specificatori di intervallo di porte possono richiedere un due punti, ad esempio 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 

Basta usare il file system

Il file system è una struttura ad albero che può essere utilizzata come una mappa hash. La tua tabella hash sarà una directory temporanea, le tue chiavi saranno nomi di file ei tuoi valori saranno i contenuti del file. Il vantaggio è che può gestire enormi hashmap e non richiede una shell specifica.

Creazione di Hashtable

hashtable=$(mktemp -d)

Aggiungi un elemento

echo $value > $hashtable/$key

Leggi un elemento

value=$(< $hashtable/$key)

Prestazione

Certo, è lento, ma non così lento. L'ho provato sulla mia macchina, con un SSD e btrfs , e fa circa 3000 elementi di lettura / scrittura al secondo .

Sono d’accordo con @lhunath e altri che l’array associativo è la strada da percorrere con Bash 4. Se sei bloccato su Bash 3 (OSX, vecchie distribuzioni che non puoi aggiornare) puoi usare anche expr, che dovrebbe essere ovunque, una stringa e espressioni regolari. Mi piace soprattutto quando il dizionario non è troppo grande.

  1. Scegli 2 separatori che non utilizzerai in chiavi e valori (es. ‘,’ E ‘:’)
  2. Scrivi la tua mappa come una stringa (nota il separatore ‘,’ anche all’inizio e alla fine)

     animals=",moo:cow,woof:dog," 
  3. Usa un’espressione regolare per estrarre i valori

     get_animal { echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")" } 
  4. Dividere la stringa per elencare gli elementi

     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 } 

Ora puoi usarlo:

 $ animal = get_animal "moo" cow $ get_animal_items cow likes to moo dog likes to woof 

Mi è piaciuta la risposta di Al P, ma volevo che l’unicità venisse applicata a buon mercato, quindi ho fatto un passo in più – usa una directory. Esistono alcune ovvie limitazioni (limiti del file di directory, nomi di file non validi) ma dovrebbe funzionare nella maggior parte dei casi.

 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 

Esegue anche un po ‘meglio nei miei test.

 $ 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 

Ho pensato di inserirmi. Ciao!

Modifica: Aggiunta di hdestroy ()

Due cose, puoi utilizzare la memoria al posto di / tmp in qualsiasi kernel 2.6 usando / dev / shm (Redhat) altre distribuzioni possono variare. Anche hget può essere reimplementato usando come segue:

 function hget { while read key idx do if [ $key = $2 ] then echo $idx return fi done < /dev/shm/hashmap.$1 } 

Inoltre, assumendo che tutti i tasti siano univoci, il ritorno cortocircuisce il ciclo di lettura e impedisce la lettura di tutte le voci. Se la tua implementazione può avere chiavi duplicate, lascia semplicemente fuori il reso. Ciò consente di risparmiare le spese di lettura e biforcazione sia di grep che di awk. L'utilizzo di / dev / shm per entrambe le implementazioni ha prodotto il seguente utilizzo dell'hget tempo su un hash di 3 voci alla ricerca dell'ultima voce:

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 

su più invocazioni non ho mai visto meno di un miglioramento del 50%. Questo può essere attribuito a fork fork, a causa dell'uso di /dev/shm .

Bash 3 soluzione:

Leggendo alcune delle risposte, ho messo insieme una breve e rapida funzione che mi piacerebbe dare un contributo che potrebbe aiutare gli altri.

 # 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[@]) 

Prima di bash 4 non esiste un buon modo per utilizzare gli array associativi in ​​bash. La soluzione migliore è utilizzare un linguaggio interpretato che supporti effettivamente tali elementi, come awk. D’altra parte, bash 4 li supporta.

Per quanto riguarda i modi meno buoni in bash 3, ecco un riferimento che potrebbe aiutare: http://mywiki.wooledge.org/BashFAQ/006

Un collega ha appena menzionato questo thread. Ho implementato in modo indipendente le tabelle hash all’interno di bash e non dipende dalla versione 4. Da un post sul mio blog di marzo 2010 (prima di alcune delle risposte qui …) intitolato Hash tables in 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 

Certo, fa una chiamata esterna per cksum ed è quindi un po ‘rallentato, ma l’implementazione è molto pulita e utilizzabile. Non è bidirezionale, e il modo integrato è molto meglio, ma nessuno dei due dovrebbe essere utilizzato in ogni caso. Bash è per una tantum, e cose del genere dovrebbero raramente comportare una complessità che potrebbe richiedere degli hash, tranne forse nel tuo .bashrc e nei tuoi amici.

Per ottenere un po ‘più di prestazioni, ricordati che grep ha una funzione di stop, da fermarsi quando trova la n-esima corrispondenza in questo caso n sarebbe 1.

grep –max_count = 1 … o grep -m 1 …

Ho anche usato il modo bash4 ma trovo e fastidioso bug.

Avevo bisogno di aggiornare dynamicmente il contenuto dell’array associativo così mi sono comportato in questo modo:

 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 

Scopro che con bash 4.3.11 l’aggiunta a una chiave esistente nel dict ha comportato l’aggiunta del valore se già presente. Quindi, ad esempio dopo una certa ripetizione il contenuto del valore era “checkKOcheckKOallCheckOK” e questo non era buono.

Nessun problema con bash 4.3.39 dove appenging una chiave esistente significa sottostare il valore actuale se già presente.

Ho risolto questo problema semplicemente pulendo / dichiarando lo array associativo statusCheck prima del ciclo:

 unset statusCheck; declare -A statusCheck 

Creo HashMaps in bash 3 usando variabili dinamiche. Ho spiegato come funziona nella mia risposta a: Array associativi negli script di Shell

Puoi anche dare un’occhiata a shell_map , che è un’implementazione di HashMap fatta in bash 3.