Jak zdefiniować tabele skrótów w Bash?

556

Co jest odpowiednikiem słowników Python, ale w Bash (powinno działać w systemach OS X i Linux).

Sridhar Ratnakumar
źródło
4
Poproś bash o uruchomienie skryptu python / perl ... To bardzo elastyczne!
e2-e4,
Rozważ użycie xonsh (jest na github).
Oliver,

Odpowiedzi:

938

Bash 4

Bash 4 natywnie obsługuje tę funkcję. Upewnij się hashbang skrypcie jest #!/usr/bin/env bashalbo #!/bin/bashwięc nie kończy się przy użyciu sh. Upewnij się, że albo wykonujesz skrypt bezpośrednio, albo wykonujesz scriptgo bash script. (Nie faktycznie wykonujący skrypt Bash z Basha nie stało, i będzie bardzo mylące!)

Deklarujesz tablicę asocjacyjną, wykonując:

declare -A animals

Możesz wypełnić go elementami za pomocą normalnego operatora przypisania tablicy. Na przykład, jeśli chcesz mieć mapę animal[sound(key)] = animal(value):

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

Lub scal je:

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

Następnie używaj ich tak jak normalnych tablic. Posługiwać się

  • animals['key']='value' ustawić wartość

  • "${animals[@]}" aby rozwinąć wartości

  • "${!animals[@]}"(zauważ !), aby rozwinąć 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 ich evaldo emulacji . Unikaj evaljak zarazy, ponieważ jest to plaga skryptów powłoki. Najważniejszym powodem jest to, że evaltraktuje twoje dane jako kod wykonywalny (istnieje również wiele innych powodów).

Przede wszystkim : rozważ aktualizację do wersji bash 4. Ułatwi to cały proces.

Jeśli istnieje powód, dla którego nie można dokonać aktualizacji, declarejest to znacznie bezpieczniejsza opcja. Nie ocenia danych jako kodu bashowegoeval robi i jako taki nie pozwala na tak łatwe wprowadzanie dowolnego kodu.

Przygotujmy odpowiedź, wprowadzając pojęcia:

Po pierwsze, pośredniość.

$ 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łącz je razem:

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

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

Użyjmy tego:

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

Uwaga: declarenie można wprowadzić funkcji. Każde użycie declarefunkcji bash zmienia lokalną zmienną, którą tworzy się w zakres tej funkcji, co oznacza, że ​​nie możemy uzyskać do niej dostępu ani modyfikować tablic globalnych. (W bash 4 możesz użyć deklaruj -g, aby deklarować zmienne globalne - ale w bash 4 możesz w pierwszej kolejności używać tablic asocjacyjnych, unikając tego obejścia).

Podsumowanie:

  • Uaktualnij do wersji bash 4 i użyj declare -Adla tablic asocjacyjnych.
  • Użyj declare opcji, jeśli nie możesz zaktualizować.
  • Rozważ użycie awkzamiast tego i całkowicie unikaj problemu.
lhunath
źródło
1
@Richard: Prawdopodobnie nie używasz bash. Czy twój hashbang sh zamiast bash, czy w inny sposób wywołujesz swój kod za pomocą sh? Spróbuj umieścić to tuż przed deklarowaniem: echo „$ BASH_VERSION $ POSIXLY_CORRECT”, powinno wyjść, 4.xa nie y.
lhunath,
5
Nie można zaktualizować: jedynym powodem, dla którego piszę skrypty w Bash, jest przenośność „uruchom gdziekolwiek”. Oparcie się na nie-uniwersalnej funkcji Bash wyklucza to podejście. Szkoda, bo inaczej byłoby to dla mnie doskonałe rozwiązanie!
Steve Pitchers
3
Szkoda, że ​​OSX domyślnie ustawia się na Bash 3, ponieważ dla wielu osób jest to „domyślny”. Myślałem, że przestraszenie ShellShock mogło być naciskiem, którego potrzebowali, ale najwyraźniej nie.
Ken
13
@ken to problem z licencją. Bash na OSX utknął w najnowszej kompilacji bez licencji GPLv3.
lhunath
2
... lub sudo port install bashdla tych (mądrze, IMHO), którzy nie chcą, aby katalogi w PATH były zapisywane dla wszystkich użytkowników bez wyraźnej eskalacji uprawnień dla poszczególnych procesów.
Charles Duffy,
125

