Bash에서 해시 테이블을 정의하는 방법은 무엇입니까?


답변:


938

배쉬 4

Bash 4는 기본적으로이 기능을 지원합니다. 확인 스크립트의 hashbang이다 #!/usr/bin/env bash또는 #!/bin/bash당신이 사용하게하지 않도록 sh. 당신이 직접 스크립트를 실행하고 있는지 확인하거나 실행 script으로 bash script. (실제로 배쉬와 배쉬 스크립트가 실행되지 않는 일이 될 것입니다 정말 혼란!)

다음을 수행하여 연관 배열을 선언하십시오.

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가 있다면 훨씬 안전한 옵션입니다. 데이터를 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 4에서는 선언 -g를 사용하여 전역 변수를 선언 할 수 있지만 bash 4에서는 우선이 해결 방법을 피하면서 연관 배열을 사용할 수 있습니다.

요약:

  • bash 4로 업그레이드하고 declare -A연관 배열에 사용하십시오.
  • declare업그레이드 할 수없는 경우이 옵션을 사용하십시오 .
  • awk대신 사용 하고 문제를 피하십시오.

1
@Richard : 아마도 실제로 bash를 사용하지 않는 것 같습니다. bash 대신 hashbang sh입니까, 아니면 sh로 코드를 호출합니까? echo "$ BASH_VERSION $ POSIXLY_CORRECT"선언 바로 앞에 이것을 입력하십시오 . 출력 4.x하지 않아야 y합니다.
lhunath

5
업그레이드 할 수 없음 : Bash에서 스크립트를 작성하는 유일한 이유는 "어디서나 실행"이식성 때문입니다. 따라서 Bash 규칙의 비 범용 기능에 의존 하여이 접근법을 배제합니다. 그렇지 않으면 부끄러운 일입니다. 그렇지 않으면 나에게 훌륭한 솔루션이었을 것입니다!
Steve Pitchers 2014 년

3
이것이 많은 사람들에게 "기본"을 나타 내기 때문에 OSX가 여전히 Bash 3으로 기본 설정되는 것은 부끄러운 일입니다. 나는 ShellShock의 공포가 그들이 필요로 한 추진일지도 모른다고 생각했지만 분명히 그렇지는 않았다.
ken

13
@ken 라이센스 문제입니다. OSX의 Bash는 최신 비 GPLv3 라이센스 빌드에서 멈췄습니다.
lhunath

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 (1))이있는 O (n) 대신 다른 키 목록과 비교하려면 O (n * m) 생각합니다.
CodeManX

1
아이디어는 효율성에 관한 것이 아니라 perl, python 또는 bash 4의 배경을 가진 사람들의 이해 / 읽기 능력에 관한 것입니다. 비슷한 방식으로 글을 쓸 수 있습니다.
Bubnoff

1
@CoDEmanX : 이것은 해킹 이며, 영리하고 우아하지만 여전히 초보적인 해결 방법 으로 2007 년에 Bash 3.x로 가난한 영혼이 여전히 붙어 있도록 도와줍니다. 이러한 간단한 코드에서는 "적절한 해시 맵"또는 효율성 고려 사항을 기대할 수 없습니다.
MestreLion

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
해시 맵 [ "key"] = "value"구문에 대한 찬성도, 그렇지 않으면 환상적인 답변에서 빠진 것을 발견했습니다.
thomanski

@ 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'). 그러나 어느 쪽이든, 나는 이것이 모두 끔찍하고 성능면이라는 것을 알게 될 것입니다.

빠른 해시 조회를 원한다면 실제로 실제로 작동하는 끔찍하고 끔찍한 해킹이 있습니다. 이것은 키 / 값을 임시 파일에 한 줄에 쓴 다음 'grep "^ $ key"를 사용하여 잘라내거나 awk 또는 sed 또는 값을 검색하는 파이프를 사용하여 가져옵니다.

내가 말했듯이, 그것은 끔찍하게 들리며 느리게 들리고 모든 종류의 불필요한 IO를 해야하는 것처럼 들리지만 실제로는 매우 큰 해시에서도 매우 빠릅니다 (디스크 캐시는 훌륭하고 그렇지 않습니까?). 테이블. 키 고유성을 직접 적용해야합니다. 수백 개의 항목 만 있어도 출력 파일 / grep 콤보는 약간 더 빠릅니다. 내 경험상 몇 배 더 빠릅니다. 또한 적은 메모리를 사용합니다.

이를 수행하는 한 가지 방법이 있습니다.

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 변수 capitols)의 i; "$ i" ""
hget

22

파일 시스템 만 사용하십시오

