web-dev-qa-db-ja.com

Bashでハッシュテーブルを定義するにはどうすればいいですか?

Pythonの辞書と同等のものは何ですか しかしBashでは(OS XとLinuxの間で動作するはずです).

466

バッシュ4

Bash 4はこの機能をネイティブにサポートしています。スクリプトのハッシュバングが#!/usr/bin/env bashまたは#!/bin/bashであることを確認して、shを使用しないようにしてください。スクリプトを直接実行するか、scriptbash script付きで実行してください。 (実際にはBashで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より前は、連想配列はありません。 それらをエミュレートするためにevalを使わないでください evalをペストのように使用しないでください。これは、というシェルスクリプトのペストです。最も重要な理由はevalがあなたのデータを実行可能コードとして扱うということです(他にもたくさんの理由があります)。

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

アップグレードできない理由がある場合は、declareがはるかに安全なオプションです。 evalのようにbashコードのようにデータを評価するわけではないので、任意のコードを挿入することはそれほど簡単ではありません。

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

まず、間接化です。

$ 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は関数に入れることはできません。 bash関数内でdeclareを使用すると、作成した変数 local がその関数の範囲になります。つまり、グローバル配列にアクセスしたり変更したりすることはできません。 (bash 4では、グローバル変数を宣言するためにdeclare -gを使用できますが、bash 4では、この回避策を回避して、最初から連想配列を使用できます。)

概要:

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

パラメータの置き換えもありますが、間接PCのように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の方法はもちろん良いですが、もしあなたがハックを必要とするなら...ハックだけがするでしょう。同様の手法で配列/ハッシュを検索できます。

103
Bubnoff

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

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" )
61
aktivb

以下のようにハッシュに名前を付けるように、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」)。しかし、どちらにせよ、これはすべて非常にひどい、パフォーマンス的なものであることがわかります。

もしあなたが本当に速いハッシュルックアップを本当に望むなら、実際に本当にうまくいくひどい、ひどいハックがあります。それはこれです:あなたのkey/valuesを1行に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`
23
Al P.

ファイルシステムを使うだけ

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

ハッシュテーブルの作成

hashtable=$(mktemp -d)

要素を追加する

echo $value > $hashtable/$key

要素を読む

value=$(< $hashtable/$key)

パフォーマンス

もちろん、遅いですがthat遅くはありません。私はSSDと btrfs を使って私のマシンでそれをテストしました、そしてそれは毎秒3000要素の読み書きを行います。

15
lovasoa
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
14
DigitalRoss

以下のufwファイアウォールスクリプトのコードスニペット内に示されているように、bash組み込み read を使用した解決策を検討してください。この方法には、必要なだけ(2つだけでなく)区切られたフィールドセットを使用するという利点があります。我々は を使用しましたポート範囲指定子にコロンが必要な場合があるため、 delimiter、つまり 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
10
AsymLabs

連想配列がBash 4で動作する方法であることに@lhunathや他の人たちに同意します。あなたがBash 3(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
6
marco

私は本当に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()の追加

5
Cole Stanfield

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

Bash 3の less 良い方法に関しては、ここに参考になるかもしれません: http://mywiki.wooledge.org/BashFAQ/006

2
kojiro

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[@])
2
Milan Adamovsky

同僚がこのスレッドに言及したばかりです。私は独自にbash内にハッシュテーブルを実装しましたが、バージョン4には依存していません。2010年3月の私のブログ投稿(ここでいくつかの回答の前に...) Hash tables in bash

I 以前 ハッシュにcksumを使用していましたが、それ以来 Javaの文字列hashCode をネイティブ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や友人を除いて、ハッシュを必要とする複雑さを伴うことはめったにありません。

2
Adam Katz

二つのこと、カーネル2.6では/ tmpの代わりに/ dev/shm(Redhat)を使うことで/ tmpの代わりにメモリを使うことができます。以下のように、readを使ってhgetを再実装することもできます。

function hget {

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

さらに、すべてのキーが一意であると仮定することによって、returnは読み込みループを短絡し、すべてのエントリを読み飛ばす必要がなくなります。あなたの実装が重複したキーを持つことができるならば、単にリターンを省いてください。これはgrepとawkの両方を読んだりフォークしたりする費用を節約します。両方の実装で/ dev/shmを使用すると、最後のエントリを検索する3エントリのハッシュに対して以下のusing 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を使用しているため、これはすべてfork over headに起因する可能性があります。

2
jrichard

私はまた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で辞書の既存のキーに追加すると、すでに存在する場合は値を追加することになりました。したがって、たとえば、繰り返した後の値の内容は "checkKOcheckKOallCheckOK"であり、これは良くありませんでした。

Bash 4.3.39では、既存のキーを追加することでアクチュアリー値がすでに存在する場合はそれを代替することを意味しますが、問題ありません。

これを解決したのは、単にcicleの前でstatusCheck連想配列を消去/宣言するだけです。

unset statusCheck; declare -A statusCheck
0
Alex

動的変数を使用してbash 3でHashMapsを作成します。私はそれが私の答えの中でどのように働くかを説明しました: シェルスクリプトの中の連想配列

また、Shell_mapを見ることもできます。これは、bash 3で作成されたHashMap実装です。

0