Chúng tôi yêu cầu một tập lệnh mô phỏng các mảng kết hợp hoặc Bản đồ như cấu trúc dữ liệu cho Shell Scripting, bất kỳ phần thân nào?
Chúng tôi yêu cầu một tập lệnh mô phỏng các mảng kết hợp hoặc Bản đồ như cấu trúc dữ liệu cho Shell Scripting, bất kỳ phần thân nào?
Câu trả lời:
Để thêm vào câu trả lời của Irfan , đây là phiên bản ngắn hơn và nhanh hơn get()
vì nó không yêu cầu lặp lại nội dung bản đồ:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
Một tùy chọn khác, nếu tính di động không phải là mối quan tâm chính của bạn, là sử dụng các mảng kết hợp được tích hợp vào trình bao. Điều này sẽ hoạt động trong bash 4.0 (hiện có sẵn trên hầu hết các bản phát hành chính, mặc dù không phải trên OS X trừ khi bạn tự cài đặt nó), ksh và zsh:
declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"
echo ${newmap[company]}
echo ${newmap[name]}
Tùy thuộc vào vỏ, bạn có thể cần phải typeset -A newmap
thay thế declare -A newmap
hoặc trong một số trường hợp có thể không cần thiết chút nào.
test -z ${variable+x}
( x
không quan trọng, đó có thể là bất kỳ chuỗi nào). Đối với một mảng kết hợp trong Bash, bạn có thể làm tương tự; sử dụng test -z ${map[key]+x}
.
Một cách khác không 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"
Bạn cũng có thể ném một câu lệnh if để tìm kiếm trong đó. nếu [[$ var = ~ / blah /]]. hay bất cứ cái gì.
Tôi nghĩ rằng bạn cần phải lùi lại và suy nghĩ về những gì một bản đồ, hoặc mảng kết hợp, thực sự là gì. Tất cả đó là một cách để lưu trữ một giá trị cho một khóa nhất định và lấy lại giá trị đó một cách nhanh chóng và hiệu quả. Bạn cũng có thể muốn lặp lại các khóa để truy xuất mọi cặp giá trị khóa hoặc xóa các khóa và các giá trị liên quan của chúng.
Bây giờ, hãy nghĩ về một cấu trúc dữ liệu bạn sử dụng toàn bộ thời gian trong kịch bản lệnh shell và thậm chí chỉ trong trình bao mà không viết tập lệnh, có các thuộc tính này. Bướng bỉnh? Đó là hệ thống tập tin.
Thực sự, tất cả những gì bạn cần để có một mảng kết hợp trong lập trình shell là một thư mục tạm thời. mktemp -d
là hàm tạo mảng kết hợp của bạn:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
Nếu bạn không thích sử dụng echo
và cat
, bạn luôn có thể viết một số hàm bao nhỏ; những cái này được mô hình hóa từ Irfan, mặc dù chúng chỉ xuất giá trị chứ không đặt các biến tùy ý như $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
chỉnh sửa : Cách tiếp cận này thực sự nhanh hơn một chút so với tìm kiếm tuyến tính bằng cách sử dụng sed do người hỏi đề xuất, cũng như mạnh mẽ hơn (nó cho phép các khóa và giá trị chứa -, =, dấu cách, qnd ": SP:"). Thực tế là nó sử dụng hệ thống tập tin không làm cho nó chậm; những tệp này thực sự không bao giờ được đảm bảo ghi vào đĩa trừ khi bạn gọi sync
; đối với các tệp tạm thời như thế này với thời gian tồn tại ngắn, không có khả năng nhiều tệp sẽ không bao giờ được ghi vào đĩa.
Tôi đã thực hiện một vài điểm chuẩn về mã của Irfan, sửa đổi mã của Irfan và mã của tôi bằng chương trình trình điều khiển sau:
#!/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
Kết quả:
$ thời gian ./do.sh irfan 10 5 số 0m0.975 thật người dùng 0m0.280 sys 0m0.691s $ thời gian ./do.sh brian 10 5 số 0m0.226 thực người dùng 0m0.057s sys 0m0.123s $ thời gian ./do.sh jerry 10 5 0m0.706s thật người dùng 0m0.228s sys 0m0.530s $ thời gian ./do.sh irfan 100 5 số 0m10.633 thật người dùng 0m4.366 sys 0m7.127s $ thời gian ./do.sh brian 100 5 số 0m1.682 thực người dùng 0m0.546s sys 0m1.082s $ thời gian ./do.sh jerry 100 5 0m9.315s thật người dùng 0m4.565s sys 0m5.446s $ thời gian ./do.sh irfan 10 500 1m46.197 thật người dùng 0m44.869 sys 1m12.282 $ thời gian ./do.sh brian 10 500 0m16.003s thực người dùng 0m5.135 sys 0m10.396s $ thời gian ./do.sh jerry 10 500 1m24.414s thật người dùng 0m39.696 sys 0m54.834s $ thời gian ./do.sh irfan 1000 5 4m25.145 thực người dùng 3m17.286 sys 1m21.490 $ thời gian ./do.sh brian 1000 5 số 0m19.442 thực người dùng 0m5.287 sys 0m10.751s $ thời gian ./do.sh jerry 1000 5 5m29.136 thực người dùng 4m48.926s sys 0m59.336s
Bash4 hỗ trợ điều này nguyên bản. Không sử dụng grep
hoặc eval
, chúng là xấu nhất của hack.
Đối với một verbose, câu trả lời chi tiết với mã ví dụ, xem: /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; }'
}
Thí dụ:
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
Bây giờ trả lời câu hỏi này.
Các kịch bản sau mô phỏng các mảng kết hợp trong các kịch bản shell. Nó đơn giản và rất dễ hiểu.
Bản đồ không có gì ngoài một chuỗi không bao giờ kết thúc có keyValuePair được lưu dưới dạng --name = Irfan --designation = SSE --company = My: SP: own: SP: Company
khoảng trắng được thay thế bằng ': SP:' cho các giá trị
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
chỉnh sửa: Chỉ cần thêm một phương thức khác để tìm nạp tất cả các khóa.
getKeySet() {
if [ "$#" != 1 ];
then
exit 1;
fi
mapName=$1;
eval map="\"\$$mapName\""
keySet=`
echo $map |
sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
`
}
eval
dữ liệu như thể đó là mã bash, và hơn thế nữa: bạn không trích dẫn đúng. Cả hai đều gây ra hàng loạt lỗi và tiêm mã tùy ý.
Đối với Bash 3, có một trường hợp cụ thể có một giải pháp hay và đơn giản:
Nếu bạn không muốn xử lý nhiều biến hoặc các khóa đơn giản là các định danh biến không hợp lệ và mảng của bạn được đảm bảo có ít hơn 256 mục , bạn có thể lạm dụng các giá trị trả về của hàm. Giải pháp này không yêu cầu bất kỳ giá trị con nào vì giá trị này có sẵn dưới dạng một biến, cũng không có bất kỳ phép lặp nào để hiệu suất hét lên. Ngoài ra, nó rất dễ đọc, gần giống như phiên bản Bash 4.
Đây là phiên bản cơ bản nhất:
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[$?]}
Hãy nhớ rằng, sử dụng các trích dẫn duy nhất case
, nếu không, nó có thể là toàn cầu. Thực sự hữu ích cho băm tĩnh / đóng băng ngay từ đầu, nhưng người ta có thể viết một trình tạo chỉ mục từ một hash_keys=()
mảng.
Xem ra, nó mặc định là cái đầu tiên, vì vậy bạn có thể muốn đặt phần tử zeroth sang một bên:
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
Hãy cẩn thận: chiều dài bây giờ không chính xác.
Ngoài ra, nếu bạn muốn giữ lập chỉ mục dựa trên số không, bạn có thể bảo lưu một giá trị chỉ mục khác và bảo vệ chống lại khóa không tồn tại, nhưng nó ít đọc hơn:
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[$?]}
Hoặc, để giữ độ dài chính xác, hãy bù chỉ số theo một:
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))]}
Bạn có thể sử dụng tên biến động và để tên biến hoạt động giống như các khóa của hàm băm.
Ví dụ: nếu bạn có một tệp đầu vào có hai cột, tên, tín dụng, làm ví dụ dưới đây và bạn muốn tính tổng thu nhập của mỗi người dùng:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
Lệnh dưới đây sẽ tổng hợp mọi thứ, sử dụng các biến động làm khóa, dưới dạng bản đồ _ $ {person} :
while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)
Để đọc kết quả:
set | grep map
Đầu ra sẽ là:
map_David=100
map_John=500
map_Mary=150
map_Paul=500
Xây dựng các kỹ thuật này, tôi đang phát triển trên GitHub một chức năng hoạt động giống như Đối tượng HashMap , shell_map .
Để tạo " phiên bản HashMap ", hàm shell_map có thể tự tạo các bản sao dưới các tên khác nhau. Mỗi bản sao chức năng mới sẽ có một biến $ FUNCNAME khác nhau. $ FUNCNAME sau đó được sử dụng để tạo một không gian tên cho mỗi phiên bản Map.
Các khóa bản đồ là các biến toàn cục, ở dạng $ FUNCNAME_DATA_ $ KEY, trong đó $ KEY là khóa được thêm vào Bản đồ. Các biến này là các biến động .
Dưới đây tôi sẽ đặt một phiên bản đơn giản hóa của nó để bạn có thể sử dụng làm ví dụ.
#!/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
}
Sử dụng:
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!"
Một cách khác không phải bash-4 (tức là bash 3, tương thích với 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
Bản in:
A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz
Các chức năng với các case
hành vi như một mảng kết hợp. Thật không may, nó không thể sử dụng return
, vì vậy nó phải echo
xuất ra nó, nhưng đây không phải là vấn đề, trừ khi bạn là một người theo chủ nghĩa thuần túy trốn tránh các subshells.
Thật đáng tiếc tôi đã không nhìn thấy câu hỏi trước đây - Tôi đã viết khung vỏ thư viện chứa các bản đồ khác (mảng kết hợp). Phiên bản cuối cùng của nó có thể được tìm thấy ở đây .
Thí dụ:
#!/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")"
Tôi đã thấy đúng, như đã đề cập, phương pháp hoạt động tốt nhất là viết ra key / vals vào một tệp, sau đó sử dụng grep / awk để lấy chúng. Nghe có vẻ như tất cả các loại IO không cần thiết, nhưng bộ đệm đĩa khởi động và làm cho nó cực kỳ hiệu quả - nhanh hơn nhiều so với việc cố gắng lưu trữ chúng trong bộ nhớ bằng một trong các phương pháp trên (như các điểm chuẩn hiển thị).
Đây là một phương pháp nhanh chóng, gọn gàng mà tôi thích:
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`
Nếu bạn muốn thực thi một giá trị đơn cho mỗi khóa, bạn cũng có thể thực hiện một hành động grep / sed nhỏ trong hput ().
Cách đây vài năm, tôi đã viết thư viện script cho bash hỗ trợ các mảng kết hợp giữa các tính năng khác (ghi nhật ký, tệp cấu hình, hỗ trợ mở rộng cho đối số dòng lệnh, tạo trợ giúp, kiểm tra đơn vị, v.v.). Thư viện chứa một trình bao bọc cho các mảng kết hợp và tự động chuyển sang mô hình thích hợp (nội bộ cho bash4 và mô phỏng cho các phiên bản trước). Nó được gọi là shell-framework và được lưu trữ tại origo.ethz.ch nhưng ngày nay tài nguyên đã bị đóng. Nếu ai đó vẫn cần nó tôi có thể chia sẻ nó với bạn.
Shell không có bản đồ tích hợp như cấu trúc dữ liệu, tôi sử dụng chuỗi thô để mô tả các mục như thế:
ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)
khi trích xuất các mục và thuộc tính của nó:
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
Điều này có vẻ như không thông minh hơn câu trả lời của người khác, nhưng dễ hiểu đối với người mới.
Tôi đã sửa đổi giải pháp của Vadim bằng cách sau:
####################################################################
# 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; }'
}
Thay đổi là map_get để ngăn nó trả lại lỗi nếu bạn yêu cầu khóa không tồn tại, mặc dù tác dụng phụ là nó cũng sẽ âm thầm bỏ qua các bản đồ bị thiếu, nhưng nó phù hợp với trường hợp sử dụng của tôi hơn vì tôi chỉ muốn kiểm tra khóa để bỏ qua các mục trong vòng lặp.
Trả lời trễ, nhưng xem xét giải quyết vấn đề theo cách này, sử dụng bash dựng sẵn đọc như minh họa trong đoạn mã từ tập lệnh tường lửa ufw theo sau. Cách tiếp cận này có lợi thế là sử dụng nhiều bộ trường được phân tách (không chỉ 2) như mong muốn. Chúng tôi đã sử dụng | dấu phân cách vì các bộ xác định phạm vi cổng có thể yêu cầu dấu hai chấm, tức là 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