Bagaimana cara mendefinisikan tabel hash di Bash?

557

Apa yang setara dengan kamus Python tetapi di Bash (harus bekerja di OS X dan Linux).

Sridhar Ratnakumar
sumber
4
Apakah bash menjalankan skrip python / perl ... Itu sangat fleksibel!
e2-e4
Pertimbangkan untuk menggunakan xonsh (ada di github).
Oliver

Jawaban:

939

Bash 4

Bash 4 secara native mendukung fitur ini. Pastikan hashbang skrip Anda #!/usr/bin/env bashatau #!/bin/bashAnda tidak menggunakannya sh. Pastikan Anda mengeksekusi skrip Anda secara langsung, atau mengeksekusi scriptdengan bash script. (Sebenarnya tidak menjalankan skrip Bash dengan Bash memang terjadi, dan akan sangat membingungkan!)

Anda mendeklarasikan array asosiatif dengan melakukan:

declare -A animals

Anda dapat mengisinya dengan elemen menggunakan operator penetapan array normal. Misalnya, jika Anda ingin memiliki peta animal[sound(key)] = animal(value):

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

Atau gabungkan mereka:

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

Kemudian gunakan mereka seperti array normal. Menggunakan

  • animals['key']='value' untuk menetapkan nilai

  • "${animals[@]}" untuk memperluas nilai

  • "${!animals[@]}" (perhatikan ! ) untuk membuka kunci

Jangan lupa mengutipnya:

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

Bash 3

Sebelum bash 4, Anda tidak memiliki array asosiatif. Jangan gunakan evaluntuk meniru mereka . Hindari evalseperti wabah, karena merupakan wabah shell scripting. Alasan terpenting adalah itueval memperlakukan data Anda sebagai kode yang dapat dieksekusi (ada banyak alasan lain juga).

Pertama dan terutama : Pertimbangkan untuk meningkatkan ke bash 4. Ini akan membuat seluruh proses lebih mudah bagi Anda.

Jika ada alasan Anda tidak bisa memutakhirkan, itu declareadalah opsi yang jauh lebih aman. Itu tidak mengevaluasi data seperti kode basheval tidak, dan dengan demikian tidak memungkinkan injeksi kode arbitrer dengan mudah.

Mari kita siapkan jawabannya dengan memperkenalkan konsep:

Pertama, tipuan.

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

Kedua, declare:

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

Satukan mereka:

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

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

Mari kita gunakan:

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

Catatan: declaretidak bisa dimasukkan ke dalam fungsi. Setiap penggunaan declarefungsi bash dalam mengubah variabel yang dibuatnya lokal untuk lingkup fungsi itu, berarti kita tidak bisa akses atau memodifikasi array global dengan itu. (Dalam bash 4 Anda dapat menggunakan menyatakan -g untuk mendeklarasikan variabel global - tetapi dalam bash 4, Anda dapat menggunakan array asosiatif di tempat pertama, menghindari solusi ini.)

Ringkasan:

  • Tingkatkan ke bash 4 dan gunakan declare -A untuk array asosiatif.
  • Menggunakan declare opsi jika Anda tidak dapat memutakhirkan.
  • Pertimbangkan untuk menggunakan awkdan hindari masalah ini sama sekali.
lununath
sumber
1
@ Richard: Mungkin, Anda sebenarnya tidak menggunakan bash. Apakah hashbang sh Anda bukan bash, atau apakah Anda sebaliknya menggunakan kode Anda dengan sh? Coba letakkan ini tepat sebelum deklarasi Anda: echo "$ BASH_VERSION $ POSIXLY_CORRECT", seharusnya hasilnya 4.xdan bukan y.
lhunath
5
Tidak dapat memutakhirkan: satu-satunya alasan saya menulis skrip di Bash adalah untuk portabilitas "jalankan di mana saja". Jadi mengandalkan fitur non-universal Bash mengesampingkan pendekatan ini. Yang memalukan, karena kalau tidak itu akan menjadi solusi yang sangat baik bagi saya!
Steve Pitchers
3
Sayang sekali bahwa OSX default untuk Bash 3 masih karena ini merupakan "default" untuk banyak orang. Saya pikir ketakutan ShellShock mungkin merupakan dorongan yang mereka butuhkan tetapi ternyata tidak.
ken
13
@ken itu masalah lisensi. Bash pada OSX macet di build non-GPLv3 terbaru.
lhunath
2
... atau sudo port install bash, bagi mereka (secara bijak, IMHO) yang tidak ingin membuat direktori di PATH untuk semua pengguna yang dapat ditulis tanpa eskalasi hak istimewa per-proses yang eksplisit.
Charles Duffy
125