파일 시스템은 해시 맵으로 사용할 수있는 트리 구조입니다. 해시 테이블은 임시 디렉토리가되고 키는 파일 이름이되고 값은 파일 내용이됩니다. 장점은 거대한 해시 맵을 처리 할 수 ​​있으며 특정 쉘이 필요하지 않다는 것입니다.

해시 테이블 생성

hashtable=$(mktemp -d)

요소 추가

echo $value > $hashtable/$key

요소를 읽으십시오

value=$(< $hashtable/$key)

공연

물론 느리지 만 그렇게 느리지는 않습니다 . SSD와 btrfs를 사용하여 내 컴퓨터에서 테스트했으며 초당3000 개의 요소 읽기 / 쓰기를 수행 합니다.


1
bash의 어떤 버전을 지원 mkdir -d합니까? (Ubuntu 14에서는 4.3이 아닙니다. mkdir /run/shm/foo또는 RAM이 가득 찬 경우에 의지합니다 mkdir /tmp/foo.)
Camille Goudeseune

1
아마도 mktemp -d그 대신에 의미가 있었습니까?
리드 엘리스

2
호기심의 차이 무엇 $value=$(< $hashtable/$key)value=$(< $hashtable/$key)? 감사!
Helin Wang

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가 어떻게 사용되는지 설명 할 수 있습니다 . 나를 위해 그것은 그 이상이 아닌 의견으로 보입니다. #hash는 특별한 의미가 있습니까?
Sanjay

@Sanjay ${var#start}는 변수 var에 저장된 값의 시작 부분에서 시작 텍스트를 제거합니다 .
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에서 휴식 )를 IFS=$'|' read -r first rest <<< "$fields"
유지할 수 있습니다

6

나는 @lhunath와 다른 사람들에게 연관 배열이 Bash 4와 함께 갈 수있는 방법이라는 것에 동의합니다. Bash 3 (OSX, 업데이트 할 수없는 오래된 배포판)에 붙어 있다면 expr을 사용할 수 있습니다. 정규식. 사전이 너무 크지 않을 때 특히 마음에 듭니다.

  1. 키와 값에 사용하지 않을 구분 기호를 두 개 선택하십시오 (예 : ','및 ':')
  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.6에서 / 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
}

또한 모든 키가 고유하다고 가정하면 리턴은 읽기 루프를 단락시키고 모든 항목을 읽지 않아도됩니다. 구현에 중복 키가있을 수 있으면 리턴을 생략하십시오. 이것은 grep과 awk를 읽고 포크하는 비용을 절약합니다. 두 구현 모두에 / dev / shm을 사용하면 마지막 항목을 검색하는 3 개의 항목 해시에서 시간 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 내에서 해시 테이블을 독립적으로 구현했으며 버전 4에 의존하지 않습니다. 2010 년 3 월 블로그 게시물에서 (해답 중 일부 전에 ...) bash의 Hash table 이라는 제목이 있습니다 .

나는 이전에 사용 된 cksum해시하지만 이후 번역 한 자바의 문자열의 해시 코드 기본 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는 빠른 일회성 기능을위한 것이며, 이러한 일에는 아마도 여러분 ~/.bashrc과 친구를 제외하고 해시가 필요할 수있는 복잡성이 거의 포함되지 않아야합니다 .


답변의 링크가 무섭습니다! 클릭하면 리디렉션 루프가 중단됩니다. 업데이트하십시오.
Rakib

1
@MohammadRakibAmin – 그래, 내 웹 사이트가 다운되어서 내 블로그를 부활 시킬지 의심 스럽다. 보관 된 버전으로 위의 링크를 업데이트했습니다. 관심을 가져 주셔서 감사합니다!
Adam Katz

2

bash 4 이전에는 bash에서 연관 배열을 사용하는 좋은 방법이 없습니다. 가장 좋은 방법은 실제로 awk와 같은 것들을 지원하는 해석 언어를 사용하는 것입니다. 반면에, bash 4 그것들을 지원합니다.

에 관해서는 bash는 3 좋은 가지 방법, 여기에 힘 도움말보다 기준은 다음과 같습니다 http://mywiki.wooledge.org/BashFAQ/006


2

배쉬 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'의 이름을 'pair'로 바꾸고 KEY와 VALUE를 소문자로 만들었습니다 (변수를 내보낼 때 대문자를 사용하기 때문에). 또한 getHashKey의 이름을 getHashValue로 바꾸고 키와 값을 모두 로컬로 만들었습니다 (때로는 로컬이 아닌 것을 원할 수도 있습니다). getHashKeys에서는 value에 아무것도 할당하지 않습니다. 내 값은 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.