Нам нужен сценарий, который имитирует ассоциативные массивы или структуру данных типа Map для сценариев оболочки, любое тело?
Ассоциативные массивы в сценариях оболочки
Ответы (17)
Чтобы добавить в ответ Ирфана, вот более короткая и быстрая версия 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
, а в некоторых случаях это может вообще не понадобиться.
test -z ${variable+x}
(x
не имеет значения, это может быть любая строка). Для ассоциативного массива в Bash вы можете сделать то же самое; используйте test -z ${map[key]+x}
.
- person Brian Campbell; 06.02.2014
Еще один способ без использования 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)
Если вам не нравится использовать 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 ": СП: "). Тот факт, что он использует файловую систему, не замедляет его; эти файлы никогда не гарантированно будут записаны на диск, если вы не вызовете 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
$0
будет равно -bash
, и похоже, что basename
интерпретирует это как флаги, а не как аргумент для генерации базового имени от. Одно из исправлений - просто жестко запрограммировать prefix
. Это не используется ни для чего очень важного, а просто добавляет строку к имени временных каталогов, поэтому, если вы видите их в /tmp
, вы можете сказать, откуда они. Ваша реализация basename
может поддерживать использование --
для отделения флагов от аргументов: basename -- "$0"
.
- person Brian Campbell; 02.05.2017
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
Теперь отвечу на этот вопрос.
Следующие сценарии моделируют ассоциативные массивы в сценариях оболочки. Это просто и очень легко понять.
Карта - это не что иное, как бесконечная строка, в которой 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
edit: Только что добавлен еще один метод для получения всех ключей.
getKeySet() {
if [ "$#" != 1 ];
then
exit 1;
fi
mapName=$1;
eval map="\"\$$mapName\""
keySet=`
echo $map |
sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
`
}
eval
обрабатываете данные так, как будто это код на bash, и более того: вы неправильно их цитируете. Оба вызывают массу ошибок и произвольное внедрение кода.
- person lhunath; 12.08.2010
Еще один способ, отличный от 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
, поэтому он должен 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=()
.
Будьте осторожны, по умолчанию используется первый, поэтому вы можете отложить нулевой элемент:
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))]}
Вы можете использовать имена динамических переменных и позволить именам переменных работать как ключи хэш-карты.
Например, если у вас есть входной файл с двумя столбцами, имя, кредит, как в примере ниже, и вы хотите просуммировать доход каждого пользователя:
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, 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!"
Добавление другого варианта, если доступен 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 '"'
Я обнаружил, что, как уже упоминалось, самый эффективный метод - это записать key / vals в файл, а затем использовать grep / awk для их получения. Это звучит как всевозможные ненужные операции ввода-вывода, но дисковый кеш срабатывает и делает его чрезвычайно эффективным - намного быстрее, чем пытаться сохранить их в памяти с помощью одного из вышеперечисленных методов (как показывают тесты).
Вот быстрый и чистый метод, который мне нравится:
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 в hput ().
Как жаль, что не увидел раньше вопроса - я написал библиотеку shell-framework, которая содержит среди прочего карты (ассоциативные массивы). Последнюю версию можно найти здесь.
Пример:
#!/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 и эмулируемую для предыдущих версий). Он назывался shell-framework и размещался на сайте 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
Кажется, что это не умнее, чем ответ других людей, но легко понять, чтобы новички могли пустить пыль в глаза.
Я модифицировал решение Вадима следующим образом:
####################################################################
# 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, чтобы он не возвращал ошибки, если вы запрашиваете ключ, которого не существует, хотя побочным эффектом является то, что он также будет молча игнорировать отсутствующие карты, но это лучше подходит для моего варианта использования, поскольку я просто хотел проверить наличие ключа, чтобы пропускать элементы в цикле.
Поздний ответ, но рассмотрите возможность решения проблемы таким образом, используя встроенную функцию bash read, как показано в следующем фрагменте кода из сценария брандмауэра ufw. Преимущество этого подхода состоит в том, что используется столько наборов полей с разделителями (а не только 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