Ada substitusi parameter, meskipun mungkin un-PC juga ... seperti tipuan.

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

Cara BASH 4 tentu saja lebih baik, tetapi jika Anda membutuhkan peretasan ... hanya peretasan yang akan dilakukan. Anda dapat mencari array / hash dengan teknik serupa.

Bubnoff
sumber
5
Saya akan mengubahnya VALUE=${animal#*:}untuk melindungi kasus di manaARRAY[$x]="caesar:come:see:conquer"
glenn jackman
2
Ini juga berguna untuk menempatkan tanda kutip ganda di sekitar $ {ARRAY [@]} jika ada spasi di kunci atau nilai, seperti difor animal in "${ARRAY[@]}"; do
devguydavid
1
Tetapi bukankah efisiensinya cukup buruk? Saya berpikir O (n * m) jika Anda ingin membandingkan dengan daftar kunci lain, bukan O (n) dengan hashmaps yang tepat (pencarian waktu konstan, O (1) untuk satu kunci).
CodeManX
1
Idenya kurang tentang efisiensi, lebih banyak tentang memahami / membaca-kemampuan bagi mereka dengan latar belakang di perl, python atau bahkan bash 4. Memungkinkan Anda untuk menulis dengan cara yang sama.
Bubnoff
1
@CoDEmanX: ini adalah hack , pintar dan elegan namun masih belum sempurna solusi untuk membantu jiwa-jiwa miskin masih tertahan di 2007 dengan Bash 3.x. Anda tidak dapat mengharapkan "hashmaps yang tepat" atau pertimbangan efisiensi dalam kode sederhana seperti itu.
MestreLion
85

Inilah yang saya cari di sini:

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

Ini tidak berfungsi untuk saya dengan bash 4.1.5:

animals=( ["moo"]="cow" )
aktivb
sumber
2
Perhatikan, bahwa nilainya mungkin tidak mengandung spasi, jika tidak Anda menambahkan lebih banyak elemen sekaligus
rubo77
6
Upvote untuk sintaks hashmap ["key"] = "value" yang saya juga temukan hilang dari jawaban diterima yang sebaliknya fantastis.
thomanski
@ rubo77 juga, itu menambahkan beberapa kunci. Adakah cara untuk mengatasinya?
Xeverous
25

Anda selanjutnya dapat memodifikasi antarmuka hput () / hget () sehingga Anda telah menamai hash sebagai berikut:

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

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

lalu

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

Ini memungkinkan Anda menentukan peta lain yang tidak bertentangan (misalnya, 'rcapitals' yang melakukan pencarian negara oleh ibu kota). Tapi, bagaimanapun juga, saya pikir Anda akan menemukan bahwa ini semua cukup mengerikan, dari segi kinerja.

Jika Anda benar-benar ingin pencarian hash cepat, ada peretasan yang mengerikan, yang sebenarnya bekerja sangat baik. Ini adalah ini: tulis kunci / nilai Anda ke file sementara, satu per baris, kemudian gunakan 'grep "^ $ key"' untuk mengeluarkannya, menggunakan pipa dengan cut atau awk atau sed atau apa pun untuk mengambil nilai.

Seperti yang saya katakan, kedengarannya mengerikan, dan kedengarannya seperti itu harus lambat dan melakukan semua jenis IO yang tidak perlu, tetapi dalam praktiknya sangat cepat (cache disk mengagumkan, bukan?), Bahkan untuk hash yang sangat besar meja. Anda harus memaksakan keunikan kunci sendiri, dll. Bahkan jika Anda hanya memiliki beberapa ratus entri, file output / grep combo akan menjadi sedikit lebih cepat - dalam pengalaman saya beberapa kali lebih cepat. Ini juga memakan lebih sedikit memori.

Inilah satu cara untuk melakukannya:

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.
sumber
1
Bagus! Anda bahkan dapat mengulanginya: untuk i dalam $ (compgen -A variabel capital); jangan "$ i" "" selesai
zhaorufei
22

Cukup gunakan sistem file

Sistem file adalah struktur pohon yang dapat digunakan sebagai peta hash. Tabel hash Anda akan menjadi direktori sementara, kunci Anda akan menjadi nama file, dan nilai Anda akan menjadi isi file. Keuntungannya adalah ia dapat menangani hashmaps besar, dan tidak memerlukan shell spesifik.

Penciptaan Hashtable

hashtable=$(mktemp -d)

Tambahkan elemen

echo $value > $hashtable/$key

Baca elemen

value=$(< $hashtable/$key)

Performa

Tentu saja, ini lambat, tetapi tidak terlalu lambat. Saya mengujinya di mesin saya, dengan SSD dan btrfs , dan itu sekitar 3000 elemen baca / tulis per detik .

lovasoa
sumber
1
Versi bash mana yang didukung mkdir -d? (Tidak 4.3, di Ubuntu 14. Saya akan menggunakan mkdir /run/shm/foo, atau jika itu mengisi RAM mkdir /tmp/foo,.)
Camille Goudeseune
1
Mungkin mktemp -ditu yang dimaksudkan?
Reid Ellis
2
Penasaran apa perbedaan antara $value=$(< $hashtable/$key)dan value=$(< $hashtable/$key)? Terima kasih!
Helin Wang
1
"Mengujinya di mesin saya" Ini terdengar seperti cara yang bagus untuk membakar lubang melalui SSD Anda. Tidak semua distro Linux menggunakan tmpfs secara default.
kirbyfan64sos
Saya sedang memproses sekitar 50000 hash. Perl dan PHP melakukannya di bawah 1/2 detik. Simpul dalam 1 detik dan sesuatu. Opsi FS terdengar lambat. Namun, dapatkah kita memastikan bahwa file-file itu hanya ada di RAM, entah bagaimana?
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
sumber
31
Sigh, itu sepertinya tidak perlu menghina dan itu tidak akurat pula. Seseorang tidak akan memasukkan validasi input, melarikan diri, atau pengkodean (lihat, saya benar-benar tahu) di nyali tabel hash, melainkan dalam pembungkus dan sesegera mungkin setelah input.
DigitalRoss
@DigitalRoss dapatkah Anda menjelaskan apa gunanya #hash in eval echo '$ {hash' "$ 1" '# hash}' . bagi saya sepertinya saya sebagai komentar tidak lebih dari itu. apakah #hash memiliki arti khusus di sini?
Sanjay
@Sanjay ${var#start}menghapus teks mulai dari awal nilai yang disimpan dalam variabel var .
jpaugh
11

Pertimbangkan solusi menggunakan bash builtin read seperti yang diilustrasikan dalam cuplikan kode dari skrip firewall ufw yang mengikuti. Pendekatan ini memiliki keuntungan menggunakan sebanyak set bidang terbatas (tidak hanya 2) seperti yang diinginkan. Kami telah menggunakan | pembatas karena penentu rentang port mungkin memerlukan titik dua, yaitu 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
sumber
2
@CharlieMartin: baca adalah fitur yang sangat kuat dan kurang dimanfaatkan oleh banyak programmer bash. Ini memungkinkan bentuk-bentuk kompak pemrosesan daftar mirip-lisp . Sebagai contoh, dalam contoh di atas kita dapat menghapus hanya elemen pertama dan mempertahankan sisanya (yaitu konsep yang mirip dengan pertama dan sisanya di lisp) dengan melakukan:IFS=$'|' read -r first rest <<< "$fields"
AsymLabs
6

Saya setuju dengan @lhunath dan lainnya bahwa array asosiatif adalah cara untuk menggunakan Bash 4. Jika Anda terjebak pada Bash 3 (OSX, distro lama yang tidak dapat Anda perbarui) Anda dapat menggunakan expr, yang seharusnya ada di mana-mana, sebuah string dan ekspresi reguler. Saya suka terutama ketika kamusnya tidak terlalu besar.

  1. Pilih 2 pemisah yang tidak akan Anda gunakan dalam kunci dan nilai (mis. ',' Dan ':')
  2. Tulis peta Anda sebagai string (perhatikan pemisah ',' juga di awal dan akhir)

    animals=",moo:cow,woof:dog,"
  3. Gunakan regex untuk mengekstrak nilai

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. Pisahkan string untuk membuat daftar item

    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
    }

Sekarang Anda dapat menggunakannya:

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

Saya sangat menyukai jawaban Al P tetapi ingin keunikan ditegakkan dengan murah jadi saya mengambil satu langkah lebih jauh - menggunakan direktori. Ada beberapa batasan yang jelas (batas file direktori, nama file tidak valid) tetapi harus berfungsi untuk sebagian besar kasus.

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

Ini juga melakukan sedikit lebih baik dalam pengujian saya.

$ 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

Kupikir aku akan ikut. Bersulang!

Edit: Menambahkan hdestroy ()

Cole Stanfield
sumber
3

Dua hal, Anda dapat menggunakan memori alih-alih / tmp di kernel 2.6 dengan menggunakan / dev / shm (Redhat) distro lain mungkin berbeda. Hget juga dapat diimplementasikan menggunakan baca sebagai berikut:

function hget {

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

Selain itu dengan mengasumsikan bahwa semua tombol unik, kembalikan sirkuit pendek loop baca dan mencegah harus membaca semua entri. Jika implementasi Anda dapat memiliki kunci duplikat, cukup tinggalkan kembalinya. Ini menghemat biaya membaca dan forking baik grep dan awk. Menggunakan / dev / shm untuk kedua implementasi menghasilkan berikut menggunakan waktu hget pada hash entri 3 mencari entri terakhir:

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

Baca / gema:

$ time echo $(hget FD oracle)
3

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

pada banyak pemanggilan, saya tidak pernah melihat peningkatan yang kurang dari 50%. Ini semua dapat dikaitkan dengan fork over head, karena penggunaan /dev/shm.

jrichard
sumber
3

Seorang rekan kerja baru saja menyebutkan utas ini. Saya sudah menerapkan tabel hash secara mandiri dalam bash, dan itu tidak tergantung pada versi 4. Dari posting blog saya pada Maret 2010 (sebelum beberapa jawaban di sini ...) berjudul Tabel hash di bash :

Saya sebelumnya pernah menggunakan cksumhash tetapi sejak itu menerjemahkan hashCode string Java ke bash / zsh asli.

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

Ini bukan dua arah, dan cara bawaannya jauh lebih baik, tetapi tidak pula seharusnya digunakan. Bash hanya untuk sekali saja, dan hal-hal seperti itu seharusnya jarang melibatkan kompleksitas yang mungkin membutuhkan hash, kecuali mungkin pada Anda ~/.bashrcdan teman-teman.

Adam Katz
sumber
Tautan dalam jawabannya menakutkan! Jika Anda mengkliknya, Anda terjebak dalam lingkaran pengalihan. Mohon perbarui.
Rakib
1
@MohammadRakibAmin - Ya, situs web saya sedang down dan saya ragu saya akan menghidupkan kembali blog saya. Saya telah memperbarui tautan di atas ke versi yang diarsipkan. Terima kasih atas minat Anda!
Adam Katz
2

Sebelum bash 4 tidak ada cara yang baik untuk menggunakan array asosiatif di bash. Taruhan terbaik Anda adalah menggunakan bahasa yang ditafsirkan yang sebenarnya memiliki dukungan untuk hal-hal seperti itu, seperti awk. Di sisi lain, bash 4 melakukannya mendukung mereka.

Adapun cara-cara yang kurang baik di bash 3, berikut ini adalah referensi yang mungkin bisa membantu: http://mywiki.wooledge.org/BashFAQ/006

kojiro
sumber
2

Solusi Bash 3:

Dalam membaca beberapa jawaban saya mengumpulkan fungsi kecil cepat saya ingin berkontribusi kembali yang dapat membantu orang lain.

# 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
sumber
Saya pikir ini potongan yang cukup rapi. Itu bisa menggunakan sedikit pembersihan (meskipun tidak banyak). Dalam versi saya, saya telah mengganti nama 'kunci' menjadi 'pasangan' dan membuat huruf kecil KUNCI dan VALUE (karena saya menggunakan huruf besar ketika variabel diekspor). Saya juga mengganti nama getHashKey menjadi getHashValue dan membuat kunci dan nilai lokal (kadang-kadang Anda ingin mereka tidak menjadi lokal). Di getHashKeys, saya tidak menetapkan nilai apa pun. Saya menggunakan titik koma untuk pemisahan, karena nilai saya adalah URL.
0

Saya juga menggunakan cara bash4 tapi saya menemukan bug yang mengganggu.

Saya perlu memperbarui konten array asosiatif secara dinamis sehingga saya menggunakan cara ini:

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

Saya mengetahui bahwa dengan bash 4.3.11 menambahkan ke kunci yang ada di dict menghasilkan menambahkan nilai jika sudah ada. Jadi misalnya setelah beberapa kali pengulangan konten nilainya adalah "checkKOcheckKOallCheckOK" dan ini tidak baik.

Tidak ada masalah dengan bash 4.3.39 di mana appenging kunci yang ada berarti mengganti nilai aktual jika sudah ada.

Saya memecahkan ini hanya membersihkan / menyatakan array asosiatif statusCheck sebelum cicle:

unset statusCheck; declare -A statusCheck
Alex
sumber
-1

Saya membuat HashMaps di bash 3 menggunakan variabel dinamis. Saya menjelaskan cara kerjanya dalam jawaban saya untuk: Array asosiatif dalam skrip Shell

Anda juga dapat melihat di shell_map , yang merupakan implementasi HashMap dibuat di bash 3.

Bruno Negrão Zica
sumber