Qual è l'equivalente dei dizionari Python ma in Bash (dovrebbe funzionare su OS X e Linux).
Qual è l'equivalente dei dizionari Python ma in Bash (dovrebbe funzionare su OS X e Linux).
Risposte:
Bash 4 supporta nativamente questa funzione. Assicurati che l'hashbang del tuo script sia #!/usr/bin/env bash
o meno #!/bin/bash
così non finisci per usarlo sh
. Assicurati di eseguire direttamente lo script o di eseguirlo script
con bash script
. (Non è in realtà l'esecuzione di uno script Bash con Bash non accada, e sarà davvero confondere!)
Dichiara una matrice associativa facendo:
declare -A animals
È possibile riempirlo con elementi utilizzando il normale operatore di assegnazione di array. Ad esempio, se si desidera avere una mappa di animal[sound(key)] = animal(value)
:
animals=( ["moo"]="cow" ["woof"]="dog")
O uniscili:
declare -A animals=( ["moo"]="cow" ["woof"]="dog")
Quindi utilizzali come normali array. Uso
animals['key']='value'
per impostare il valore
"${animals[@]}"
per espandere i valori
"${!animals[@]}"
(notare il !
) per espandere le chiavi
Non dimenticare di citarli:
echo "${animals[moo]}"
for sound in "${!animals[@]}"; do echo "$sound - ${animals[$sound]}"; done
Prima della bash 4, non hai array associativi. Non usare eval
per emularli . Evita eval
come la peste, perché è la piaga degli script di shell. Il motivo più importante è quelloeval
tratta i tuoi dati come codice eseguibile (ci sono anche molti altri motivi).
Innanzitutto : considera l'aggiornamento a bash 4. Questo renderà l'intero processo molto più facile per te.
Se c'è un motivo che non è possibile aggiornare, declare
è un'opzione molto più sicura. Non valuta i dati come codice bash comeeval
fa, e come tale non consente l'iniezione di codice arbitrario così facilmente.
Prepariamo la risposta introducendo i concetti:
Innanzitutto, il riferimento indiretto.
$ animals_moo=cow; sound=moo; i="animals_$sound"; echo "${!i}"
cow
In secondo luogo, declare
:
$ sound=moo; animal=cow; declare "animals_$sound=$animal"; echo "$animals_moo"
cow
Riuniscili:
# Set a value:
declare "array_$index=$value"
# Get a value:
arrayGet() {
local array=$1 index=$2
local i="${array}_$index"
printf '%s' "${!i}"
}
Usiamolo:
$ sound=moo
$ animal=cow
$ declare "animals_$sound=$animal"
$ arrayGet animals "$sound"
cow
Nota: declare
non può essere inserito in una funzione. Qualsiasi uso declare
all'interno di una funzione bash trasforma la variabile che crea locale nell'ambito di quella funzione, il che significa che non possiamo accedere o modificare array globali con essa. (In bash 4 puoi usare declare -g per dichiarare le variabili globali - ma in bash 4, puoi usare le matrici associative in primo luogo, evitando questa soluzione alternativa.)
Sommario:
declare -A
per array associativi.declare
opzione se non è possibile eseguire l'aggiornamento.awk
invece di utilizzare ed evita del tutto il problema.4.x
e non y
.
sudo port install bash
, per quelli (saggiamente, IMHO) riluttanti a rendere scrivibili le directory nel PERCORSO per tutti gli utenti senza esplicita escalation di privilegi per processo.
C'è la sostituzione dei parametri, anche se potrebbe essere anche un PC-come ... come l'indirizzamento.
#!/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]##*:}"
Il modo BASH 4 è ovviamente migliore, ma se hai bisogno di un hack ... solo un hack lo farà. Puoi cercare l'array / hash con tecniche simili.
VALUE=${animal#*:}
per proteggere il caso in cuiARRAY[$x]="caesar:come:see:conquer"
for animal in "${ARRAY[@]}"; do
Questo è quello che stavo cercando qui:
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
Questo non ha funzionato per me con bash 4.1.5:
animals=( ["moo"]="cow" )
È possibile modificare ulteriormente l'interfaccia hput () / hget () in modo da aver nominato gli hash come segue:
hput() {
eval "$1""$2"='$3'
}
hget() {
eval echo '${'"$1$2"'#hash}'
}
e poi
hput capitals France Paris
hput capitals Netherlands Amsterdam
hput capitals Spain Madrid
echo `hget capitals France` and `hget capitals Netherlands` and `hget capitals Spain`
Ciò consente di definire altre mappe che non sono in conflitto (ad esempio, "rcapitals" che effettuano la ricerca per paese per capitale). Ma, in entrambi i casi, penso che scoprirai che è tutto piuttosto terribile, dal punto di vista delle prestazioni.
Se vuoi davvero una ricerca hash veloce, c'è un hack terribile e terribile che funziona davvero molto bene. È questo: scrivi la tua chiave / i valori in un file temporaneo, uno per riga, quindi usa 'grep "^ $ key"' per estrarli, usando pipe con cut o awk o sed o qualunque cosa per recuperare i valori.
Come ho detto, sembra terribile e sembra che dovrebbe essere lento e fare ogni sorta di IO non necessario, ma in pratica è molto veloce (la cache del disco è fantastica, non è vero?), Anche per hash molto grandi tabelle. Devi imporre te stesso l'unicità chiave, ecc. Anche se hai solo poche centinaia di voci, la combinazione di file di output / grep sarà un po 'più veloce - nella mia esperienza molte volte più veloce. Mangia anche meno memoria.
Ecco un modo per farlo:
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`
Il file system è una struttura ad albero che può essere utilizzata come mappa hash. La tua tabella hash sarà una directory temporanea, le tue chiavi saranno nomi di file e i tuoi valori saranno i contenuti del file. Il vantaggio è che può gestire enormi hashmap e non richiede una shell specifica.
hashtable=$(mktemp -d)
echo $value > $hashtable/$key
value=$(< $hashtable/$key)
Certo, è lento, ma non così lento. L'ho provato sulla mia macchina, con un SSD e btrfs , e fa circa 3000 elementi in lettura / scrittura al secondo .
mkdir -d
? (Non 4.3, su Ubuntu 14. Ricorrerei a mkdir /run/shm/foo
, o se quello avesse riempito la RAM mkdir /tmp/foo
mktemp -d
invece era destinato?
$value=$(< $hashtable/$key)
e value=$(< $hashtable/$key)
? Grazie!
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}
rimuove l' inizio del testo dall'inizio del valore memorizzato nella variabile var .
Prendere in considerazione una soluzione che utilizza la bash integrato di lettura , come illustrato nel frammento di codice da uno script firewall ufw che segue. Questo approccio ha il vantaggio di utilizzare tutti i set di campi delimitati (non solo 2) desiderati. Abbiamo usato il | delimitatore perché gli identificatori dell'intervallo di porte potrebbero richiedere due punti, ovvero 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"
Sono d'accordo con @lhunath e altri che l'array associativo è la strada da percorrere con Bash 4. Se sei bloccato su Bash 3 (OSX, vecchie distro che non puoi aggiornare) puoi usare anche expr, che dovrebbe essere ovunque, una stringa ed espressioni regolari. Mi piace soprattutto quando il dizionario non è troppo grande.
Scrivi la tua mappa come una stringa (nota il separatore ',' anche all'inizio e alla fine)
animals=",moo:cow,woof:dog,"
Utilizzare una regex per estrarre i valori
get_animal {
echo "$(expr "$animals" : ".*,$1:\([^,]*\),.*")"
}
Dividi la stringa per elencare gli elementi
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
}
Ora puoi usarlo:
$ animal = get_animal "moo"
cow
$ get_animal_items
cow likes to moo
dog likes to woof
Mi è piaciuta molto la risposta di Al P, ma volevo che l'unicità venisse rafforzata a buon mercato, quindi ho fatto un ulteriore passo avanti: utilizzare una directory. Esistono alcune ovvie limitazioni (limiti dei file di directory, nomi di file non validi) ma dovrebbe funzionare nella maggior parte dei casi.
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
Si comporta anche un po 'meglio nei miei test.
$ 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
Ho pensato di intervenire. Saluti!
Modifica: aggiunta di hdestroy ()
Due cose, puoi usare la memoria invece di / tmp in qualsiasi kernel 2.6 usando / dev / shm (Redhat) altre distribuzioni possono variare. Anche hget può essere reimplementato usando read come segue:
function hget {
while read key idx
do
if [ $key = $2 ]
then
echo $idx
return
fi
done < /dev/shm/hashmap.$1
}
Inoltre, supponendo che tutti i tasti siano univoci, il ritorno mette in corto circuito il ciclo di lettura e impedisce di leggere tutte le voci. Se l'implementazione può avere chiavi duplicate, semplicemente tralascia il ritorno. Ciò consente di risparmiare le spese di lettura e fork sia grep che awk. L'uso di / dev / shm per entrambe le implementazioni ha prodotto quanto segue usando time hget su un hash a 3 voci alla ricerca dell'ultima voce:
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
Lettura / echo:
$ time echo $(hget FD oracle)
3
real 0m0.004s
user 0m0.000s
sys 0m0.004s
su più invocazioni non ho mai visto meno di un miglioramento del 50%. Tutto ciò può essere attribuito a fork over head, a causa dell'uso di /dev/shm
.
Un collega ha appena menzionato questa discussione. Ho implementato le tabelle hash in modo indipendente all'interno di bash e non dipende dalla versione 4. Da un mio post sul blog nel marzo 2010 (prima di alcune delle risposte qui ...) intitolato Tabelle hash in bash :
In precedenza ho usato cksum
l'hash ma da allora ho tradotto la stringa Java hashCode in bash / zsh nativo.
# 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)
Non è bidirezionale e il modo integrato è molto meglio, ma nessuno dei due dovrebbe essere utilizzato comunque. Bash è per una rapida tantum, e cose del genere dovrebbero raramente comportare complessità che potrebbe richiedere hash, tranne forse nei tuoi ~/.bashrc
e amici.
Prima di bash 4 non esiste un buon modo per usare array associativi in bash. La tua scommessa migliore è usare un linguaggio interpretato che in realtà ha il supporto per cose del genere, come awk. D'altra parte, bash 4 non li supporta.
Per quanto riguarda i modi meno buoni in bash 3, ecco un riferimento che potrebbe aiutare: http://mywiki.wooledge.org/BashFAQ/006
Soluzione Bash 3:
Nel leggere alcune delle risposte ho messo insieme una piccola funzione veloce che vorrei dare un contributo che potrebbe aiutare gli altri.
# 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[@])
Ho anche usato il modo bash4 ma trovo e fastidioso bug.
Avevo bisogno di aggiornare dinamicamente il contenuto dell'array associativo, quindi ho usato in questo modo:
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
Scopro che con bash 4.3.11 l'aggiunta a una chiave esistente nel dict ha comportato l'aggiunta del valore se già presente. Quindi, ad esempio, dopo una certa ripetizione, il contenuto del valore era "checkKOcheckKOallCheckOK" e questo non era buono.
Nessun problema con bash 4.3.39 in cui ottenere una chiave esistente significa sostituire il valore attuale se già presente.
Ho risolto questo problema semplicemente pulendo / dichiarando l'array associativo statusCheck prima del ciclo:
unset statusCheck; declare -A statusCheck
Creo HashMaps in bash 3 usando variabili dinamiche. Ho spiegato come funziona nella mia risposta a: array associativi negli script Shell
Inoltre puoi dare un'occhiata a shell_map , che è un'implementazione di HashMap realizzata in bash 3.