Jak definiować tabele hash w Bash?

Jaki jest odpowiednik słowników Pythona ale w Bash (powinien działać na OS X i Linux).

Author: codeforester, 2009-09-29

16 answers

Bash 4

Bash 4 natywnie obsługuje tę funkcję. Upewnij się, że hashbang twojego skryptu to #!/usr/bin/env bash lub #!/bin/bash lub cokolwiek innego, co odwołuje się do bash, a nie sh. Upewnij się, że wykonujesz skrypt i nie robisz czegoś głupiego, jak sh script, co spowodowałoby zignorowanie twojego bash hashbang. To jest podstawowe rzeczy, ale tak wielu wciąż zawodzi w tym, stąd ponowna iteracja.

Deklarujesz tablicę asocjacyjną wykonując:

declare -A animals

Możesz wypełnić go elementami użycie normalnego operatora przypisania tablicy:

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

Lub połączyć je:

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

Następnie używaj ich jak normalnych tablic. "${animals[@]}" rozszerza wartości, "${!animals[@]}" (zauważ, że !) rozszerza klucze. Nie zapomnij ich zacytować:

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

Bash 3

Przed bash 4 nie masz tablic asocjacyjnych. nie używaj eval do ich naśladowania . Należy unikać eval jak plaga, ponieważ to jest plaga skryptów powłoki. Najważniejsze powodem jest to, że nie chcesz traktować swoich danych jako kodu wykonywalnego (jest też wiele innych powodów).

Po pierwsze i najważniejsze: po prostu rozważ aktualizację do bash 4. Poważnie. przyszłość jest teraz , przestań żyć przeszłością i {41]} cierpieć z niej {42]} zmuszając głupie złamane i brzydkie hacki na Twój kod i każda biedna dusza utkwiła w jego utrzymaniu.

Jeśli masz jakąś głupią wymówkę, dlaczego " nie możesz uaktualnić", declare jest o wiele bezpieczniejszą opcją. Informatyka nie ocenia danych tak, jak robi to kod bash, jak robi to eval, i jako taki nie pozwala na dowolne wstrzyknięcie kodu tak łatwo.

Przygotujmy odpowiedź wprowadzając pojęcia:

Po pierwsze, indirection (poważnie; nigdy nie używaj tego, chyba że jesteś chory psychicznie lub masz inną złą wymówkę do pisania hacków).

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

Po Drugie, declare:

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

Połączyć:

# Set a value:
declare "array_$index=$value"

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

Let ' s use it:

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

Uwaga: declare nie można umieścić w funkcja. Każde użycie declare wewnątrz funkcji bash zamienia zmienną, którą tworzy local W Zakres tej funkcji, co oznacza, że nie możemy uzyskać dostępu ani modyfikować za pomocą niej tablic globalnych. (W bash 4 możesz użyć declare-g do deklarowania zmiennych globalnych - ale w bash 4 powinieneś używać tablic asocjacyjnych, a nie tego hacka.)

Podsumowanie

Uaktualnij do bash 4 i użyj declare -A. Jeśli nie możesz, rozważ całkowite przejście na awk, zanim zrobisz brzydkie hacki jako opisane powyżej. I zdecydowanie trzymaj się z dala od hakerstwa.

 680
Author: lhunath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-18 18:31:26

Istnieje podstawianie parametrów, choć może to być również un-PC ...jak indirection.

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

Sposób BASH 4 jest oczywiście lepszy, ale jeśli potrzebujesz hack ...wystarczy włamanie. Można przeszukiwać tablicę / hash za pomocą podobnych technik.

 94
Author: Bubnoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-09 18:11:26

Tego właśnie szukałem:

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

To nie działa dla mnie z bash 4.1.5:

animals=( ["moo"]="cow" )
 50
Author: aktivb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-23 08:36:05

Możesz dalej modyfikować interfejs hput()/hget() tak, że nazwałeś hasze w następujący sposób:

hput() {
    eval "$1""$2"='$3'
}

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

A następnie

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

To pozwala zdefiniować inne mapy, które nie są ze sobą sprzeczne (np. 'rcapitals', który przeszukuje kraj według stolicy). Ale tak czy siak, myślę, że przekonasz się, że to wszystko jest dość straszne, jeśli chodzi o wydajność.

