Ассоциативные массивы в скриптах Shell - PullRequest
106 голосов
/ 27 марта 2009

Нам потребовался скрипт, имитирующий ассоциативные массивы или структуру, подобную Map, для сценариев оболочки, любое тело?

Ответы [ 17 ]

134 голосов
/ 27 марта 2009

Другой вариант, если переносимость не является вашей основной задачей, это использовать ассоциативные массивы, встроенные в оболочку. Это должно работать в 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, или в некоторых случаях это может не понадобиться вообще.

86 голосов
/ 15 декабря 2010

Еще один способ, не связанный с bash.

#!/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 /]]. или что угодно.

32 голосов
/ 27 марта 2009

Я думаю, что вам нужно отступить назад и подумать о том, что на самом деле представляет собой карта или ассоциативный массив. Все это - способ сохранить значение для данного ключа и быстро и эффективно вернуть это значение. Вы также можете захотеть перебирать ключи для получения каждой пары ключ-значение или удалять ключи и связанные с ними значения.

Теперь подумайте о структуре данных, которую вы все время используете в сценариях оболочки, и даже просто в оболочке без написания сценария, которая обладает этими свойствами. Тупик? Это файловая система.

Действительно, все, что вам нужно, чтобы иметь ассоциативный массив в программировании оболочки, это временный каталог. mktemp -d является вашим конструктором ассоциативного массива:

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

Если вы не хотите использовать echo и cat, вы всегда можете написать несколько маленьких оберток; они смоделированы от Ирфана, хотя они просто выводят значение, а не задают произвольные переменные, такие как $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, предложенного спрашивающим, а также более надежен (он позволяет ключам и значениям содержать -, =, space, qnd ": SP:"). Тот факт, что он использует файловую систему, не делает ее медленной; эти файлы фактически никогда не гарантированно будут записаны на диск, если вы не вызовете sync; для временных файлов, подобных этому, с коротким временем жизни, не исключено, что многие из них никогда не будут записаны на диск.

Я сделал несколько тестов кода Ирфана, модификации Джерри кода Ирфана и моего кода, используя следующую программу драйвера:

#!/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 10 5

    real    0m0.975s
    user    0m0.280s
    sys     0m0.691s

    $ time ./driver.sh brian 10 5

    real    0m0.226s
    user    0m0.057s
    sys     0m0.123s

    $ time ./driver.sh jerry 10 5

    real    0m0.706s
    user    0m0.228s
    sys     0m0.530s

    $ time ./driver.sh irfan 100 5

    real    0m10.633s
    user    0m4.366s
    sys     0m7.127s

    $ time ./driver.sh brian 100 5

    real    0m1.682s
    user    0m0.546s
    sys     0m1.082s

    $ time ./driver.sh jerry 100 5

    real    0m9.315s
    user    0m4.565s
    sys     0m5.446s

    $ time ./driver.sh irfan 10 500

    real    1m46.197s
    user    0m44.869s
    sys     1m12.282s

    $ time ./driver.sh brian 10 500

    real    0m16.003s
    user    0m5.135s
    sys     0m10.396s

    $ time ./driver.sh jerry 10 500

    real    1m24.414s
    user    0m39.696s
    sys     0m54.834s

    $ time ./driver.sh irfan 1000 5

    real    4m25.145s
    user    3m17.286s
    sys     1m21.490s

    $ time ./driver.sh brian 1000 5

    real    0m19.442s
    user    0m5.287s
    sys     0m10.751s

    $ time ./driver.sh jerry 1000 5

    real    5m29.136s
    user    4m48.926s
    sys     0m59.336s

20 голосов
/ 27 марта 2009

Чтобы добавить к ответ Ирфана , вот более короткая и быстрая версия get(), так как она не требует итерации по содержимому карты:

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

    map=${!mapName}
    value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
15 голосов
/ 30 сентября 2009
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
7 голосов
/ 12 августа 2010

Bash4 поддерживает это изначально. Не используйте grep или eval, они самые уродливые из хаков.

Подробный подробный ответ с примером кода см .: https://stackoverflow.com/questions/3467959

6 голосов
/ 28 июня 2011
####################################################################
# 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
4 голосов
/ 27 марта 2009

Теперь отвечаю на этот вопрос.

Следующие сценарии имитируют ассоциативные массивы в сценариях оболочки. Это просто и очень легко понять.

Карта - это не что иное, как бесконечная строка, в которой keyValuePair сохранена как --name = Irfan - обозначение = SSE --company = Мой: 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

edit: Просто добавлен еще один метод для извлечения всех ключей.

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

    mapName=$1; 

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

    keySet=`
           echo $map | 
           sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
          `
}
3 голосов
/ 03 марта 2014

Для 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=().

Обратите внимание, по умолчанию используется первый, так что вы можете отложить нулевой элемент:

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[$?]}

Или, чтобы длина была правильной, смещение индекса на единицу:

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))]}
2 голосов
/ 03 июня 2016

Вы можете использовать имена динамических переменных и позволить именам переменных работать как ключи хэш-карты.

Например, если у вас есть входной файл с двумя столбцами, name, 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 используется для создания пространства имен для каждого экземпляра карты.

Ключи карты - это глобальные переменные в форме $ 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!"
...