Was ist das Äquivalent zu Python-Wörterbüchern, aber in Bash (sollte unter OS X und Linux funktionieren).
Was ist das Äquivalent zu Python-Wörterbüchern, aber in Bash (sollte unter OS X und Linux funktionieren).
Antworten:
Bash 4 unterstützt diese Funktion nativ. Stellen Sie sicher , dass Ihr Skript Hash - Bang ist #!/usr/bin/env bash
oder #!/bin/bash
so dass Sie nicht am Ende mit sh
. Stellen Sie sicher , entweder sind Ausführung Skript direkt, oder führen Sie script
mit bash script
. (Nicht ausgeführt wird tatsächlich ein Bash - Skript mit Bash nicht passieren, und wird wirklich verwirrend!)
Sie deklarieren ein assoziatives Array, indem Sie Folgendes tun:
declare -A animals
Sie können es mit dem normalen Array-Zuweisungsoperator mit Elementen füllen. Wenn Sie beispielsweise eine Karte haben möchten von animal[sound(key)] = animal(value)
:
animals=( ["moo"]="cow" ["woof"]="dog")
Oder führen Sie sie zusammen:
declare -A animals=( ["moo"]="cow" ["woof"]="dog")
Verwenden Sie sie dann wie normale Arrays. Verwenden
animals['key']='value'
Wert setzen
"${animals[@]}"
um die Werte zu erweitern
"${!animals[@]}"
(beachten Sie die !
), um die Tasten zu erweitern
Vergiss nicht, sie zu zitieren:
echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
Vor Bash 4 haben Sie keine assoziativen Arrays. Nicht verwenden eval
, um sie zu emulieren . Vermeiden Sie eval
wie die Pest, denn es ist die Pest von Shell Scripting. Der wichtigste Grund ist, dass eval
Ihre Daten als ausführbarer Code behandelt werden (es gibt auch viele andere Gründe).
In erster Linie : Erwägen Sie ein Upgrade auf Bash 4. Dies erleichtert Ihnen den gesamten Vorgang erheblich.
Wenn es einen Grund gibt, warum Sie kein Upgrade durchführen können, declare
ist dies eine weitaus sicherere Option. Es wertet Daten nicht wie Bash-Code aus eval
und erlaubt daher nicht so einfach das Einfügen von willkürlichem Code.
Bereiten wir die Antwort vor, indem wir die Konzepte einführen:
Erstens die Indirektion.
$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}"
cow
Zweitens declare
:
$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo"
cow
Bring sie zusammen:
# Set a value:
declare "array_$index=$value"
# Get a value:
arrayGet() {
local array=$1 index=$2
local i="${array}_$index"
printf '%s' "${!i}"
}
Lass es uns benutzen:
$ sound=moo
$ animal=cow
$ declare "animals_$sound=$animal"
$ arrayGet animals "$sound"
cow
Hinweis: declare
Kann nicht in eine Funktion eingefügt werden. Bei jeder Verwendung declare
innerhalb einer Bash-Funktion wird die von ihr lokal erstellte Variable in den Bereich dieser Funktion verschoben, was bedeutet, dass wir damit nicht auf globale Arrays zugreifen oder diese ändern können. (In Bash 4 können Sie deklarieren -g verwenden, um globale Variablen zu deklarieren. In Bash 4 können Sie jedoch zunächst assoziative Arrays verwenden, um diese Problemumgehung zu vermeiden.)
Zusammenfassung:
declare -A
für assoziative Arrays.declare
Option, wenn Sie kein Upgrade durchführen können.awk
stattdessen und vermeiden Sie das Problem insgesamt.4.x
und nicht y
.
sudo port install bash
für diejenigen (mit Bedacht IMHO), die nicht bereit sind, Verzeichnisse im PATH für alle Benutzer ohne explizite prozessbezogene Eskalation von Berechtigungen beschreibbar zu machen.
Es gibt eine Parametersubstitution, obwohl es auch ein Un-PC sein kann ... wie Indirektion.
#!/bin/bash
# Array pretending to be a Pythonic dictionary
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
printf "%s is an extinct animal which likes to %s\n" "${ARRAY[1]%%:*}" "${ARRAY[1]##*:}"
Der BASH 4-Weg ist natürlich besser, aber wenn Sie einen Hack brauchen ... reicht nur ein Hack. Sie können das Array / den Hash mit ähnlichen Techniken durchsuchen.
VALUE=${animal#*:}
um den Fall zu schützen, in demARRAY[$x]="caesar:come:see:conquer"
for animal in "${ARRAY[@]}"; do
Das habe ich hier gesucht:
declare -A hashmap
hashmap["key"]="value"
hashmap["key2"]="value2"
echo "${hashmap["key"]}"
for key in ${!hashmap[@]}; do echo $key; done
for value in ${hashmap[@]}; do echo $value; done
echo hashmap has ${#hashmap[@]} elements
Dies hat bei Bash 4.1.5 bei mir nicht funktioniert:
animals=( ["moo"]="cow" )
Sie können die Schnittstelle hput () / hget () weiter ändern, sodass Sie Hashes wie folgt benannt haben:
hput() {
eval "$1""$2"='$3'
}
hget() {
eval echo '${'"$1$2"'#hash}'
}
und dann
hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
Auf diese Weise können Sie andere Karten definieren, die nicht in Konflikt stehen (z. B. "Hauptstädte", die die Ländersuche nach Hauptstadt durchführen). Aber ich denke, Sie werden feststellen, dass dies alles ziemlich schrecklich ist, was die Leistung betrifft.
Wenn Sie wirklich eine schnelle Hash-Suche wünschen, gibt es einen schrecklichen, schrecklichen Hack, der wirklich gut funktioniert. Es ist dies: Schreiben Sie Ihre Schlüssel / Werte in eine temporäre Datei, eine pro Zeile, und verwenden Sie dann 'grep "^ $ key"', um sie herauszuholen. Verwenden Sie dazu Pipes mit cut oder awk oder sed oder was auch immer, um die Werte abzurufen.
Wie ich schon sagte, es klingt schrecklich und es klingt so, als ob es langsam sein und alle möglichen unnötigen E / A-Vorgänge ausführen sollte, aber in der Praxis ist es sehr schnell (der Festplatten-Cache ist fantastisch, nicht wahr?), Selbst für sehr großen Hash Tabellen. Sie müssen die Eindeutigkeit der Schlüssel selbst erzwingen usw. Selbst wenn Sie nur einige hundert Einträge haben, wird die Kombination aus Ausgabedatei und grep erheblich schneller sein - meiner Erfahrung nach um ein Vielfaches schneller. Es frisst auch weniger Speicher.
Hier ist eine Möglichkeit, dies zu tun:
hinit() {
rm -f /tmp/hashmap.$1
}
hput() {
echo "$2 $3" >> /tmp/hashmap.$1
}
hget() {
grep "^$2 " /tmp/hashmap.$1 | awk '{ print $2 };'
}
hinit capitals
hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
Das Dateisystem ist eine Baumstruktur, die als Hash-Map verwendet werden kann. Ihre Hash-Tabelle ist ein temporäres Verzeichnis, Ihre Schlüssel sind Dateinamen und Ihre Werte sind Dateiinhalte. Der Vorteil ist, dass es große Hashmaps verarbeiten kann und keine bestimmte Shell benötigt.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
Natürlich ist es langsam, aber nicht so langsam. Ich habe es auf meinem Computer mit einer SSD und btrfs getestet und es liest / schreibt ungefähr 3000 Elemente pro Sekunde .
mkdir -d
? (Nicht 4.3, auf Ubuntu 14. Ich würde zurückgreifen mkdir /run/shm/foo
, oder wenn das RAM füllen würde mkdir /tmp/foo
.)
mktemp -d
war stattdessen gemeint?
$value=$(< $hashtable/$key)
und value=$(< $hashtable/$key)
? Vielen Dank!
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
${var#start}
entfernt den Text Start von Anfang an dem in der Variablen gespeicherten Wert var .
Stellen Sie sich eine Lösung vor, bei der das eingebaute Bash-Read verwendet wird , wie im Code-Snippet eines folgenden ufw-Firewall-Skripts dargestellt. 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
IFS=$'|' read -r first rest <<< "$fields"
Ich stimme @lhunath und anderen zu, dass das assoziative Array der richtige Weg für Bash 4 ist. Wenn Sie an Bash 3 festhalten (OSX, alte Distributionen, die Sie nicht aktualisieren können), können Sie auch expr verwenden, was überall eine Zeichenfolge sein sollte und reguläre Ausdrücke. Ich mag es besonders, wenn das Wörterbuch nicht zu groß ist.
Schreiben Sie Ihre Karte als Zeichenfolge (beachten Sie das Trennzeichen '' auch am Anfang und am Ende).
animals=",moo:cow,woof:dog,"
Verwenden Sie einen regulären Ausdruck, um die Werte zu extrahieren
get_animal {
echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
}
Teilen Sie die Zeichenfolge, um die Elemente aufzulisten
get_animal_items {
arr=$(echo "${animals:1:${#animals}-2}" | tr "," "\n")
for i in $arr
do
value="${i##*:}"
key="${i%%:*}"
echo "${value} likes to $key"
done
}
Jetzt können Sie es verwenden:
$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
Ich mochte die Antwort von Al P sehr, wollte aber, dass die Eindeutigkeit billig durchgesetzt wird, also ging ich noch einen Schritt weiter - benutze ein Verzeichnis. Es gibt einige offensichtliche Einschränkungen (Verzeichnisdateilimits, ungültige Dateinamen), aber es sollte in den meisten Fällen funktionieren.
hinit() {
rm -rf /tmp/hashmap.$1
mkdir -p /tmp/hashmap.$1
}
hput() {
printf "$3" > /tmp/hashmap.$1/$2
}
hget() {
cat /tmp/hashmap.$1/$2
}
hkeys() {
ls -1 /tmp/hashmap.$1
}
hdestroy() {
rm -rf /tmp/hashmap.$1
}
hinit ids
for (( i = 0; i < 10000; i++ )); do
hput ids "key$i" "value$i"
done
for (( i = 0; i < 10000; i++ )); do
printf '%s\n' $(hget ids "key$i") > /dev/null
done
hdestroy ids
Es ist auch ein bisschen besser in meinen Tests.
$ time bash hash.sh
real 0m46.500s
user 0m16.767s
sys 0m51.473s
$ time bash dirhash.sh
real 0m35.875s
user 0m8.002s
sys 0m24.666s
Ich dachte nur, ich würde mitmachen. Prost!
Bearbeiten: Hinzufügen von hdestroy ()
Zwei Dinge, Sie können Speicher anstelle von / tmp in jedem Kernel 2.6 verwenden, indem Sie / dev / shm (Redhat) verwenden. Andere Distributionen können variieren. Außerdem kann hget mit read wie folgt neu implementiert werden:
function hget {
while read key idx
do
if [ $key = $2 ]
then
echo $idx
return
fi
done < /dev/shm/hashmap.$1
}
Durch die Annahme, dass alle Schlüssel eindeutig sind, schließt die Rückgabe die Leseschleife kurz und verhindert, dass alle Einträge durchgelesen werden müssen. Wenn Ihre Implementierung doppelte Schlüssel haben kann, lassen Sie die Rückgabe einfach weg. Dies spart die Kosten für das Lesen und Gabeln von grep und awk. Die Verwendung von / dev / shm für beide Implementierungen ergab Folgendes unter Verwendung von time hget für einen Hash mit 3 Einträgen, der nach dem letzten Eintrag suchte:
Grep / Awk:
hget() {
grep "^$2 " /dev/shm/hashmap.$1 | awk '{ print $2 };'
}
$ time echo $(hget FD oracle)
3
real 0m0.011s
user 0m0.002s
sys 0m0.013s
Lesen / Echo:
$ time echo $(hget FD oracle)
3
real 0m0.004s
user 0m0.000s
sys 0m0.004s
Bei mehreren Aufrufen habe ich nie weniger als 50% Verbesserung gesehen. Dies alles kann aufgrund der Verwendung von auf Gabel über Kopf zurückgeführt werden /dev/shm
.
Ein Mitarbeiter hat gerade diesen Thread erwähnt. Ich habe Hash-Tabellen unabhängig in Bash implementiert und es ist nicht abhängig von Version 4. Aus einem meiner Blog-Posts im März 2010 (vor einigen Antworten hier ...) mit dem Titel Hash-Tabellen in Bash :
Ich früher verwendet , cksum
um Hash aber seit übersetzt Java - String hashCode zu nativem bash / zsh.
# Here's the hashing function
ht() {
local h=0 i
for (( i=0; i < ${#1}; i++ )); do
let "h=( (h<<5) - h ) + $(printf %d \'${1:$i:1})"
let "h |= h"
done
printf "$h"
}
# Example:
myhash[`ht foo bar`]="a value"
myhash[`ht baz baf`]="b value"
echo ${myhash[`ht baz baf`]} # "b value"
echo ${myhash[@]} # "a value b value" though perhaps reversed
echo ${#myhash[@]} # "2" - there are two values (note, zsh doesn't count right)
Es ist nicht bidirektional und die eingebaute Methode ist viel besser, sollte aber auch nicht wirklich verwendet werden. Bash ist für schnelle Unikate gedacht, und solche Dinge sollten ziemlich selten mit Komplexität verbunden sein, die Hashes erfordern könnte, außer vielleicht bei Ihnen ~/.bashrc
und Ihren Freunden.
Vor Bash 4 gibt es keine gute Möglichkeit, assoziative Arrays in Bash zu verwenden. Am besten verwenden Sie eine interpretierte Sprache, die solche Dinge wie awk tatsächlich unterstützt. Auf der anderen Seite tut Bash 4 sie unterstützen.
In Bezug auf weniger gute Möglichkeiten in Bash 3 finden Sie hier eine Referenz, die möglicherweise hilfreich ist: http://mywiki.wooledge.org/BashFAQ/006
Bash 3-Lösung:
Beim Lesen einiger Antworten habe ich eine kurze kleine Funktion zusammengestellt, die ich zurückgeben möchte, um anderen zu helfen.
# Define a hash like this
MYHASH=("firstName:Milan"
"lastName:Adamovsky")
# Function to get value by key
getHashKey()
{
declare -a hash=("${!1}")
local key
local lookup=$2
for key in "${hash[@]}" ; do
KEY=${key%%:*}
VALUE=${key#*:}
if [[ $KEY == $lookup ]]
then
echo $VALUE
fi
done
}
# Function to get a list of all keys
getHashKeys()
{
declare -a hash=("${!1}")
local KEY
local VALUE
local key
local lookup=$2
for key in "${hash[@]}" ; do
KEY=${key%%:*}
VALUE=${key#*:}
keys+="${KEY} "
done
echo $keys
}
# Here we want to get the value of 'lastName'
echo $(getHashKey MYHASH[@] "lastName")
# Here we want to get all keys
echo $(getHashKeys MYHASH[@])
Ich habe auch den bash4-Weg benutzt, aber ich finde einen nervigen Fehler.
Ich musste den assoziativen Array-Inhalt dynamisch aktualisieren, also habe ich Folgendes verwendet:
for instanceId in $instanceList
do
aws cloudwatch describe-alarms --output json --alarm-name-prefix $instanceId| jq '.["MetricAlarms"][].StateValue'| xargs | grep -E 'ALARM|INSUFFICIENT_DATA'
[ $? -eq 0 ] && statusCheck+=([$instanceId]="checkKO") || statusCheck+=([$instanceId]="allCheckOk"
done
Ich finde heraus, dass das Anhängen von bash 4.3.11 an einen vorhandenen Schlüssel im Diktat dazu führte, dass der Wert angehängt wurde, falls er bereits vorhanden war. Nach einiger Wiederholung war der Inhalt des Werts beispielsweise "checkKOcheckKOallCheckOK" und dies war nicht gut.
Kein Problem mit Bash 4.3.39, bei dem das Anhängen eines vorhandenen Schlüssels bedeutet, den tatsächlichen Wert zu ersetzen, wenn er bereits vorhanden ist.
Ich habe dieses Problem gelöst, indem ich das assoziative Array statusCheck vor dem Cicle bereinigt / deklariert habe:
unset statusCheck; declare -A statusCheck
Ich erstelle HashMaps in Bash 3 mit dynamischen Variablen. Wie das funktioniert, erklärte ich in meiner Antwort auf: Assoziative Arrays in Shell-Skripten erklärt
Sie können auch einen Blick auf shell_map werfen , eine HashMap-Implementierung aus Bash 3.