program tip

쉘 스크립트의 연관 배열

radiobox 2020. 7. 26. 12:49
반응형

쉘 스크립트의 연관 배열


우리는 셸 스크립팅을 위해 연관 배열 또는 Map like 데이터 구조를 시뮬레이션하는 스크립트가 필요합니까?


Irfan의 답변에 추가하려면 다음은 get()맵 내용을 반복 할 필요가 없기 때문에 짧고 빠른 버전입니다 .

get() {
    mapName=$1; key=$2

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}

이식성이 주요 관심사가 아닌 경우 다른 옵션은 쉘에 내장 된 연관 배열을 사용하는 것입니다. 이것은 bash 4.0 (지금은 대부분의 주요 배포판에서 사용 가능하지만 직접 설치하지 않으면 OS X에서는 불가능), ksh 및 zsh에서 작동합니다.

declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"

echo ${newmap[company]}
echo ${newmap[name]}

쉘에 따라 typeset -A newmap대신에 작업을 수행해야 할 수도 declare -A newmap있고 전혀 필요하지 않을 수도 있습니다.


비 bash 4 방법.

#!/bin/bash

# A pretend Python dictionary with bash 3 
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

echo -e "${ARRAY[1]%%:*} is an extinct animal which likes to ${ARRAY[1]#*:}\n"

거기에서 검색하기 위해 if 문을 던질 수 있습니다. [[$ var = ~ / blah /]] 인 경우 또는 무엇이든.


나는 당신이 뒤로 물러서서 맵이나 연관 배열이 실제로 무엇인지 생각해야한다고 생각합니다. 모든 것은 주어진 키에 대한 값을 저장하고 그 값을 빠르고 효율적으로 되 돌리는 방법입니다. 키를 반복하여 모든 키 값 쌍을 검색하거나 키 및 관련 값을 삭제할 수도 있습니다.

이제는 항상 셸 스크립팅 및 스크립트를 작성하지 않은 셸에서 이러한 속성을 가진 데이터 구조를 사용하십시오. 그만? 파일 시스템입니다.

실제로, 쉘 프로그래밍에서 연관 배열을 갖기 위해 필요한 것은 임시 디렉토리입니다. mktemp -d연관 배열 생성자입니다.

prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)

echoand를 사용 하고 싶지 않다면 언제든지 cat작은 래퍼를 작성할 수 있습니다. 이러한 것들은 다음과 같은 임의의 변수를 설정하는 대신 값을 출력하지만 Irfan에서 모델링되었습니다 $value.

#!/bin/sh

prefix=$(basename -- "$0")
mapdir=$(mktemp -dt ${prefix})
trap 'rm -r ${mapdir}' EXIT

put() {
  [ "$#" != 3 ] && exit 1
  mapname=$1; key=$2; value=$3
  [ -d "${mapdir}/${mapname}" ] || mkdir "${mapdir}/${mapname}"
  echo $value >"${mapdir}/${mapname}/${key}"
}