Jeśli naprawdę chcesz szybkiego wyszukiwania hash, jest straszny, straszny hack, który naprawdę działa naprawdę dobrze. To jest to: napisz swój klucz / wartości do pliku tymczasowego, jeden-na linię, a następnie użyj 'grep" ^ $ key"', aby je wyciągnąć, używając rur z cut, awk, sed lub cokolwiek, aby pobrać wartości.

Jak już mówiłem, brzmi to okropnie i brzmi, jakby miało być wolne i robić wszelkiego rodzaju niepotrzebne IO, ale w praktyce jest bardzo szybkie (pamięć podręczna dysku jest niesamowita, prawda?), nawet dla bardzo dużych tabel hashowych. Musisz sam wyegzekwować wyjątkowość klucza itp. Nawet jeśli masz tylko kilkaset wpisów, combo plik wyjściowy / grep jest będzie trochę szybciej - z doświadczenia wiem, że kilka razy szybciej. Zużywa też mniej pamięci.

Jest jeden sposób, aby to zrobić:

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`
 22
Author: Al P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
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
 13
Author: DigitalRoss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-09-29 22:45:35

Rozważ rozwiązanie za pomocą wbudowanego w bash przeczytaj Jak pokazano w poniższym fragmencie kodu ze skryptu zapory ufw. To podejście ma tę zaletę, że używa się tak wielu rozdzielonych zestawów pól (nie tylko 2), Jak jest to pożądane. Wykorzystaliśmy | delimiter, ponieważ specyfikatory zakresu portów mogą wymagać dwukropka, czyli 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
 9
Author: AsymLabs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-15 17:16:30

Wystarczy użyć systemu plików

System plików jest strukturą drzewiastą, która może być używana jako mapa hash. Twoja tabela hash będzie katalogiem tymczasowym, Twoje klucze będą nazwami plików, a wartości będą zawartością plików. Zaletą jest to, że może obsługiwać ogromne hashmapy i nie wymaga specjalnej powłoki.

Tworzenie Hashtable

hashtable=$(mktemp -d)

Dodaj element

echo $value > $hashtable/$key

Odczytaj element

value=$(< $hashtable/$key)

Wydajność

Oczywiście, jego wolno, ale nie wolno. Testowałem go na moim komputerze, z dyskiem SSD i btrfs, i robi około 3000 elementów odczytu/zapisu na sekundę.

 8
Author: lovasoa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-06 13:21:19

Zgadzam się z @lhunath i innymi, że tablica asocjacyjna jest drogą do przejścia z Bashem 4. Jeśli utkniesz w Bash 3 (OSX, stare DISTRO, których nie możesz zaktualizować), możesz użyć również expr, które powinno być wszędzie, ciąg znaków i wyrażenia regularne. Podoba mi się szczególnie, gdy słownik nie jest zbyt duży.

  1. Wybierz 2 separatory, których nie będziesz używać w kluczach i wartościach (np. ',' I':')
  2. Zapisz mapę jako łańcuch znaków (zwróć uwagę na separator ',' również na początku i end)

    animals=",moo:cow,woof:dog,"
    
  3. Użyj wyrażenia regularnego, aby wyodrębnić wartości

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
    
  4. Podziel łańcuch, aby wyświetlić listę elementów

    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
    }
    

Teraz możesz go użyć:

$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
 6
Author: marco,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-01 15:13:10

Bardzo podobała mi się odpowiedź Ala P, Ale chciałem tanio wymusić wyjątkowość, więc poszedłem o krok dalej-skorzystałem z katalogu. Istnieją pewne oczywiste ograniczenia (limity plików katalogów, nieprawidłowe nazwy plików), ale powinno to działać w większości przypadków.

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

Sprawdza się też nieco lepiej w moich testach.

$ 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
Pomyślałem, że się wtrącę. Zdrowie!

Edit: dodanie hdestroy ()

 5
Author: Cole Stanfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-14 21:19:11

Dwie rzeczy, możesz używać pamięci zamiast / tmp w dowolnym jądrze 2.6 używając /dev / shm (Redhat) inne dystrybucje mogą się różnić. Również hget może być ponownie zaimplementowany za pomocą odczytu w następujący sposób:

function hget {

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

Ponadto, zakładając, że wszystkie klucze są unikalne, zwraca zwarcie pętli odczytu i zapobiega konieczności odczytu wszystkich wpisów. Jeśli twoja implementacja może mieć zduplikowane klucze, po prostu pomiń zwrot. Pozwala to zaoszczędzić koszty czytania i rozwidlania zarówno grep, jak i awk. Using / dev / shm dla obu implementacji uzyskano następujące użycie time hget na hash 3 wpisów szukających ostatniego wpisu:

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

Przy wielu inwokacjach nigdy nie widziałem mniej niż 50% poprawy. Wszystko to można przypisać widelcowi nad głową, ze względu na użycie /dev/shm.

 2
Author: jrichard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-15 17:44:08

Rozwiązanie Bash 3:

Czytając niektóre z odpowiedzi, stworzyłem krótką, małą funkcję, którą chciałbym wnieść z powrotem, która może pomóc innym.

# 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[@])
 2
Author: Milan Adamovsky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-29 15:26:17

Przed bash 4 nie ma dobrego sposobu na używanie tablic asocjacyjnych w bash. Najlepszym rozwiązaniem jest użycie języka interpretowanego, który rzeczywiście ma wsparcie dla takich rzeczy, jak awk. Z drugiej strony, bash 4 obsługuje je .

Jeśli chodzi o mniej dobrych sposobów w bash 3, Oto odniesienie, które może pomóc: http://mywiki.wooledge.org/BashFAQ/006

 1
Author: kojiro,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-08-12 12:53:37

Współpracownik właśnie wspomniał o tym wątku. Niezależnie zaimplementowałem tabele hash w bash i nie jest to zależne od wersji 4. Z mojego posta na blogu z marca 2010 (przed niektórymi odpowiedziami tutaj...) o nazwie tabele Hash w 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

Oczywiście, wywołuje zewnętrzne wywołanie cksum i dlatego jest nieco spowolniony, ale implementacja jest bardzo czysta i użyteczna. Nie jest dwukierunkowy, a wbudowany sposób jest o wiele lepszy, ale tak naprawdę nie powinien być używany w każdym razie. Bash jest do szybkich jednorazowych, a takie rzeczy powinny dość rzadko obejmować złożoność, która może wymagać skrótów, chyba że w Twoim .bashrc i przyjaciele.

 1
Author: Adam Katz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-18 00:39:57

Aby uzyskać nieco większą wydajność pamiętaj, że grep ma funkcję stop, aby zatrzymać, gdy znajdzie N-te dopasowanie w tym przypadku N będzie równe 1.

Grep --max_count=1 ... lub grep-m 1..

 0
Author: bozon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-04-01 15:47:27

Użyłem również sposobu bash4, ale znalazłem i denerwujący błąd.

Musiałem dynamicznie zaktualizować zawartość tablicy asocjacyjnej, więc użyłem tego sposobu:

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

Dowiedziałem się, że w bash 4.3.11 dołączanie do istniejącego klucza w dict skutkowało dodaniem wartości, jeśli już istnieje. Tak więc na przykład po pewnym powtórzeniu zawartość wartości była "checkKOcheckKOallCheckOK" i to nie było dobre.

Nie ma problemu z bash 4.3.39 gdzie appenging istniejącego klucza oznacza zastąp wartość actuale, jeśli już istnieje.

Rozwiązałem to po prostu czyszczenie / deklarowanie tablicy asocjacyjnej statusCheck przed cyklem:

unset statusCheck; declare -A statusCheck
 0
Author: Alex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-04 06:31:09

Tworzę Hashmapy w bash 3 używając zmiennych dynamicznych. Wyjaśniłem jak to działa w mojej odpowiedzi na: tablice asocjacyjne w skryptach Powłoki

Możesz też zajrzeć do shell_map, który jest implementacją HashMap wykonaną w bash 3.

 0
Author: Bruno Negrão Zica,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:42