Bashでハッシュテーブルを定義する方法


回答:


938

バッシュ4

Bash 4はこの機能をネイティブでサポートしています。ことを確認してくださいスクリプトのhashbangがある#!/usr/bin/env bashか、#!/bin/bashあなたが使用して終了しないようにsh。あなたが直接あなたのスクリプトを実行していることを確認し、または実行するscriptbash script。(実際にはバッシュとのbashスクリプトを実行していない起こる、とされ、本当に混乱!)

連想配列を宣言するには、次のようにします。

declare -A animals

通常の配列代入演算子を使用して、要素で埋めることができます。たとえば、次のマップが必要な場合animal[sound(key)] = animal(value)

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

またはそれらをマージします:

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

次に、通常の配列と同じように使用します。使用する

  • animals['key']='value' 値を設定する

  • "${animals[@]}" 値を拡張する

  • "${!animals[@]}"(に注意!)キーを展開します

それらを引用することを忘れないでください:

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

バッシュ3

bash 4以前は、連想配列はありません。 それらをエミュレートするために使用しないでくださいevalevalそれシェルスクリプトのペストなので、ペストのようなものは避けてください。最も重要な理由はeval、データを実行可能コードとして扱うことです(他にも多くの理由があります)。

何よりもまず何よりも bash 4へのアップグレードを検討してください。これにより、プロセス全体がはるかに簡単になります。

アップグレードできない理由がある場合declareは、はるかに安全なオプションです。それはのようなbashコードとしてデータを評価しませんevalしないため、任意のコードインジェクションを非常に簡単に行うことができません。

概念を紹介して答えを準備しましょう:

まず、間接。

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

次に、declare

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

それらを一緒にする:

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

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

それを使ってみましょう:

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

注:declare関数に含めることはできません。declarebash関数内で使用すると、ローカルで作成する変数がその関数のスコープに変わります。つまり、bash関数を使用してグローバル配列にアクセスしたり変更したりすることはできません。(bash 4ではdeclare -gを使用してグローバル変数を宣言できますが、bash 4では最初に連想配列を使用して、この回避策を回避できます。)

概要:

  • bash 4にアップグレードしてdeclare -A、連想配列に使用します。
  • declareアップグレードできない場合は、このオプションを使用してください。
  • awk代わりに使用することを検討し、問題を完全に回避してください。

1
@Richard:おそらく、実際にはbashを使用していません。あなたのhashbangはbashの代わりにshですか、それともshでコードを呼び出していますか?これを宣言の直前に置いてみてください:echo "$ BASH_VERSION $ POSIXLY_CORRECT"、それは出力するべきで、出力4.xしないはずyです。
lhunath

5
アップグレードできません。Bashでスクリプトを作成する唯一の理由は、「どこでも実行」できる移植性のためです。したがって、Bashの非ユニバーサル機能に依存すると、このアプローチは除外されます。それは残念です、それ以外の場合は私にとって優れた解決策でした!
スティーブピッチャー

3
OSXがデフォルトでBash 3になっているのは残念です。これは多くの人にとって「デフォルト」を表すためです。ShellShockの恐怖は彼らが必要とするプッシュだったかもしれないと思ったが、どうやらそうではなかった。
ケン

13
@kenそれはライセンスの問題です。OSX上のBashは、最新の非GPLv3ライセンスのビルドで行き詰まっています。
lhunath 2014年

2
...またはsudo port install bash、(賢明なことに、IMHO)すべてのユーザーのPATH内のディレクトリを、明示的なプロセスごとの特権の昇格なしに書き込み可能にしたくない場合。
Charles Duffy、

125

非PCの場合もあるが、間接指定のように、パラメータの置換がある。

#!/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の方が優れていますが、ハックが必要な場合は、ハックだけで十分です。同様の手法で配列/ハッシュを検索できます。