get() {
  [ "$#" != 2 ] && exit 1
  mapname=$1; key=$2
  cat "${mapdir}/${mapname}/${key}"
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

value=$(get "newMap" "company")
echo $value

value=$(get "newMap" "name")
echo $value

편집 :이 접근법은 실제로 질문자가 제안한 sed를 사용하는 선형 검색보다 훨씬 빠르며 더 강력합니다 (키와 값에-, =, space, qnd ": SP :"를 포함 할 수 있음). 파일 시스템을 사용한다고해서 속도가 느려지지는 않습니다. 이 파일들은 실제로 전화하지 않는 한 디스크에 기록되는 것을 보장하지 않습니다 sync. 수명이 짧은 이와 같은 임시 파일의 경우 많은 파일이 디스크에 쓰여지지 않을 가능성은 거의 없습니다.

다음 드라이버 프로그램을 사용하여 Irfan의 코드, Jerry의 Irfan 코드의 수정 및 코드의 벤치 마크를 수행했습니다.

#!/bin/sh

mapimpl=$1
numkeys=$2
numvals=$3

. ./${mapimpl}.sh    #/ <- fix broken stack overflow syntax highlighting

for (( i = 0 ; $i < $numkeys ; i += 1 ))
do
    for (( j = 0 ; $j < $numvals ; j += 1 ))
    do
        put "newMap" "key$i" "value$j"
        get "newMap" "key$i"
    done
done

결과 :

    $ 시간 ./driver.sh irfan 10 5

    실제 0m0.975s
    사용자 0m0.280s
    시스 0m0.691s

    $ 시간 ./driver.sh brian 10 5

    실제 0m0.226s
    사용자 0m0.057s
    시스 0m0.123s

    $ 시간 ./driver.sh jerry 10 5

    실제 0m0.706s
    사용자 0m0.228s
    시스 0m0.530s

    $ 시간 ./driver.sh irfan 100 5

    실제 0m10.633s
    사용자 0m4.366s
    시스 0m7.127s

    $ 시간 ./driver.sh brian 100 5

    실제 0m1.682s
    사용자 0m0.546s
    시스 0m1.082s

    $ 시간 ./driver.sh jerry 100 5

    진짜 0m9.315s
    사용자 0m4.565s
    시스 0m5.446s

    $ 시간 ./driver.sh irfan 10 500

    실제 1m46.197s
    사용자 0m44.869s
    시스 1m12.282s

    $ 시간 ./driver.sh brian 10 500

    실제 0m16.003s
    사용자 0m5.135s
    시스 0m10.396s

    $ 시간 ./driver.sh jerry 10 500

    진짜 1m24.414s
    사용자 0m39.696s
    시스 0m54.834s

    $ 시간 ./driver.sh irfan 1000 5

    실제 4m
    사용자 3 분 17
    시스 1m21.490s

    $ 시간 ./driver.sh brian 1000 5

    실제 0m19.442s
    사용자 0m5.287s
    시스 0m10.751s

    $ 시간 ./driver.sh jerry 1000 5

    진짜 5m29.136s
    사용자 4m48.926s
    시스 0m59.336s


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

Bash4는이를 기본적으로 지원합니다. 사용하지 마십시오 grep또는 eval, 그들은 해킹의 추악한입니다.

예제 코드가 포함 된 자세한 상세 답변은 https://stackoverflow.com/questions/3467959를 참조하십시오.


####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get
{
    alias "${1}$2" | awk -F"'" '{ print $2; }'
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

예:

mapName=$(basename $0)_map_
map_put $mapName "name" "Irfan Zulfiqar"
map_put $mapName "designation" "SSE"

for key in $(map_keys $mapName)
do
    echo "$key = $(map_get $mapName $key)
done

이제이 질문에 대답하십시오.

다음 스크립트는 쉘 스크립트에서 연관 배열을 시뮬레이션합니다. 간단하고 이해하기 쉽습니다.

map은 keyValuePair가 --name = Irfan --designation = SSE --company = My : SP : Own : SP : Company로 저장된 끝없는 문자열 일뿐입니다.

값은 공백이 ': SP :'로 바뀝니다.

put() {
    if [ "$#" != 3 ]; then exit 1; fi
    mapName=$1; key=$2; value=`echo $3 | sed -e "s/ /:SP:/g"`
    eval map="\"\$$mapName\""
    map="`echo "$map" | sed -e "s/--$key=[^ ]*//g"` --$key=$value"
    eval $mapName="\"$map\""
}

get() {
    mapName=$1; key=$2; valueFound="false"

    eval map=\$$mapName

    for keyValuePair in ${map};
    do
        case "$keyValuePair" in
            --$key=*) value=`echo "$keyValuePair" | sed -e 's/^[^=]*=//'`
                      valueFound="true"
        esac
        if [ "$valueFound" == "true" ]; then break; fi
    done
    value=`echo $value | sed -e "s/:SP:/ /g"`
}

put "newMap" "name" "Irfan Zulfiqar"
put "newMap" "designation" "SSE"
put "newMap" "company" "My Own Company"

get "newMap" "company"
echo $value

get "newMap" "name"
echo $value

편집 : 모든 키를 가져 오는 다른 방법을 추가했습니다.

getKeySet() {
    if [ "$#" != 1 ]; 
    then 
        exit 1; 
    fi

    mapName=$1; 

    eval map="\"\$$mapName\""

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
          `
}

Bash 3에는 훌륭하고 간단한 해결책이있는 특별한 경우가 있습니다.

당신은 많은 변수를 처리하지 않으려는 경우, 또는 키를 단순히 잘못된 변수 식별자, 당신의 배열이 보장 256 개 항목을 당신이 함수의 반환 값을 악용 할 수 있습니다. 이 솔루션은 값을 변수로 쉽게 사용할 수 있거나 성능이 비명을 지르는 반복으로 하위 쉘이 필요하지 않습니다. 또한 Bash 4 버전과 거의 비슷하게 읽을 수 있습니다.

가장 기본적인 버전은 다음과 같습니다.

hash_index() {
    case $1 in
        'foo') return 0;;
        'bar') return 1;;
        'baz') return 2;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo"
echo ${hash_vals[$?]}

에 작은 따옴표를 사용하십시오. case그렇지 않으면 글 로빙 될 수 있습니다. 처음부터 정적 / 고정 해시에 유용하지만 hash_keys=()배열 에서 색인 생성기를 작성할 수 있습니다.

조심하십시오, 기본값은 첫 번째 요소이므로 0 번째 요소를 따로 설정할 수 있습니다.

hash_index() {
    case $1 in
        'foo') return 1;;
        'bar') return 2;;
        'baz') return 3;;
    esac
}

hash_vals=("",           # sort of like returning null/nil for a non existent key
           "foo_val"
           "bar_val"
           "baz_val");

hash_index "foo" || echo ${hash_vals[$?]}  # It can't get more readable than this

경고 : 길이가 잘못되었습니다.

또는 0부터 시작하여 인덱싱을 유지하려는 경우 다른 인덱스 값을 예약하고 존재하지 않는 키로부터 보호 할 수 있지만 읽기는 쉽지 않습니다.

hash_index() {
    case $1 in
        'foo') return 0;;
        'bar') return 1;;
        'baz') return 2;;
        *)   return 255;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo"
[[ $? -ne 255 ]] && echo ${hash_vals[$?]}

또는 길이를 올바르게 유지하려면 인덱스를 1 씩 오프셋하십시오.

hash_index() {
    case $1 in
        'foo') return 1;;
        'bar') return 2;;
        'baz') return 3;;
    esac
}

hash_vals=("foo_val"
           "bar_val"
           "baz_val");

hash_index "foo" || echo ${hash_vals[$(($? - 1))]}

동적 변수 이름을 사용하고 변수 이름이 해시 맵의 키처럼 작동하게 할 수 있습니다.

예를 들어, 다음과 같이 이름, credit이라는 두 개의 열이있는 입력 파일이 있고 각 사용자의 수입을 합산하려는 경우 :

Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100

다음 명령은 동적 변수를 키로 사용하여 map _ $ {person} 형식으로 모든 것을 합산합니다 .

while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)

결과를 읽으려면 :

set | grep map

출력은 다음과 같습니다.

map_David=100
map_John=500
map_Mary=150
map_Paul=500

이러한 기술을 자세히 설명하기 위해 GitHub에서 HashMap Object , shell_map 과 같은 기능을 개발 입니다.

" HashMap 인스턴스 " 를 작성하기 위해 shell_map 함수 는 다른 이름으로 자체 사본을 작성할 수 있습니다. 각각의 새로운 함수 사본에는 다른 $ FUNCNAME 변수가 있습니다. 그런 다음 $ FUNCNAME을 사용하여 각 Map 인스턴스의 네임 스페이스를 만듭니다.

맵 키는 $ FUNCNAME_DATA_ $ KEY 형식의 전역 변수입니다. 여기서 $ KEY는 맵에 추가 된 키입니다. 이러한 변수는 동적 변수 입니다.

아래 예제로 사용할 수 있도록 간단한 버전을 소개하겠습니다.

#!/bin/bash

shell_map () {
    local METHOD="$1"

    case $METHOD in
    new)
        local NEW_MAP="$2"

        # loads shell_map function declaration
        test -n "$(declare -f shell_map)" || return

        # declares in the Global Scope a copy of shell_map, under a new name.
        eval "${_/shell_map/$2}"
    ;;
    put)
        local KEY="$2"  
        local VALUE="$3"

        # declares a variable in the global scope
        eval ${FUNCNAME}_DATA_${KEY}='$VALUE'
    ;;
    get)
        local KEY="$2"
        local VALUE="${FUNCNAME}_DATA_${KEY}"
        echo "${!VALUE}"
    ;;
    keys)
        declare | grep -Po "(?<=${FUNCNAME}_DATA_)\w+((?=\=))"
    ;;
    name)
        echo $FUNCNAME
    ;;
    contains_key)
        local KEY="$2"
        compgen -v ${FUNCNAME}_DATA_${KEY} > /dev/null && return 0 || return 1
    ;;
    clear_all)
        while read var; do  
            unset $var
        done < <(compgen -v ${FUNCNAME}_DATA_)
    ;;
    remove)
        local KEY="$2"
        unset ${FUNCNAME}_DATA_${KEY}
    ;;
    size)
        compgen -v ${FUNCNAME}_DATA_${KEY} | wc -l
    ;;
    *)
        echo "unsupported operation '$1'."
        return 1
    ;;
    esac
}

용법:

shell_map new credit
credit put Mary 100
credit put John 200
for customer in `credit keys`; do 
    value=`credit get $customer`       
    echo "customer $customer has $value"
done
credit contains_key "Mary" && echo "Mary has credit!"

나는 이전에 질문을 보지 못한 것이 얼마나 유감 스러웠습니까? 지도 (Associative arrays)를 포함하는 라이브러리 쉘 프레임 워크 를 작성했습니다. 그것의 마지막 버전은 여기 에서 찾을 수 있습니다 .

예:

#!/bin/bash 
#include map library
shF_PATH_TO_LIB="/usr/lib/shell-framework"
source "${shF_PATH_TO_LIB}/map"

#simple example get/put
putMapValue "mapName" "mapKey1" "map Value 2"
echo "mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#redefine old value to new
putMapValue "mapName" "mapKey1" "map Value 1"
echo "after change mapName[mapKey1]: $(getMapValue "mapName" "mapKey1")"

#add two new pairs key/values and print all keys
putMapValue "mapName" "mapKey2" "map Value 2"
putMapValue "mapName" "mapKey3" "map Value 3"
echo -e "mapName keys are \n$(getMapKeys "mapName")"

#create new map
putMapValue "subMapName" "subMapKey1" "sub map Value 1"
putMapValue "subMapName" "subMapKey2" "sub map Value 2"

#and put it in mapName under key "mapKey4"
putMapValue "mapName" "mapKey4" "subMapName"

#check if under two key were placed maps
echo "is map mapName[mapKey3]? - $(if isMap "$(getMapValue "mapName" "mapKey3")" ; then echo Yes; else echo No; fi)"
echo "is map mapName[mapKey4]? - $(if isMap "$(getMapValue "mapName" "mapKey4")" ; then echo Yes; else echo No; fi)"

#print map with sub maps
printf "%s\n" "$(mapToString "mapName")"

이미 언급했듯이 가장 성능이 좋은 방법은 키 / val을 파일에 쓴 다음 grep / awk를 사용하여 검색하는 것입니다. 모든 종류의 불필요한 IO처럼 들리지만 디스크 캐시가 작동하여 위의 방법 중 하나를 사용하여 메모리에 저장하는 것보다 훨씬 빠릅니다.

내가 좋아하는 빠르고 깨끗한 방법은 다음과 같습니다.

hinit() {
    rm -f /tmp/hashmap.$1
}

hput() {
    echo "$2 $3" >> /tmp/hashmap.$1
}

hget() {
    grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };'
}

hinit capitols
hput capitols France Paris
hput capitols Netherlands Amsterdam
hput capitols Spain Madrid

echo `hget capitols France` and `hget capitols Netherlands` and `hget capitols Spain`

키당 단일 값을 적용하려면 hput ()에서 약간의 grep / sed 작업을 수행 할 수도 있습니다.


몇 년 전에 나는 다른 기능 (로깅, 구성 파일, 명령 줄 인수에 대한 확장 지원, 도움말 생성, 단위 테스트 등) 중에서 연관 배열을 지원하는 bash 용 스크립트 라이브러리를 작성했습니다. 라이브러리에는 연관 배열을위한 랩퍼가 포함되어 있으며 적절한 모델 (bash4의 내부 및 이전 버전의 에뮬레이션)로 자동 전환됩니다. 쉘 프레임 워크라고하며 origo.ethz.ch에서 호스팅되었지만 오늘날 리소스는 닫힙니다. 누군가가 여전히 필요하면 공유 할 수 있습니다.


셸에는 데이터 구조와 같은 기본 제공 맵이 없으므로 원시 문자열을 사용하여 다음과 같은 항목을 설명합니다.

ARRAY=(
    "item_A|attr1|attr2|attr3"
    "item_B|attr1|attr2|attr3"
    "..."
)

항목 및 해당 속성을 추출 할 때 :

for item in "${ARRAY[@]}"
do
    item_name=$(echo "${item}"|awk -F "|" '{print $1}')
    item_attr1=$(echo "${item}"|awk -F "|" '{print $2}')
    item_attr2=$(echo "${item}"|awk -F "|" '{print $3}')

    echo "${item_name}"
    echo "${item_attr1}"
    echo "${item_attr2}"
done

이것은 다른 사람들의 대답보다 영리하지 않은 것처럼 보이지만 새로운 사람들이 이해하기 쉽습니다.


jq가 사용 가능한 경우 다른 옵션 추가 :

export NAMES="{
  \"Mary\":\"100\",
  \"John\":\"200\",
  \"Mary\":\"50\",
  \"John\":\"300\",
  \"Paul\":\"100\",
  \"Paul\":\"400\",
  \"David\":\"100\"
}"
export NAME=David
echo $NAMES | jq --arg v "$NAME" '.[$v]' | tr -d '"' 

Vadim의 솔루션을 다음과 같이 수정했습니다.

####################################################################
# Bash v3 does not support associative arrays
# and we cannot use ksh since all generic scripts are on bash
# Usage: map_put map_name key value
#
function map_put
{
    alias "${1}$2"="$3"
}

# map_get map_name key
# @return value
#
function map_get {
    if type -p "${1}$2"
        then
            alias "${1}$2" | awk -F "'" '{ print $2; }';
    fi
}

# map_keys map_name 
# @return map keys
#
function map_keys
{
    alias -p | grep $1 | cut -d'=' -f1 | awk -F"$1" '{print $2; }'
}

존재하지 않는 키를 요청하면 오류가 반환되지 않도록 map_get을 변경하는 것이 좋지만 부작용은 누락 된 맵을 자동으로 무시하지만 방금 후 유스 케이스에 더 적합합니다. 루프에서 항목을 건너 뛰기 위해 키를 확인하려고했습니다.


늦은 답변이지만 다음과 같은 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

참고 URL : https://stackoverflow.com/questions/688849/associative-arrays-in-shell-scripts

반응형