Bash'te karma tablolar nasıl tanımlanır?


Yanıtlar:


938

Bash 4

Bash 4 bu özelliği doğal olarak desteklemektedir. Senaryonuzun hashbang olduğundan emin olun #!/usr/bin/env bashya da #!/bin/bashkullanarak bitirmek olmaz sh. Ya doğrudan scriptinizi emin olun veya yürütme scriptile bash script. (Aslında Bash ile bir Bash komut dosyası yürütme değil gelmez olur ve olacak gerçekten kafa karıştırıcı!)

Aşağıdakileri yaparak ilişkilendirilebilir bir dizi bildirirsiniz:

declare -A animals

Normal dizi atama işlecini kullanarak bunu öğelerle doldurabilirsiniz. Örneğin, aşağıdakilerin haritasını çıkarmak istiyorsanız animal[sound(key)] = animal(value):

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

Veya birleştirin:

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

Sonra onları normal diziler gibi kullanın. kullanım

  • animals['key']='value' değer ayarlamak

  • "${animals[@]}" değerleri genişletmek

  • "${!animals[@]}"(fark !tuşları genişletmek için)

Onlara teklif vermeyi unutmayın:

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

Bash 3

Bash 4'ten önce ilişkilendirilebilir dizileriniz yok. Onları taklit etmek için kullanmayıneval . Önlemek evalçünkü, veba gibi olan kabuk komut dosyası vebası. Bunun en önemli nedeni, evalverilerinize yürütülebilir kod olarak davranılmasıdır (başka birçok nedeni de vardır).

İlk ve en önemlisi : Bash 4'e geçmeyi düşünün. Bu, tüm süreci sizin için daha kolay hale getirecektir.

Yükseltme yapamamanızın bir nedeni varsa, declareçok daha güvenli bir seçenektir. Verileri bash kodu gibi değerlendirmez evalve bu nedenle rastgele kod enjeksiyonuna oldukça kolay izin vermez.

Cevabı kavramları tanıtarak hazırlayalım:

İlk olarak, dolaylı.

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

İkinci olarak, declare:

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

Onları bir araya getirin:

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

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

Kullanalım:

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

Not: declarebir işleve yerleştirilemez. declareBir bash işlevinin içindeki herhangi bir kullanım, yerel olarak oluşturduğu değişkeni bu işlevin kapsamına çevirir , yani global dizilere onunla erişemeyiz veya değiştiremeyiz. (Bash 4'te global değişkenleri bildirmek için declare -g komutunu kullanabilirsiniz - ancak bash 4'te bu çözümden kaçınarak ilk etapta ilişkisel diziler kullanabilirsiniz.)

Özet:

  • Bash 4'e yükseltin ve declare -Ailişkilendirilebilir diziler için kullanın .
  • declareYükseltme yapamıyorsanız seçeneği kullanın .
  • awkBunun yerine kullanmayı düşünün ve sorunu tamamen önleyin.

1
@Richard: Muhtemelen, aslında bash kullanmıyorsunuzdur. Sizin hashbang sh bash yerine mi yoksa kodunuzu sh ile mi çağırıyorsunuz? Bunu beyanınızdan hemen önce koymayı deneyin: echo "$ BASH_VERSION $ POSIXLY_CORRECT", çıktı almalı 4.xve vermemelidir y.
lhunath

5
Yükseltilemiyor: Bash'de komut dosyaları yazmamın tek nedeni, "her yerde çalıştır" taşınabilirliğidir. Bash'in evrensel olmayan bir özelliğine güvenmek bu yaklaşımı ortadan kaldırır. Bu utanç verici, çünkü aksi takdirde benim için mükemmel bir çözüm olurdu!
Steve Pitchers

3
Birçok insan için "varsayılan" ı temsil ettiği için OSX'in varsayılan olarak Bash 3'ü kullanması bir utanç. ShellShock korkutucusunun ihtiyaç duydukları itme olabileceğini düşündüm ama görünüşe göre değil.
ken

13
@ken bu bir lisanslama sorunu. OSX'teki Bash, GPLv3 olmayan en son lisanslı yapıda sıkışmıştır.
lhunath

2
... veya sudo port install bash(akıllıca, IMHO), işlem başına ayrıcalık yükselmesi olmadan tüm kullanıcılar için PATH dizinlerini dizin yapmak istemeyenlere.
Charles Duffy