5
私はそれをVALUE=${animal#*:}ケースを保護するために変更しますARRAY[$x]="caesar:come:see:conquer"
グレン・ジャックマン

2
次のように、キーまたは値にスペースがある場合は、$ {ARRAY [@]}を二重引用符で囲むと便利ですfor animal in "${ARRAY[@]}"; do
devguydavid

1
しかし、効率はかなり悪いのではないでしょうか。適切なハッシュマップを使用したO(n)ではなく、別のキーのリストと比較したい場合は、O(n * m)を考えています(一定の時間のルックアップ、単一のキーのO(1))。
CodeManX 2015

1
アイデアは効率についてではなく、perl、python、またはbash 4のバックグラウンドを持つ人のための理解/読みやすさについてです。同様の方法で書くことができます。
Bubnoff 2015

1
@CoDEmanX:これはハックであり、賢くてエレガントですが、貧しい人々が2007年もBash 3.xで立ち往生しているのを手助けするための基本的な回避策です。そのような単純なコードでは、「適切なハッシュマップ」や効率の考慮は期待できません。
MestreLion 2017年

85

これは私がここで探していたものです:

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

これはbash 4.1.5では機能しませんでした。

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

2
値にスペースが含まれていない可能性があることに注意してください。そうでない場合は、一度に複数の要素を追加します
rubo77

6
hashmap ["key"] = "value"構文に賛成票を投じてください。これも、他の素晴らしい受け入れられた回答から欠落していることがわかりました。
thomanski 2016年

@ rubo77キーも複数のキーを追加します。これを回避する方法はありますか?
Xeverous、

25

次のようにハッシュに名前を付けるように、hput()/ hget()インターフェースをさらに変更できます。

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

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

その後

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

これにより、競合しない他のマップを定義できます(たとえば、首都で国を検索する「rcapitals」)。しかし、いずれにしても、パフォーマンスに関しては、これはかなりひどいことに気付くでしょう。

本当に高速なハッシュ検索が必要な場合、実際にうまく機能する恐ろしい、恐ろしいハックがあります。それはこれです:キー/値を一時ファイルに1行ずつ書き込み、次に 'grep "^ $ key"を使用してそれらを取得し、パイプを使用してcutまたはawkまたはsedなどの値を取得します。

私が言ったように、それはひどく聞こえ、遅く、あらゆる種類の不要なIOを実行する必要があるように聞こえますが、実際には非常に高速です(ディスクキャッシュは素晴らしいですよね)。テーブル。自分でキーの一意性を強制する必要があります。数百のエントリしかない場合でも、出力ファイルとgrepの組み合わせはかなり高速になります-私の経験では数倍高速です。また、食べるメモリも少なくなります。

これを行う1つの方法を次に示します。

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
すごい!あなたはそれを反復することさえできます:$(compgen -A variable capitols)でのi; do hget "$ i" "" done
zhaorufei 2010年

22

ファイルシステムを使用する

ファイルシステムは、ハッシュマップとして使用できるツリー構造です。ハッシュテーブルは一時ディレクトリになり、キーはファイル名になり、値はファイルの内容になります。利点は、巨大なハッシュマップを処理でき、特定のシェルを必要としないことです。

ハッシュテーブルの作成

hashtable=$(mktemp -d)

要素を追加する

echo $value > $hashtable/$key

要素を読む

value=$(< $hashtable/$key)

パフォーマンス

もちろん、その遅いではなく、それは遅いです。私は自分のマシンでSSDとbtrfsを使用してテストしました。1秒あたり3000エレメントの読み取り/書き込みを実行します。


1
bashのどのバージョンがサポートされていmkdir -dますか?(Ubuntu 14では4.3ではありません。私はmkdir /run/shm/foo、またはRAMがいっぱいになった場合に使用しますmkdir /tmp/foo。)
Camille Goudeseune

1
おそらくmktemp -d代わりに意味されたのでしょうか?
Reid Ellis

2
$value=$(< $hashtable/$key)とはvalue=$(< $hashtable/$key)どう違いますか?ありがとう!
Helin Wang 2017

1
「私のマシンでテストしました」これは、SSDに穴を開けるのに最適な方法のように思えます。すべてのLinuxディストリビューションがデフォルトでtmpfsを使用するわけではありません。
kirbyfan64sos

約50000のハッシュを処理しています。PerlとPHPはそれを1/2秒未満で行います。1秒程度のノード。FSオプションが遅く聞こえます。しかし、どういうわけか、ファイルが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

31
ため息、それは不必要に侮辱のようであり、それはとにかく不正確です。入力の検証、エスケープ、またはエンコード(実際には知っていますが、参照)をハッシュテーブルの根本に置くのではなく、ラッパーで入力後できるだけ早く入れます。
DigitalRoss

@DigitalRossでは、eval echo '$ {hash' "$ 1" '#hash }'での#hashの使用方法を説明できますか。私にとってはそれ以上のコメントではないようです。#ハッシュはここで特別な意味を持っていますか?
Sanjay

@Sanjay ${var#start}は、変数varに格納されている値の先頭からテキストstartを削除します。
jpaugh

11

次のufwファイアウォールスクリプトからのコードスニペット内に示されているbash組み込み読み取りを使用するソリューションを検討してください。このアプローチには、2つだけではなく、区切られたフィールドセットを必要なだけ使用できるという利点があります。|を使用しました ポート範囲指定子がコロンを必要とする場合があるため、区切り文字、つまり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は非常に強力な機能であり、多くのbashプログラマーによって活用されていません。それは、lispのようなリスト処理のコンパクトな形式を可能にします。例えば、上記の例では私たちは最初の要素を取り除き、残りの部分を保持(すなわちAと同様の考え方をすることができます最初休むことによって、Lispで):IFS=$'|' read -r first rest <<< "$fields"
AsymLabs

6

@lhunathやその他の人には、連想配列がBash 4を使用する方法であることに同意します。Bash3(OSX、更新できない古いディストリビューション)にこだわっている場合は、どこにでもあるはずのexprも使用できます。文字列と正規表現。辞書があまり大きくないときは特に好きです。

  1. キーと値で使用しない2つの区切り文字を選択します(例: '、'、 ':')。
  2. マップを文字列として記述します(区切り文字「、」も最初と最後にあることに注意してください)

    animals=",moo:cow,woof:dog,"
  3. 正規表現を使用して値を抽出する

    get_animal {
        echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
    }
  4. 文字列を分割してアイテムを一覧表示します

    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
    }

今、あなたはそれを使うことができます:

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

5

私はAl Pの答えが本当に好きでしたが、独自性を安価に適用したいと思ったので、さらに一歩進んで-ディレクトリを使用しました。いくつかの明らかな制限(ディレクトリファイルの制限、無効なファイル名)がありますが、ほとんどの場合は機能します。

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

また、私のテストでは少しパフォーマンスが向上しています。

$ 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

ちょうど私がピッチングすると思った。乾杯!

編集:hdestroy()の追加


3

2つのことは、/ dev / shm(Redhat)を使用することにより、カーネル2.6で/ tmpの代わりにメモリを使用できることです。他のディストリビューションは異なる場合があります。また、hgetは次のようにreadを使用して再実装できます。

function hget {

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

さらに、すべてのキーが一意であると想定することで、リターンは読み取りループを短絡させ、すべてのエントリを読み取る必要がなくなります。実装に重複キーがある可能性がある場合は、戻り値を省略してください。これにより、grepとawkの両方の読み取りとフォークの費用が節約されます。両方の実装で/ dev / shmを使用すると、最後のエントリを検索する3エントリのハッシュでtime hgetを使用して次の結果が得られました。

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

読み取り/エコー:

$ time echo $(hget FD oracle)
3

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

複数の呼び出しで、50%未満の改善は見られませんでした。の使用により、これはすべてオーバーヘッドフォークに起因する可能性があります/dev/shm


3

同僚がこのスレッドについて言及しました。題し(...ここでは答えのいくつかの前に)私は独立してbashの内のハッシュテーブルを実装しました、そして、それは2010年3月に私のブログの記事からバージョン4に依存しないbashでハッシュテーブルを

私が以前に使用されcksumたハッシュではなくので、翻訳したJavaの文字列のハッシュコードをネイティブの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)

これは双方向ではなく、組み込みの方法の方がはるかに優れていますが、いずれにしてもどちらも実際に使用するべきではありません。Bashは迅速な1回限りのものであり、そのようなことは、おそらくあなた~/.bashrcや友人を除いて、ハッシュを必要とする可能性がある複雑さを伴うことはほとんどありません。


答えのリンクは怖いです!これをクリックすると、リダイレクトループに陥ります。更新してください。
Rakib

1
@MohammadRakibAmin –ええ、私のウェブサイトはダウンしており、ブログを復活させることはないと思います。上記のリンクをアーカイブバージョンに更新しました。関心をお寄せいただきありがとうございます。
Adam Katz

2

bash 4より前のバージョンでは、連想配列をbashで使用する良い方法はありません。あなたの最善の策は、awkのようなそのようなものを実際にサポートするインタープリター型言語を使用することです。一方、bash 4 それらをサポートしています。

以下のbash 3で良いの通り、ここかもしれないのヘルプより参照は次のとおりです。http://mywiki.wooledge.org/BashFAQ/006


2

Bash 3ソリューション:

いくつかの回答を読むにあたり、他の人を助けるために貢献したいと思います。

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

これはかなりきちんとしたスニペットだと思います。少しクリーンアップを使用できます(ただし、それほど多くはありません)。私のバージョンでは、「キー」の名前を「ペア」に変更し、KEYとVALUEを小文字にしました(変数がエクスポートされるときに大文字を使用するため)。また、getHashKeyの名前をgetHashValueに変更し、キーと値の両方をローカルにしました(ただし、ローカルにしたくない場合もあります)。getHashKeysでは、値に何も割り当てません。値はURLなので、分離にはセミコロンを使用します。

0

私はbash4の方法も使用しましたが、バグを見つけて迷惑です。

連想配列のコンテンツを動的に更新する必要があるため、次のように使用しました。

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では、dictの既存のキーに追加すると、すでに存在する場合は値が追加されることがわかりました。したがって、たとえば、いくつかの繰り返しの後、値の内容は「checkKOcheckKOallCheckOK」であり、これは良くありませんでした。

bash 4.3.39では問題はありません。既存のキーを追加することは、すでに存在する場合、実際の値を補うことを意味します。

cicleの前にstatusCheck連想配列をクリーニング/宣言するだけでこれを解決しました:

unset statusCheck; declare -A statusCheck

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.