Come definire le tabelle hash in Bash?


Risposte:


939

Bash 4

Bash 4 supporta nativamente questa funzione. Assicurati che l'hashbang del tuo script sia #!/usr/bin/env basho meno #!/bin/bashcosì non finisci per usarlo sh. Assicurati di eseguire direttamente lo script o di eseguirlo scriptcon bash script. (Non è in realtà l'esecuzione di uno script Bash con Bash non accada, e sarà davvero confondere!)

Dichiara una matrice associativa facendo:

declare -A animals

È possibile riempirlo con elementi utilizzando il normale operatore di assegnazione di array. Ad esempio, se si desidera avere una mappa di animal[sound(key)] = animal(value):

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

O uniscili:

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

Quindi utilizzali come normali array. Uso

  • animals['key']='value' per impostare il valore

  • "${animals[@]}" per espandere i valori

  • "${!animals[@]}"(notare il !) per espandere le chiavi

Non dimenticare di citarli:

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

Bash 3

Prima della bash 4, non hai array associativi. Non usare evalper emularli . Evita evalcome la peste, perché è la piaga degli script di shell. Il motivo più importante è quelloeval tratta i tuoi dati come codice eseguibile (ci sono anche molti altri motivi).

Innanzitutto : considera l'aggiornamento a bash 4. Questo renderà l'intero processo molto più facile per te.

Se c'è un motivo che non è possibile aggiornare, declareè un'opzione molto più sicura. Non valuta i dati come codice bash comeeval fa, e come tale non consente l'iniezione di codice arbitrario così facilmente.

Prepariamo la risposta introducendo i concetti:

Innanzitutto, il riferimento indiretto.

$ 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

Riuniscili:

# 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: declarenon può essere inserito in una funzione. Qualsiasi uso declareall'interno di una funzione bash trasforma la variabile che crea locale nell'ambito di quella funzione, il che significa che non possiamo accedere o modificare array globali con essa. (In bash 4 puoi usare declare -g per dichiarare le variabili globali - ma in bash 4, puoi usare le matrici associative in primo luogo, evitando questa soluzione alternativa.)

Sommario:

  • Esegui l'upgrade a bash 4 e utilizzalo declare -Aper array associativi.
  • Utilizzare l' declareopzione se non è possibile eseguire l'aggiornamento.
  • Valuta awkinvece di utilizzare ed evita del tutto il problema.

1
@Richard: Presumibilmente, in realtà non stai usando bash. Il tuo hashbang sh invece di bash o stai invocando il tuo codice con sh? Prova a metterlo subito prima di dichiarare: echo "$ BASH_VERSION $ POSIXLY_CORRECT", dovrebbe essere generato 4.xe non y.
lhunath,

5
Impossibile eseguire l'aggiornamento: l'unico motivo per cui scrivo script in Bash è la portabilità "corri ovunque". Quindi basarsi su una caratteristica non universale di Bash esclude questo approccio. È un peccato, perché altrimenti sarebbe stata un'ottima soluzione per me!
Steve Pitchers,

3
È un peccato che OSX sia impostato su Bash 3 come impostazione predefinita per molte persone. Pensavo che lo spavento di ShellShock potesse essere stata la spinta di cui avevano bisogno ma apparentemente no.
Ken,

13
@ken è un problema di licenza. Bash su OSX è bloccato all'ultima build con licenza non GPLv3.
lhunath,

2
... o sudo port install bash, per quelli (saggiamente, IMHO) riluttanti a rendere scrivibili le directory nel PERCORSO per tutti gli utenti senza esplicita escalation di privilegi per processo.
Charles Duffy,

125

C'è la sostituzione dei parametri, anche se potrebbe essere anche un PC-come ... come l'indirizzamento.

#!/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 è ovviamente migliore, ma se hai bisogno di un hack ... solo un hack lo farà. Puoi cercare l'array / hash con tecniche simili.


