Wir benötigten ein Skript, das assoziative Arrays oder eine Map-ähnliche Datenstruktur für Shell Scripting simuliert.
Wir benötigten ein Skript, das assoziative Arrays oder eine Map-ähnliche Datenstruktur für Shell Scripting simuliert.
Antworten:
Um Irfans Antwort zu ergänzen , hier eine kürzere und schnellere Version von, get()
da keine Iteration über den Karteninhalt erforderlich ist:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
Eine andere Option, wenn Portabilität nicht Ihr Hauptanliegen ist, ist die Verwendung von assoziativen Arrays, die in die Shell integriert sind. Dies sollte in Bash 4.0 (jetzt verfügbar in den meisten wichtigen Distributionen, jedoch nicht in OS X, es sei denn, Sie installieren es selbst), ksh und zsh funktionieren:
declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"
echo ${newmap[company]}
echo ${newmap[name]}
Abhängig von der Shell müssen Sie möglicherweise eine typeset -A newmap
anstelle von ausführen declare -A newmap
, oder in einigen Fällen ist dies möglicherweise überhaupt nicht erforderlich.
test -z ${variable+x}
(das x
spielt keine Rolle, das kann eine beliebige Zeichenfolge sein). Für ein assoziatives Array in Bash können Sie ähnliche Aktionen ausführen. verwenden test -z ${map[key]+x}
.
Ein weiterer Non-Bash-4-Weg.
#!/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"
Sie können dort auch eine if-Anweisung für die Suche eingeben. if [[$ var = ~ / blah /]]. oder Wasauchimmer.
Ich denke, Sie müssen einen Schritt zurücktreten und darüber nachdenken, was eine Karte oder ein assoziatives Array wirklich ist. Es ist lediglich eine Möglichkeit, einen Wert für einen bestimmten Schlüssel zu speichern und diesen Wert schnell und effizient zurückzugewinnen. Möglicherweise möchten Sie auch in der Lage sein, die Schlüssel zu durchlaufen, um jedes Schlüsselwertpaar abzurufen, oder Schlüssel und die zugehörigen Werte zu löschen.
Stellen Sie sich nun eine Datenstruktur vor, die Sie beim Shell-Scripting ständig verwenden, und sogar nur in der Shell, ohne ein Skript zu schreiben, das diese Eigenschaften aufweist. Stumped? Es ist das Dateisystem.
Alles, was Sie für ein assoziatives Array in der Shell-Programmierung benötigen, ist ein temporäres Verzeichnis. mktemp -d
ist Ihr assoziativer Array-Konstruktor:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
Wenn Sie keine Lust haben, echo
und zu verwenden cat
, können Sie immer ein paar kleine Wrapper schreiben. Diese sind von Irfans modelliert, obwohl sie nur den Wert ausgeben, anstatt beliebige Variablen wie $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 : Dieser Ansatz ist tatsächlich ziemlich viel schneller als die vom Fragesteller vorgeschlagene lineare Suche mit sed und robuster (er ermöglicht, dass Schlüssel und Werte -, =, Leerzeichen, qnd ": SP:" enthalten). Die Tatsache, dass es das Dateisystem verwendet, macht es nicht langsam; Es wird nie garantiert, dass diese Dateien auf die Festplatte geschrieben werden, es sei denn, Sie rufen an sync
. Bei temporären Dateien wie diesen mit kurzer Lebensdauer ist es nicht unwahrscheinlich, dass viele von ihnen niemals auf die Festplatte geschrieben werden.
Ich habe einige Benchmarks für Irfans Code, Jerrys Modifikation von Irfans Code und meinen Code mit dem folgenden Treiberprogramm durchgeführt:
#!/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
Die Ergebnisse:
$ time ./driver.sh irfan 10 5 echte 0m0.975s Benutzer 0m0.280s sys 0m0.691s $ time ./driver.sh brian 10 5 echte 0m0.226s Benutzer 0m0.057s sys 0m0.123s $ time ./driver.sh jerry 10 5 echte 0m0.706s Benutzer 0m0.228s sys 0m0.530s $ time ./driver.sh irfan 100 5 echte 0m10.633s Benutzer 0m4.366s sys 0m7.127s $ time ./driver.sh brian 100 5 echte 0m1.682s Benutzer 0m0.546s sys 0m1.082s $ time ./driver.sh jerry 100 5 echte 0m9.315s Benutzer 0m4.565s sys 0m5.446s $ time ./driver.sh irfan 10 500 echte 1m46.197s Benutzer 0m44.869s sys 1m12.282s $ time ./driver.sh brian 10 500 echte 0m16.003s Benutzer 0m5.135s sys 0m10.396s $ time ./driver.sh jerry 10 500 echte 1m24.414s Benutzer 0m39.696s sys 0m54.834s $ time ./driver.sh irfan 1000 5 echte 4m25.145s Benutzer 3m17.286s sys 1m21.490s $ time ./driver.sh brian 1000 5 echte 0m19.442s Benutzer 0m5.287s sys 0m10.751s $ time ./driver.sh jerry 1000 5 echte 5m29.136s Benutzer 4m48.926s sys 0m59.336s
Bash4 unterstützt dies nativ. Verwenden Sie nicht grep
oder eval
, sie sind die hässlichsten Hacks.
Eine ausführliche Antwort mit Beispielcode finden Sie unter: /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; }'
}
Beispiel:
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
Beantworten Sie nun diese Frage.
Die folgenden Skripte simulieren assoziative Arrays in Shell-Skripten. Es ist einfach und sehr leicht zu verstehen.
Map ist nichts anderes als eine nie endende Zeichenfolge, in der keyValuePair als --name = Irfan --designation = SSE --company = My: SP: Own: SP: Company gespeichert ist
Leerzeichen werden für Werte durch ': SP:' ersetzt
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
Bearbeiten: Fügte gerade eine andere Methode hinzu, um alle Schlüssel abzurufen.
getKeySet() {
if [ "$#" != 1 ];
then
exit 1;
fi
mapName=$1;
eval map="\"\$$mapName\""
keySet=`
echo $map |
sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
`
}
eval
Daten so ein, als wäre es Bash-Code, und außerdem zitieren Sie sie nicht richtig. Beide verursachen eine Vielzahl von Fehlern und eine willkürliche Code-Injektion.
Für Bash 3 gibt es einen speziellen Fall, der eine schöne und einfache Lösung bietet:
Wenn Sie nicht viele Variablen verarbeiten möchten oder Schlüssel einfach ungültige Variablenkennungen sind und Ihr Array garantiert weniger als 256 Elemente enthält , können Sie Funktionsrückgabewerte missbrauchen. Diese Lösung erfordert weder eine Unterschale, da der Wert als Variable verfügbar ist, noch eine Iteration, sodass die Leistung schreit. Außerdem ist es sehr gut lesbar, fast wie die Bash 4-Version.
Hier ist die grundlegendste Version:
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[$?]}
Denken Sie daran, verwenden Sie einfache Anführungszeichen case
, da dies sonst zu Globbing führen kann. Wirklich nützlich für statische / eingefrorene Hashes von Anfang an, aber man könnte einen Indexgenerator aus einem hash_keys=()
Array schreiben .
Beachten Sie, dass standardmäßig das erste Element verwendet wird. Sie können also das nullte Element beiseite legen:
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
Vorsichtsmaßnahme: Die Länge ist jetzt falsch.
Wenn Sie die auf Null basierende Indizierung beibehalten möchten, können Sie alternativ einen anderen Indexwert reservieren und sich vor einem nicht vorhandenen Schlüssel schützen, der jedoch weniger lesbar ist:
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[$?]}
Um die Länge korrekt zu halten, versetzen Sie den Index um eins:
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))]}
Sie können dynamische Variablennamen verwenden und die Variablennamen wie die Schlüssel einer Hashmap arbeiten lassen.
Wenn Sie beispielsweise eine Eingabedatei mit zwei Spalten haben, Name, Gutschrift als Beispiel unten, und Sie das Einkommen jedes Benutzers summieren möchten:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
Der folgende Befehl summiert alles unter Verwendung dynamischer Variablen als Schlüssel in Form von map _ $ {person} :
while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)
So lesen Sie die Ergebnisse:
set | grep map
Die Ausgabe wird sein:
map_David=100
map_John=500
map_Mary=150
map_Paul=500
Ich arbeite an diesen Techniken und entwickle auf GitHub eine Funktion, die genau wie ein HashMap-Objekt funktioniert , shell_map .
Um " HashMap-Instanzen " zu erstellen, kann die Funktion shell_map Kopien von sich selbst unter verschiedenen Namen erstellen. Jede neue Funktionskopie hat eine andere Variable $ FUNCNAME. $ FUNCNAME wird dann verwendet, um einen Namespace für jede Map-Instanz zu erstellen.
Die Kartenschlüssel sind globale Variablen in der Form $ FUNCNAME_DATA_ $ KEY, wobei $ KEY der der Karte hinzugefügte Schlüssel ist. Diese Variablen sind dynamische Variablen .
Unten werde ich eine vereinfachte Version davon einfügen, damit Sie als Beispiel verwenden können.
#!/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
}
Verwendung:
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!"
Noch eine andere nicht-Bash-4-Methode (dh Bash-3, Mac-kompatibel):
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
Drucke:
A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz
Die Funktion mit case
wirkt wie ein assoziatives Array. Leider kann es nicht verwendet werden return
, daher muss es echo
ausgegeben werden, aber dies ist kein Problem, es sei denn, Sie sind ein Purist, der das Gabeln von Unterschalen meidet.
Wie schade, dass ich die Frage vorher nicht gesehen habe - ich habe ein Library- Shell-Framework geschrieben, das unter anderem die Maps (Assoziative Arrays) enthält. Die letzte Version finden Sie hier .
Beispiel:
#!/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")"
Ich habe festgestellt, dass es, wie bereits erwähnt, am besten ist, Schlüssel / Werte in eine Datei zu schreiben und sie dann mit grep / awk abzurufen. Es klingt nach allen möglichen unnötigen E / A-Vorgängen, aber der Festplatten-Cache wird aktiviert und ist äußerst effizient - viel schneller als der Versuch, sie mit einer der oben genannten Methoden im Speicher zu speichern (wie die Benchmarks zeigen).
Hier ist eine schnelle, saubere Methode, die mir gefällt:
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`
Wenn Sie einen Einzelwert pro Schlüssel erzwingen möchten, können Sie auch eine kleine grep / sed-Aktion in hput () ausführen.
Vor einigen Jahren schrieb ich eine Skriptbibliothek für Bash, die unter anderem assoziative Arrays unterstützte (Protokollierung, Konfigurationsdateien, erweiterte Unterstützung für Befehlszeilenargumente, Hilfe generieren, Komponententests usw.). Die Bibliothek enthält einen Wrapper für assoziative Arrays und wechselt automatisch zum entsprechenden Modell (intern für bash4 und emuliert für frühere Versionen). Es hieß Shell-Framework und wurde auf origo.ethz.ch gehostet, aber heute ist die Ressource geschlossen. Wenn jemand es noch braucht, kann ich es mit Ihnen teilen.
Shell hat keine eingebaute Karte wie Datenstruktur, ich benutze rohe Zeichenfolge, um Elemente wie diese zu beschreiben:
ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)
beim Extrahieren von Elementen und ihren Attributen:
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
Dies scheint nicht klug zu sein als die Antwort anderer, aber für neue Leute leicht zu verstehen.
Ich habe die Lösung von Vadim wie folgt geändert:
####################################################################
# 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; }'
}
Die Änderung betrifft map_get, um zu verhindern, dass Fehler zurückgegeben werden, wenn Sie einen Schlüssel anfordern, der nicht vorhanden ist. Der Nebeneffekt ist jedoch, dass fehlende Karten ebenfalls stillschweigend ignoriert werden, aber sie passen besser zu meinem Anwendungsfall, da ich gerade wollte nach einem Schlüssel suchen, um Elemente in einer Schleife zu überspringen.
Verspätete Antwort, aber erwägen Sie, das Problem auf diese Weise zu beheben, indem Sie die eingebaute Bash verwenden , die im Code-Snippet eines folgenden ufw-Firewall-Skripts dargestellt ist. Dieser Ansatz hat den Vorteil, dass beliebig viele begrenzte Feldsätze (nicht nur 2) verwendet werden. Wir haben die | verwendet Trennzeichen, da für Portbereichsspezifizierer möglicherweise ein Doppelpunkt erforderlich ist, z. B. 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