125

Parametre ikamesi vardır, ancak PC de olabilir ... dolaylı gibi.

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

BASH 4 yolu elbette daha iyi, ama bir saldırıya ihtiyacınız varsa ... sadece bir saldırı yapacak. Diziyi / karmayı benzer tekniklerle arayabilirsiniz.


5
VALUE=${animal#*:}Davayı korumak için bunu değiştiririmARRAY[$x]="caesar:come:see:conquer"
Glenn Jackman

2
Anahtarlarda veya değerlerde boşluk olması durumunda $ {ARRAY [@]} çevresine çift tırnak koymak da yararlıdırfor animal in "${ARRAY[@]}"; do
devguydavid

1
Ancak verimlilik oldukça zayıf değil mi? Ben doğru hashmaps (sabit zaman arama, O (1) tek bir anahtar için) yerine O (n) yerine başka bir anahtar listesi ile karşılaştırmak istiyorsanız O (n * m) düşünüyorum.
CodeManX

1
Fikir, verimlilikle ilgili daha az, perl, python veya hatta 4'te arka planı olanların anlama / okuma yeteneği hakkında daha fazladır. Benzer bir şekilde yazmanıza izin verir.
Bubnoff

1
@CoDEmanX: Bu, yoksul ruhların Bash 3.x ile 2007'de hala sıkışıp kalmasına yardımcı olmak için bir hack , zeki ve zarif ama yine de temel bir çözüm . Bu kadar basit bir kodda "uygun hashmaps" veya verimlilik faktörleri bekleyemezsiniz.
MestreLion

85

Burada aradığım şey:

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

Bu benim için bash 4.1.5 ile işe yaramadı:

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

2
Değerin boşluk içermeyebileceğini unutmayın, aksi takdirde aynı anda daha fazla öğe
eklersiniz

6
Ben de aksi kabul edilen fantastik cevap eksik buldum hashmap ["key"] = "değer" sözdizimi için oy verin.
thomanski

@ rubo77 anahtarı da, birden fazla anahtar ekler. Bu sorunu çözmenin bir yolu var mı?
Xeverous

25

Hput () / hget () arabirimini, hash'leri aşağıdaki gibi adlandırdığınız şekilde değiştirebilirsiniz:

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

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

ve sonra

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

Bu, çakışmayan diğer haritaları tanımlamanıza olanak tanır (örneğin, başkent tarafından ülke araması yapan 'rcapitals'). Ama her iki durumda da, bunun oldukça korkunç, performans açısından olduğunu göreceksiniz.

Gerçekten hızlı karma arama istiyorsanız, gerçekten iyi çalışan korkunç, korkunç bir kesmek var. Bu: anahtarınızı / değerlerinizi geçici bir dosyaya, her satıra bir tane yazın, sonra bunları kesmek için 'grep "^ $ key"' kullanın, kesme veya awk veya sed ile boruları veya değerleri almak için ne olursa olsun.

Dediğim gibi, kulağa korkunç geliyor ve yavaş olması ve her türlü gereksiz IO yapması gerektiği gibi geliyor, ama pratikte çok hızlı (disk önbelleği harika, değil mi?) tablolar. Anahtar benzersizliğini kendiniz, vb. Zorlamak zorundasınız. Sadece birkaç yüz girişiniz olsa bile, çıktı dosyası / grep combo biraz daha hızlı olacak - deneyimlerime göre birkaç kez daha hızlı. Ayrıca daha az bellek yer.

İşte bunu yapmanın bir yolu:

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
Harika! hatta yineleyebilirsiniz: i için $ (compgen -A değişken capitols); "$ i" "" bitti
zhaorufei

22

Sadece dosya sistemini kullanın

Dosya sistemi, karma harita olarak kullanılabilen bir ağaç yapısıdır. Karma tablonuz geçici bir dizin, anahtarlarınız dosya adları ve değerleriniz dosya içeriği olacaktır. Avantajı, büyük hashetleri işleyebilmesidir ve belirli bir kabuk gerektirmez.

Hashtable oluşturma

hashtable=$(mktemp -d)

Öğe ekle

echo $value > $hashtable/$key

Bir öğeyi okuma

value=$(< $hashtable/$key)

Verim

Tabii ki, yavaş, ama o kadar yavaş değil . Makinemde bir SSD ve btrfs ile test ettim ve saniyede yaklaşık 3000 eleman okuma / yazma yapıyor .


1
Hangi bash sürümü destekleniyor mkdir -d? (4.3 değil, Ubuntu 14'te. Ben mkdir /run/shm/foomkdir /tmp/foo
başvurdum

1
Belki mktemp -dbunun yerine kastedildi?
Reid Ellis

2
Curious $value=$(< $hashtable/$key)ve arasındaki fark value=$(< $hashtable/$key)nedir? Teşekkürler!
Helin Wang

1
"makinemde test etti" Bu, SSD'nizde bir delik açmanın harika bir yolu gibi görünüyor. Tüm Linux dağıtımları varsayılan olarak tmpfs kullanmaz.
kirbyfan64sos

Yaklaşık 50000 karma işliyorum. Perl ve PHP 1/2 saniyenin altında bir saç yapar. 1 saniyede düğüm ve bir şey. FS seçeneği yavaş geliyor. Ancak, dosyaların bir şekilde yalnızca RAM'de bulunduğundan emin olabilir miyiz?
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
Ah, bu gereksiz yere hakaret ediyor gibi görünüyor ve yine de yanlış. Karma tablonun bağırsaklarına giriş doğrulaması, kaçma veya kodlama (bkz. Aslında biliyorum) koymak yerine, bir sargıya ve girişten sonra mümkün olan en kısa sürede.
DigitalRoss

@DigitalRoss, '$ {hash' "$ 1" '# hash}' ekosunda #hash kullanımının ne olduğunu açıklayabilir . benim için bir yorum olarak göründüğünden daha fazla değil. #hash'ın burada özel bir anlamı var mı?
Sanjay

@Sanjay ${var#start}, var değişkeninde saklanan değerin başından itibaren metnin başlangıcını kaldırır .
20'de jpaugh

11

Aşağıdaki ufw güvenlik duvarı komut dosyasındaki kod snippet'inde gösterildiği gibi bash yerleşik oku kullanarak bir çözüm düşünün . Bu yaklaşımın avantajı, istendiği kadar sınırlandırılmış alan kümesi (sadece 2 değil) kullanma avantajına sahiptir. Kullandık | çünkü sınır aralığı belirteçleri iki nokta üst üste işaret gerektirebilir, yani 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 çok güçlü bir özelliktir ve birçok bash programcısı tarafından yeterince kullanılmamaktadır. Lisp benzeri liste işlemenin kompakt formlarına izin verir . Örneğin, yukarıdaki örnekte, yalnızca ilk öğeyi çıkarabilir ve geri kalanını (yani, IFS=$'|' read -r first rest <<< "$fields"
lisp'de

6

@Lhunath ve diğerleri ile ilişkisel dizinin Bash 4 ile gitmenin yolu olduğunu kabul ediyorum. Bash 3'e (OSX, güncelleyemediğiniz eski dağıtımlar) sıkışmışsanız, her yerde olması gereken bir ifade de kullanabilirsiniz. ve düzenli ifadeler. Özellikle sözlük çok büyük olmadığında hoşuma gidiyor.

  1. Anahtarlarda ve değerlerde kullanmayacağınız 2 ayırıcı seçin (örn. ',' Ve ':')
  2. Haritanızı dize olarak yazın (başında ve sonunda da ',' ayırıcısına dikkat edin)

    animals=",moo:cow,woof:dog,"
  3. Değerleri ayıklamak için normal ifade kullanın

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. Öğeleri listelemek için dizeyi bölün

    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
    }

Şimdi kullanabilirsiniz:

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

5

Al P'nin cevabını gerçekten çok beğendim ama benzersizliğin ucuza uygulanmasını istedim, bu yüzden bir adım daha attım - bir dizin kullanın. Bazı belirgin sınırlamalar (dizin dosyası sınırları, geçersiz dosya adları) vardır, ancak çoğu durumda işe yaramalıdır.

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

Ayrıca testlerimde biraz daha iyi performans gösteriyor.

$ 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

Sadece içeri gireceğimi düşündüm. Şerefe!

Düzenle: hdestroy () ekleme


3

İki şey, / dev / shm (Redhat) kullanarak herhangi bir çekirdek 2.6 / tmp yerine bellek kullanabilirsiniz diğer dağıtımlar değişebilir. Ayrıca hget aşağıdaki gibi okunarak yeniden uygulanabilir:

function hget {

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

Buna ek olarak, tüm tuşların benzersiz olduğunu varsayarak, dönüş kısa devre okuma döngüsünü devreder ve tüm girişleri okumak zorunda kalmaz. Uygulamanızda yinelenen anahtarlar varsa, dönüşü dışarıda bırakmanız yeterlidir. Bu, hem grep hem de awk'yi okuma ve çatallama masrafından tasarruf sağlar. Her iki uygulama için / dev / shm kullanılması, son girişi arayan 3 girişlik karede time hget kullanarak aşağıdakileri verdi:

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

Okuma / echo:

$ time echo $(hget FD oracle)
3

real    0m0.004s
user    0m0.000s
sys     0m0.004s

Birden fazla çağrıda% 50'den daha az iyileşme görmedim. Tüm bunlar, kullanımı nedeniyle başın üzerindeki çatala atfedilebilir /dev/shm.


3

Bir iş arkadaşı bu konudan bahsetti. Bağımsız olarak karma tabloları bash içinde uyguladım ve sürüm 4'e bağlı değil . Bash'da Hash tabloları başlıklı Mart 2010'da (burada bazı cevaplardan önce ...) bir blog yayınımdan :

Daha öncecksum karma için kullanılır ama o zamandan beri Java dize hashCode yerel bash / zsh çevirmiş .

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

İki yönlü değildir ve yerleşik yol çok daha iyidir, ancak ikisi de gerçekten kullanılmamalıdır. Bash hızlı bir defalık içindir ve bu tür şeyler, belki de sizin ~/.bashrcve arkadaşlarınız hariç, hash gerektirebilecek karmaşıklığı nadiren içermelidir .


Cevaptaki bağlantı korkutucu! Tıklarsanız, bir yönlendirme döngüsünde kalırsınız. Lütfen güncelle.
Rakib

1
@MohammadRakibAmin - Evet, web sitem kapalı ve blogumu yeniden dirilteceğimden şüpheliyim. Yukarıdaki bağlantıyı arşivlenmiş bir sürüme güncelledim. İlginiz için teşekkürler!
Adam Katz

2

Bash 4'ten önce bash'de ilişkisel diziler kullanmanın iyi bir yolu yoktur. En iyi seçeneğiniz, aslında awk gibi şeyleri destekleyen yorumlanmış bir dil kullanmaktır. Öte yandan, bash 4 yapar onlara destek.

Bash 3'te daha az iyi yollara gelince , işte yardımcı olabileceklerden bir referans: http://mywiki.wooledge.org/BashFAQ/006


2

Bash 3 çözeltisi:

Bazı cevapları okurken, diğerlerine yardımcı olabilecek hızlı ve küçük bir işlevi bir araya getirdim.

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

Bence bu oldukça düzgün bir snippet. Biraz temizleme kullanabilirsiniz (çok değil, ama). Sürümümde, 'anahtar'ı' eşleştirmek 'olarak yeniden adlandırdım ve KEY ve VALUE küçük harf yaptım (çünkü değişkenler dışa aktarılırken büyük harf kullanıyorum). Ben de getHashKey getHashValue için yeniden adlandırılmış ve hem anahtar hem de değer yerel (bazen de olsa yerel olmamasını istiyorsunuz) yaptı. GetHashKeys'de değere hiçbir şey atamıyorum. Değerlerim URL olduğu için ayırma için noktalı virgül kullanıyorum.

0

Ayrıca bash4 yolunu kullandım ama can sıkıcı bir hata buluyorum.

Böylece bu şekilde kullanılan dinamik ilişkilendirilebilir dizi içeriği güncelleştirmek gerekiyordu:

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

Bash 4.3.11 ile dikte mevcut bir anahtara eklenmenin zaten mevcutsa değerin eklenmesiyle sonuçlandığını öğrendim. Örneğin, bir miktar tekrardan sonra değerin içeriği "checkKOcheckKOallCheckOK" idi ve bu iyi değildi.

Bash 4.3.39 ile ilgili bir sorun yok, burada mevcut bir anahtarın işaretlenmesi, mevcutsa aktüal değeri azaltmak için anlamına gelir.

Bu sadece temizleme / cicle önce statusCheck ilişkisel dizi bildirerek çözüldü:

unset statusCheck; declare -A statusCheck

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.