source

셸 스크립트의 연관 배열

ittop 2023. 4. 27. 22:45
반응형

셸 스크립트의 연관 배열

셸 스크립팅을 위해 연관 배열이나 지도와 같은 데이터 구조를 시뮬레이션하는 스크립트가 필요합니다.누가 어떻게 하는지 알려줄 수 있나요?

이동성이 주요 관심사가 아니라면 셸에 내장된 연결 어레이를 사용하는 것도 방법입니다.이 기능은 bash 4.0(현재 대부분의 주요 디스트리뷰터에서 사용 가능), 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 newmapdeclare -A newmap또는 어떤 경우에는 전혀 필요하지 않을 수도 있습니다.

또 다른 비배시 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"

$var = ~ /blah/ ]인 경우 if 문을 검색할 수도 있습니다.뭐 그런 거.

저는 여러분이 한 발 물러서서 지도나 연상배열이 실제로 무엇인지 생각해 볼 필요가 있다고 생각합니다.주어진 키에 대한 값을 저장하고 해당 값을 빠르고 효율적으로 되돌릴 수 있는 방법이 전부입니다.또한 키를 반복하여 모든 키 값 쌍을 검색하거나 키와 관련 값을 삭제할 수도 있습니다.

이제 셸 스크립팅에서 항상 사용하는 데이터 구조와 스크립트를 작성하지 않고 셸에서만 사용하는 데이터 구조에 대해 생각해 보십시오. 이러한 속성이 있습니다.쩔쩔매는?파일 시스템입니다.

실제로 셸 프로그래밍에서 연상 배열이 필요한 것은 임시 디렉토리뿐입니다. mktemp -d연관 배열 생성자:

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

사용하고 싶지 않은 경우echo그리고.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

edit: 이 접근 방식은 실제로 질문자가 제안한 sed를 사용한 선형 검색보다 훨씬 빠르며, 더 강력합니다(키와 값에 -, =, 공간, 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

결과:

time ./driver.sh irfan 105
실제 0m0.975초사용자 0m0.280년대sys0m0.691s
시간 ./driver.sh brian 105
진짜 0m0226년대사용자 0m0.057ssys0m0.123s
시간 ./드라이버shjerry 105
실제 0m0.706s사용자 0m0.228ssys0m0.530s
time ./driver.sh irfan 1005
진짜 0m10.633년대사용자 0m4.366년대sys0m7.127s
시간 ./driver.sh brian 1005
실제 0m1.682초사용자 0m0.546ssys0m1.082s
시간 ./드라이버sh jerry 1005
실제 0m9.315초사용자 0m4.565ssys0m5.446s
time ./driver.sh irfan 10 500
실제 1m46.375m사용자 0m44.869년대sys 1m12.282년대
time ./driver.sh brian 10 500
실제 0m16.003초사용자 0m5.135ssys 0m10.396년대
시간 ./드라이버shjerry 10 500
실제 1m24.414초사용자 0m39.696년대sys0m54.834년대
time ./driver.sh irfan 10005
실제 4m25.145s사용자 3m17.286ssys 1m21.490s
시간 ./driver.sh brian 1000 5
진짜 0m19.442년대사용자 0m5.287년대sys0m10.751s
시간 ./드라이버sh jerry 1000 5
진짜 5미터 29.136년대사용자 4m48.926ssys 0m59.336년대

Irfan의 답변에 추가하기 위해, 여기 더 짧고 빠른 버전이 있습니다.get()지도 내용에 대한 반복이 필요하지 않기 때문에:

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

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

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

bash-4가 아닌 또 다른 방법(즉, bash 3, Mac 호환):

val_of_key() {
    case $1 in
        'A1') echo 'aaa';;
        'B2') echo 'bbb';;
        'C3') echo 'ccc';;
        *) echo 'zzz';;
    esac
}

for x in 'A1' 'B2' 'C3' 'D4'; do
    y=$(val_of_key "$x")
    echo "$x => $y"
done

인쇄:

A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz

다같습다니가 있는 입니다.case연상 배열처럼 작동합니다.도 유스럽도수없다니습용할사게를 사용할 수 없습니다.return 그야합 ▁to다▁so.echo그것의 출력, 하지만 이것은 문제가 되지 않습니다, 당신이 포킹 서브셸을 피하는 순수주의자가 아니라면요.

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

주의: 길이가 올바르지 않습니다.

또는 제로 기반 인덱싱을 유지하려는 경우 다른 인덱스 값을 예약하고 존재하지 않는 키로부터 보호할 수 있지만 읽기가 더 어렵습니다.

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))]}

이제 이 질문에 답하겠습니다.

다음 스크립트는 셸 스크립트의 연관 배열을 시뮬레이션합니다.그것은 간단하고 이해하기 쉽습니다.

맵은 --name=Irfan --designation=으로 저장된 keyValuePair를 가진 neverending 문자열에 불과합니다.SSE --company=My:SP:소유:SP:

공백이 ':'로 대체됩니다.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"
          `
}

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

예를 들어, 아래 예제와 같이 이름, 신용이라는 두 개의 열이 있는 입력 파일이 있고 각 사용자의 수입을 합계하려고 하는 경우:

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을 사용하여 각 맵 인스턴스에 대한 네임스페이스를 만듭니다.

맵 키는 $FUNCNAME_DATA_$KEY 형식의 글로벌 변수이며, 여기서 $KEY는 맵에 추가된 키입니다.이러한 변수는 동적 변수입니다.

Bellow 당신이 예시로 사용할 수 있도록 간단한 버전을 넣겠습니다.

#!/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!"

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 '"' 

이미 언급했듯이, 가장 좋은 수행 방법은 파일에 키/발을 기록한 다음 grep/awk를 사용하여 검색하는 것입니다.모든 종류의 불필요한 IO처럼 들리지만 Disk 캐쉬가 활성화되어 매우 효율적입니다. 위의 방법 중 하나를 사용하여 메모리에 저장하는 것보다 훨씬 빠릅니다(벤치마크에서 알 수 있음).

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

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`

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

전에 질문을 보지 못한 것이 유감입니다. 다른 것들 중에서도 지도(조합 배열)가 포함된 라이브러리 셸 프레임워크를 작성한 적이 있습니다.마지막 버전은 여기에서 확인할 수 있습니다.

예:

#!/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")"

몇 년 전에 저는 다른 기능(로깅, 구성 파일, 명령줄 인수에 대한 확장 지원, 도움말 생성, 장치 테스트 등) 중에서 연관 배열을 지원하는 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

이것은 다른 사람들의 대답보다 영리하지는 않지만, 새로운 사람들이 공격하기에는 이해하기 쉽습니다.

저는 다음과 같이 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

반응형