Istnieje podstawianie parametrów, chociaż może to być również nie na PC ... jak pośrednie.

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

Oczywiście sposób BASH 4 jest lepszy, ale jeśli potrzebujesz hacka ... wystarczy hack. Możesz przeszukiwać tablicę / skrót za pomocą podobnych technik.

Bubnoff
źródło
5
VALUE=${animal#*:}ARRAY[$x]="caesar:come:see:conquer"
Zmieniłbym
2
Przydatne jest również umieszczanie podwójnych cudzysłowów wokół $ {ARRAY [@]} na wypadek, gdyby w klawiszach lub wartościach były spacje, jak wfor animal in "${ARRAY[@]}"; do
devguydavid
1
Ale czy wydajność nie jest dość niska? Myślę o O (n * m), jeśli chcesz porównać z inną listą kluczy, zamiast O (n) z odpowiednimi skrótami (ciągłe wyszukiwanie czasu, O (1) dla pojedynczego klucza).
CodeManX,
1
Pomysł dotyczy mniej wydajności, a bardziej zrozumienia / umiejętności czytania dla osób z doświadczeniem w perlu, pythonie lub nawet bash 4. Pozwala pisać w podobny sposób.
Bubnoff,
1
@CoDEmanX: jest to hack , sprytne i eleganckie, ale wciąż szczątkowe obejście, które ma pomóc biednym duszom, które wciąż tkwią w 2007 roku dzięki Bash 3.x. W tak prostym kodzie nie można się spodziewać „odpowiednich skrótów” lub rozważań dotyczących wydajności.
MestreLion
85

Oto, czego szukałem tutaj:

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

Nie działało to dla mnie w wersji bash 4.1.5:

animals=( ["moo"]="cow" )
aktivb
źródło
2
Zauważ, że wartość nie może zawierać spacji, w przeciwnym razie dodasz więcej elementów naraz
rubo77
6
Upvote dla składni hashmap ["key"] = "value", której również nie znalazłem w fantastycznie przyjętej odpowiedzi.
thomanski
@ Klucz rubo77 też, dodaje wiele kluczy. Jakiś sposób to obejść?
Xeverous
25

Możesz dalej zmodyfikować interfejs hput () / hget (), tak aby nazwać skróty w następujący sposób:

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

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

i wtedy

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

Pozwala to zdefiniować inne mapy, które nie powodują konfliktów (np. „Rcapitals”, które wyszukują dane według stolic). Ale tak czy inaczej, myślę, że przekonasz się, że to wszystko jest okropne, pod względem wydajności.

Jeśli naprawdę chcesz szybkiego wyszukiwania skrótów, istnieje straszny, straszny hack, który naprawdę działa naprawdę dobrze. Jest to: zapisz klucz / wartości do pliku tymczasowego, jeden na linię, a następnie użyj „grep” ^ $ klucz ”, aby je wyciągnąć, używając rur z cięciem, awk, sed lub cokolwiek innego, aby odzyskać wartości.

Tak jak powiedziałem, brzmi to okropnie i wygląda na to, że powinien być powolny i robić wszelkiego rodzaju niepotrzebne operacje wejścia / wyjścia, ale w praktyce jest bardzo szybki (pamięć podręczna dysku jest niesamowita, prawda?), Nawet w przypadku bardzo dużego skrótu stoły Sam musisz wymusić unikalność klucza itp. Nawet jeśli masz tylko kilkaset wpisów, kombinacja pliku wyjściowego / grep będzie nieco szybsza - z mojego doświadczenia kilka razy szybsza. Zjada także mniej pamięci.

Oto jeden ze sposobów, 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`
Al P.
źródło
1
Świetny! możesz nawet iterować: dla i w $ (compgen -A zmienne kapitały); zrobić „$ i” „” zrobione
zhaorufei
22

Po prostu użyj systemu plików

System plików to struktura drzewa, która może być używana jako mapa mieszania. Tabela skrótów będzie katalogiem tymczasowym, klucze będą nazwami plików, a wartości będą zawartością plików. Zaletą jest to, że może obsługiwać ogromne mapy skrótów i nie wymaga konkretnej powłoki.

Tworzenie haszyszu

hashtable=$(mktemp -d)

Dodaj element

echo $value > $hashtable/$key

Przeczytaj element

value=$(< $hashtable/$key)

Wydajność

Oczywiście jest wolny, ale nie taki wolny. Przetestowałem to na moim komputerze, z dyskiem SSD i btrfs , i robi około 3000 elementów odczytu / zapisu na sekundę .

lovasoa
źródło
1
Która wersja bash obsługuje mkdir -d? (Nie 4.3, na Ubuntu 14. Chciałbym skorzystać z mkdir /run/shm/foo, lub jeśli to zapełniłoby RAM mkdir /tmp/foo
,.
1
Może mktemp -dzamiast tego miał na myśli?
Reid Ellis,
2
Ciekawe, jaka jest różnica między $value=$(< $hashtable/$key)i value=$(< $hashtable/$key)? Dzięki!
Helin Wang
1
„przetestowałem to na moim komputerze”. To brzmi jak świetny sposób na wypalenie dziury przez dysk SSD. Nie wszystkie dystrybucje Linuksa domyślnie używają tmpfs.
kirbyfan64sos
Przetwarzam około 50000 skrótów. Perl i PHP robią to o włosach poniżej 1/2 sekundy. Węzeł za 1 sekundę i coś. Opcja FS brzmi wolno. Czy jednak możemy się upewnić, że pliki istnieją tylko w pamięci RAM?
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
DigitalRoss
źródło
31
Westchnienie, wydaje się to niepotrzebnie obraźliwe, a i tak jest niedokładne. Nie umieszcza się sprawdzania poprawności wejścia, zmiany znaczenia ani kodowania (zobacz, tak naprawdę wiem) w tekście tabeli skrótów, ale raczej w opakowaniu i jak najszybciej po wprowadzeniu.
DigitalRoss,
@DigitalRoss możesz wyjaśnić, jakie jest zastosowanie #hash w eval echo „$ {hash” „$ 1” „# hash}” . dla mnie wydaje mi się, że to nie komentarz. czy #hash ma tutaj jakieś specjalne znaczenie?
Sanjay,
@Sanjay ${var#start}usuwa tekst zaczynający się od początku wartości przechowywanej w zmiennej var .
jpaugh
11

Rozważ rozwiązanie wykorzystujące wbudowany bash odczytany jak pokazano we fragmencie kodu z następującego skryptu zapory ufw. Zaletą tego podejścia jest wykorzystanie dowolnej liczby zestawów pól rozdzielanych (nie tylko 2). Użyliśmy | ogranicznik, ponieważ specyfikatory zakresu portów mogą wymagać dwukropka, tj. 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
AsymLabs
źródło
2
@CharlieMartin: read jest bardzo potężną funkcją i jest niedostatecznie wykorzystywany przez wielu programistów bash. Umożliwia kompaktowe formy przetwarzania list podobnych do seplenienia . Na przykład w powyższym przykładzie możemy usunąć tylko pierwszy element i zachować resztę (tj. Podobną koncepcję do pierwszej i odpoczynku w seplenieniu), wykonując:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs
6

Zgadzam się z @lhunath i innymi, że tablica asocjacyjna jest najlepszym rozwiązaniem dla Bash 4. Jeśli utkniesz w Bash 3 (OSX, stare dystrybucje, których nie możesz zaktualizować), możesz również użyć wyrażenia, które powinno być wszędzie, ciąg i wyrażenia regularne. Podoba mi się to 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. Napisz swoją mapę jako ciąg znaków (zwróć uwagę na separator „,” również na początku i na końcu)

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

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. Podziel ciąg, 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
marco
źródło
5

Naprawdę podobała mi się odpowiedź Al P, ale chciałem tanio egzekwować tanio, więc posunąłem się o krok dalej - skorzystaj z katalogu. Istnieją pewne oczywiste ograniczenia (limity plików katalogu, 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

W moich testach działa też odrobinę lepiej.

$ 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ę.

Edycja: dodawanie hdestroy ()

Cole Stanfield
źródło
3

Dwie rzeczy, możesz użyć 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żna ponownie zaimplementować, używając następującego odczytu:

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, powrót powoduje 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. Oszczędza to kosztów czytania i tworzenia zarówno grep, jak i awk. Użycie / dev / shm dla obu implementacji dało następujące użycie hgetu czasowego w haszu z 3 pozycjami w poszukiwaniu 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

Odczyt / 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. Można to wszystko przypisać widelcowi ponad głową, ze względu na użycie /dev/shm.

jrichard
źródło
3

Współpracownik właśnie wspomniał o tym wątku. Niezależnie zaimplementowałem tabele skrótów w bash i nie jest to zależne od wersji 4. Z mojego posta na blogu w marcu 2010 roku (przed niektórymi odpowiedziami tutaj ...) zatytułowanym Tabele skrótów w bash :

Ja poprzednio używany cksumdo hash ale ponieważ tłumaczone ciąg hashCode Javy do rodzimej 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)

Nie jest dwukierunkowy, a wbudowany sposób jest o wiele lepszy, ale i tak nie należy go używać. Bash jest przeznaczony do szybkich działań jednorazowych i takie rzeczy rzadko powinny wiązać się ze złożonością, która może wymagać skrótów, chyba że u ciebie ~/.bashrci twoich przyjaciół.

Adam Katz
źródło
Link w odpowiedzi jest przerażający! Po kliknięciu utkniesz w pętli przekierowania. Proszę zaktualizować.
Rakib
1
@MohammadRakibAmin - Tak, moja strona nie działa i wątpię, czy wskrzeszę mojego bloga. Zaktualizowałem powyższy link do zarchiwizowanej wersji. Dzięki za zainteresowanie!
Adam Katz
2

Przed bash 4 nie ma dobrego sposobu na użycie tablic asocjacyjnych w bash. Najlepiej jest użyć przetłumaczonego języka, który faktycznie obsługuje takie rzeczy, jak awk. Z drugiej strony bash 4 tak wspierać je.

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

kojiro
źródło
2

Rozwiązanie Bash 3:

Czytając niektóre odpowiedzi, przygotowałem krótką funkcję, którą chciałbym wesprzeć, która mogłaby 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[@])
Milan Adamovsky
źródło
Myślę, że to całkiem fajny fragment. Przydałoby się trochę oczyszczania (choć niewiele). W mojej wersji zmieniłem nazwę „klucz” na „parowanie” i utworzyłem KEY i VALUE małe litery (ponieważ używam wielkich liter podczas eksportowania zmiennych). Zmieniłem również nazwę getHashKey na getHashValue i nadałem kluczowi i wartości wartość lokalną (czasem jednak nie chcesz, aby były lokalne). W getHashKeys nie przypisuję niczego do wartości. Do separacji używam średnika, ponieważ moje wartości to adresy URL.
0

Użyłem również sposobu bash4, ale znajduję irytujący błąd.

Musiałem dynamicznie aktualizować zawartość tablicy asocjacyjnej, więc skorzystałem z 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

Przekonałem się, że dodanie bash 4.3.11 do istniejącego klucza w dykcie spowodowało dodanie wartości, jeśli już jest obecna. Na przykład po pewnym powtórzeniu treść wartości brzmiała „checkKOcheckKOallCheckOK”, co nie było dobre.

Nie ma problemu z wersją bash 4.3.39, gdzie dołączenie istniejącego klucza oznacza zastąpienie wartości aktualnej, jeśli jest już obecna.

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

unset statusCheck; declare -A statusCheck
Alex
źródło