5
Lo cambierei VALUE=${animal#*:}per proteggere il caso in cuiARRAY[$x]="caesar:come:see:conquer"
Glenn Jackman,

2
È anche utile mettere virgolette doppie attorno a $ {ARRAY [@]} nel caso in cui ci siano spazi nelle chiavi o nei valori, come infor animal in "${ARRAY[@]}"; do
devguydavid

1
Ma l'efficienza non è abbastanza scarsa? Sto pensando a O (n * m) se si desidera confrontare un altro elenco di chiavi, invece di O (n) con hashap appropriati (ricerca del tempo costante, O (1) per un singolo tasto).
CodeManX,

1
L'idea è meno sull'efficienza, più sulla comprensione / capacità di lettura per coloro che hanno un background in perl, python o addirittura bash 4. Ti permette di scrivere in modo simile.
Bubnoff,

1
@CoDEmanX: si tratta di un trucco , un intelligente ed elegante, ma ancora rudimentale soluzione per aiutare le anime povere ancora bloccato nel 2007 con Bash 3.x. Non puoi aspettarti "hashaps corretti" o considerazioni sull'efficienza in un codice così semplice.
MestreLion,

85

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

2
Nota che il valore potrebbe non contenere spazi, altrimenti aggiungi più elementi contemporaneamente
rubo77

6
Valorizza la sintassi hashmap ["key"] = "value" che anch'io ho trovato mancante dalla risposta altrimenti accettata.
Thomanski,

@ chiave rubo77 nessuno dei due, aggiunge più chiavi. Un modo per aggirare questo?
Xeverous

25

È possibile modificare ulteriormente l'interfaccia hput () / hget () in modo da aver nominato gli hash 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`

Ciò consente di definire altre mappe che non sono in conflitto (ad esempio, "rcapitals" che effettuano la ricerca per paese per capitale). Ma, in entrambi i casi, penso che scoprirai che è tutto piuttosto terribile, dal punto di vista delle prestazioni.

Se vuoi davvero una ricerca hash veloce, c'è un hack 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 qualunque 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, non è vero?), Anche per hash molto grandi tabelle. Devi imporre te stesso l'unicità chiave, ecc. Anche se hai solo poche centinaia di voci, la combinazione di file di output / grep 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`

1
Grande! puoi persino iterarlo: per i in $ (compgen -A maiuscolo); hget "$ i" "" done
zhaorufei,

22

Usa semplicemente il file system

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

Creazione 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 in lettura / scrittura al secondo .


1
Quale versione di bash supporta mkdir -d? (Non 4.3, su Ubuntu 14. Ricorrerei a mkdir /run/shm/foo, o se quello avesse riempito la RAM mkdir /tmp/foo
,.

1
Forse mktemp -dinvece era destinato?
Reid Ellis,

2
Curioso qual è la differenza tra $value=$(< $hashtable/$key)e value=$(< $hashtable/$key)? Grazie!
Helin Wang,

1
"testato sulla mia macchina" Sembra un ottimo modo per creare un buco nel tuo SSD. Per impostazione predefinita, non tutte le distribuzioni Linux utilizzano tmpfs.
Kirbyfan64sos,

Sto elaborando circa 50000 hash. Perl e PHP fanno i capelli meno di 1/2 secondo. Nodo in 1 secondo e qualcosa del genere. L'opzione FS sembra lenta. Tuttavia, possiamo assicurarci che i file esistano solo nella RAM, in qualche modo?
Rolf,

14
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

31
Sospiro, ciò sembra inutilmente offensivo ed è comunque impreciso. Uno non inserisce la convalida dell'input, l'escaping o la codifica (vedi, in realtà lo so) nelle viscere della tabella hash, ma piuttosto in un wrapper e il più presto possibile dopo l'input.
DigitalRoss

@DigitalRoss puoi spiegare a cosa serve #hash nell'eco eval '$ {hash' "$ 1" '# hash}' . per me mi sembra un commento non più di quello. #hash ha un significato speciale qui?
Sanjay,

@Sanjay ${var#start}rimuove l' inizio del testo dall'inizio del valore memorizzato nella variabile var .
jpaugh

11

Prendere in considerazione una soluzione che utilizza la bash integrato di lettura , come illustrato nel frammento di codice da uno script firewall ufw che segue. Questo approccio ha il vantaggio di utilizzare tutti i set di campi delimitati (non solo 2) desiderati. Abbiamo usato il | delimitatore perché gli identificatori dell'intervallo di porte potrebbero richiedere due punti, ovvero 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

2
@CharlieMartin: read è una funzione molto potente ed è sottoutilizzata da molti programmatori bash. Consente forme compatte di elaborazione di elenchi simili a lisp . Ad esempio, nell'esempio precedente possiamo rimuovere solo il primo elemento e conservare il resto (ovvero un concetto simile al primo e riposare in lisp) facendo:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs

6

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 distro che non puoi aggiornare) puoi usare anche expr, che dovrebbe essere ovunque, una stringa ed espressioni regolari. Mi piace soprattutto quando il dizionario non è troppo grande.

  1. Scegli 2 separatori che non utilizzerai nelle chiavi e nei valori (ad 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. Utilizzare una regex per estrarre i valori

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. Dividi 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

5

Mi è piaciuta molto la risposta di Al P, ma volevo che l'unicità venisse rafforzata a buon mercato, quindi ho fatto un ulteriore passo avanti: utilizzare una directory. Esistono alcune ovvie limitazioni (limiti dei 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

Si comporta 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 intervenire. Saluti!

Modifica: aggiunta di hdestroy ()


3

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

function hget {

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

Inoltre, supponendo che tutti i tasti siano univoci, il ritorno mette in corto circuito il ciclo di lettura e impedisce di leggere tutte le voci. Se l'implementazione può avere chiavi duplicate, semplicemente tralascia il ritorno. Ciò consente di risparmiare le spese di lettura e fork sia grep che awk. L'uso di / dev / shm per entrambe le implementazioni ha prodotto quanto segue usando time hget su un hash a 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

Lettura / 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%. Tutto ciò può essere attribuito a fork over head, a causa dell'uso di /dev/shm.


3

Un collega ha appena menzionato questa discussione. Ho implementato le tabelle hash in modo indipendente all'interno di bash e non dipende dalla versione 4. Da un mio post sul blog nel marzo 2010 (prima di alcune delle risposte qui ...) intitolato Tabelle hash in bash :

In precedenza ho usato cksuml'hash ma da allora ho tradotto la stringa Java hashCode in bash / zsh nativo.

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

Non è bidirezionale e il modo integrato è molto meglio, ma nessuno dei due dovrebbe essere utilizzato comunque. Bash è per una rapida tantum, e cose del genere dovrebbero raramente comportare complessità che potrebbe richiedere hash, tranne forse nei tuoi ~/.bashrce amici.


Il link nella risposta è spaventoso! Se fai clic su di esso, sei bloccato in un ciclo di reindirizzamento. Per favore aggiornare.
Rakib,

1
@MohammadRakibAmin - Sì, il mio sito Web non funziona e dubito che farò risorgere il mio blog. Ho aggiornato il link sopra a una versione archiviata. Grazie per il tuo interesse!
Adam Katz,

2

Prima di bash 4 non esiste un buon modo per usare array associativi in ​​bash. La tua scommessa migliore è usare un linguaggio interpretato che in realtà ha il supporto per cose del genere, come awk. D'altra parte, bash 4 non li supporta.

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


2

Soluzione Bash 3:

Nel leggere alcune delle risposte ho messo insieme una piccola funzione veloce che vorrei 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[@])

Penso che questo sia un frammento piuttosto ordinato. Potrebbe usare un po 'di pulizia (non molto, però). Nella mia versione, ho rinominato 'key' in 'pair' e ho reso KEY e VALUE minuscole (perché utilizzo le maiuscole quando le variabili vengono esportate). Ho anche rinominato getHashKey in getHashValue e reso sia la chiave che il valore locali (a volte, tuttavia, vorresti che non fossero locali). In getHashKeys, non assegno nulla al valore. Uso il punto e virgola per la separazione, poiché i miei valori sono URL.

0

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

Avevo bisogno di aggiornare dinamicamente il contenuto dell'array associativo, quindi ho usato 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 in cui ottenere una chiave esistente significa sostituire il valore attuale se già presente.

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

unset statusCheck; declare -A statusCheck

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.