Нам потрібен був сценарій, який імітує асоціативні масиви або Map як структуру даних для Shell Scripting, будь-якого тіла?
Нам потрібен був сценарій, який імітує асоціативні масиви або Map як структуру даних для Shell Scripting, будь-якого тіла?
Відповіді:
Щоб додати відповідь Ірфана , ось коротка та швидша версія, 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}
.
Ще один невдалий 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
редагувати : Цей підхід насправді трохи швидший, ніж лінійний пошук за допомогою sed, запропонований запитувачем, а також більш надійний (він дозволяє клавішам і значенням містити -, =, пробіл, 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
Результати:
$ час ./driver.sh irfan 10 5 реальні 0м0,975с користувач 0m0.280s sys 0m0.691s $ час ./driver.sh Браян 10 5 реальні 0м0.226с користувач 0m0.057s sys 0m0.123s $ time ./driver.sh jerry 10 5 реальні 0м0.706с користувач 0m0.228s sys 0m0.530s $ час ./driver.sh irfan 100 5 реальні 0м10.633с користувач 0m4.366s sys 0m7.127s $ час ./driver.sh Брайан 100 5 реальні 0м1.682с користувач 0m0.546s sys 0m1.082s $ час ./driver.sh jerry 100 5 реальні 0м9.315с користувач 0m4.565s sys 0m5.446s $ час ./driver.sh irfan 10 500 справжній 1м46.197с користувач 0m44.869s sys 1m12.282s $ час ./driver.sh brian 10 500 реальні 0м16.003с користувач 0m5.135s sys 0m10.396s $ час ./driver.sh jerry 10 500 реальні 1м24.414с користувач 0m39.696s sys 0m54.834s $ час ./driver.sh irfan 1000 5 справжні 4м25.145с користувач 3m17.286s sys 1m21.490s $ час ./driver.sh brian 1000 5 реальні 0м19.442с користувач 0m5.287s sys 0m10.751s $ час ./driver.sh jerry 1000 5 реальні 5м29.136с користувач 4m48.926s sys 0m59.336s
Bash4 підтримує це споконвічно. Не використовуйте grep
або eval
, вони найгірші з хаків.
Детальну відповідь із прикладом коду див. На веб-сторінці : /programming/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 = Мій: SP: Власний: 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"
`
}
eval
дані так, ніби це баш-код, і ще більше: ви не можете їх цитувати належним чином. Обидва викликають масу помилок і довільну ін'єкцію коду.
Для 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
Caveat: довжина зараз неправильна.
Крім того, якщо ви хочете зберегти нульову індексацію, ви можете зарезервувати інше значення індексу та захистити від неіснуючого ключа, але він менш читабельний:
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
Команда внизу підсумовує все, використовуючи динамічні змінні як ключі, у вигляді карти _ $ {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!"
Ще один не-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
виводити, але це не проблема, якщо ви не пурист, який уникає розгортання передплатників.
Шкода, що я не бачив цього питання раніше - я написав оболонку бібліотеки, яка містить серед інших карти (асоціативні масиви). Останню його версію можна знайти тут .
Приклад:
#!/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")"
Як я вже згадував, я вважаю, що найкращим методом є виписати ключ / 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 ().
кілька років тому я написав бібліотеку сценаріїв для bash, яка підтримувала асоціативні масиви серед інших функцій (ведення журналів, файли конфігурації, розширена підтримка аргументу командного рядка, створення довідки, тестування одиниць тощо). Бібліотека містить обгортку для асоціативних масивів і автоматично переходить на відповідну модель (внутрішню для bash4 та емуляцію для попередніх версій). Він називався shell-frame і розміщувався на сайті origo.ethz.ch, але сьогодні ресурс закритий. Якщо комусь це все ще потрібно, я можу поділитися цим з вами.
Shell не має вбудованої карти, як структура даних, я використовую необмежену рядок для опису таких елементів:
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, прочитаний, як показано у фрагменті коду із сценарію брандмауера 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