Abbiamo richiesto uno script che simuli array associativi o una struttura dati simile a una mappa per Shell Scripting, qualsiasi corpo?
Abbiamo richiesto uno script che simuli array associativi o una struttura dati simile a una mappa per Shell Scripting, qualsiasi corpo?
Risposte:
Per aggiungere alla risposta di Irfan , ecco una versione più breve e più veloce di get()
poiché non richiede iterazione sui contenuti della mappa:
get() {
mapName=$1; key=$2
map=${!mapName}
value="$(echo $map |sed -e "s/.*--${key}=\([^ ]*\).*/\1/" -e 's/:SP:/ /g' )"
}
Un'altra opzione, se la portabilità non è la tua preoccupazione principale, è utilizzare array associativi incorporati nella shell. Questo dovrebbe funzionare in bash 4.0 (disponibile ora sulla maggior parte delle principali distribuzioni, anche se non su OS X a meno che non lo installi tu stesso), ksh e zsh:
declare -A newmap
newmap[name]="Irfan Zulfiqar"
newmap[designation]=SSE
newmap[company]="My Own Company"
echo ${newmap[company]}
echo ${newmap[name]}
A seconda della shell, potrebbe essere necessario fare un typeset -A newmap
invece di declare -A newmap
, o in alcuni potrebbe non essere affatto necessario.
test -z ${variable+x}
( x
non importa, potrebbe essere qualsiasi stringa). Per un array associativo in Bash, puoi fare lo stesso; utilizzare test -z ${map[key]+x}
.
Un altro modo 4 non bash.
#!/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"
Potresti anche lanciare un'istruzione if per cercare lì dentro. se [[$ var = ~ / blah /]]. o qualsiasi altra cosa.
Penso che sia necessario fare un passo indietro e pensare a cosa sia veramente una mappa, o array associativo. È solo un modo per memorizzare un valore per una determinata chiave e recuperarlo in modo rapido ed efficiente. Potresti anche voler essere in grado di iterare sulle chiavi per recuperare ogni coppia di valori-chiave o eliminare le chiavi e i valori associati.
Ora, pensa a una struttura dati che usi sempre nello scripting della shell, e anche solo nella shell senza scrivere uno script, che abbia queste proprietà. Perplesso? È il filesystem.
In realtà, tutto ciò di cui hai bisogno per avere un array associativo nella programmazione della shell è una directory temporanea. mktemp -d
è il tuo costruttore di array associativo:
prefix=$(basename -- "$0")
map=$(mktemp -dt ${prefix})
echo >${map}/key somevalue
value=$(cat ${map}/key)
Se non hai voglia di usare echo
e cat
, puoi sempre scrivere dei piccoli wrapper; questi sono modellati su quelli di Irfan, anche se restituiscono semplicemente il valore piuttosto che impostare variabili arbitrarie come $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
modifica : questo approccio è in realtà un po 'più veloce della ricerca lineare che utilizza sed suggerita dall'interrogante, oltre che più robusto (consente a chiavi e valori di contenere -, =, spazio, qnd ": SP:"). Il fatto che utilizzi il filesystem non lo rallenta; in realtà non è mai garantito che questi file vengano scritti sul disco a meno che non si chiami sync
; per file temporanei come questo con una breve durata, non è improbabile che molti di essi non vengano mai scritti su disco.
Ho eseguito alcuni benchmark del codice di Irfan, la modifica di Jerry del codice di Irfan e il mio codice, utilizzando il seguente programma di driver:
#!/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
I risultati:
$ time ./driver.sh irfan 10 5 0 m 0,975 s reali utente 0m0.280s sys 0m0.691s $ time ./driver.sh brian 10 5 0 m 0,226 reali utente 0m0.057s sys 0m0.123s $ time ./driver.sh jerry 10 5 0 m 0,706 s reali utente 0m0.228s sys 0m0.530s $ time ./driver.sh irfan 100 5 0 m 10,633 reali utente 0m4.366s sys 0m7.127s $ time ./driver.sh brian 100 5 0 m reali 1,682 s utente 0m0.546s sys 0m1.082s $ time ./driver.sh jerry 100 5 0 m 9,315 reali utente 0m4.565s sys 0m5.446s $ time ./driver.sh irfan 10500 1 m reale 46,197 s utente 0m44.869s sys 1m12.282s $ time ./driver.sh brian 10500 0 m 16.003 reali utente 0m5.135s sys 0m10.396s $ time ./driver.sh jerry 10500 1 m reale 24,414 s utente 0m39.696s sys 0m54.834s $ time ./driver.sh irfan 1000 5 4 m reali 25,145 s utente 3m17.286s sys 1m21.490s $ time ./driver.sh brian 1000 5 0 m 19,442 reali utente 0m5.287s sys 0m10.751s $ time ./driver.sh jerry 1000 5 5 m reali 29,136 s utente 4m48.926s sys 0m59.336s
Bash4 lo supporta in modo nativo. Non usare grep
o eval
, sono gli hack più brutti.
Per una risposta dettagliata e dettagliata con codice di esempio, vedere: /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; }'
}
Esempio:
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
Ora rispondo a questa domanda.
I seguenti script simulano array associativi negli script della shell. È semplice e molto facile da capire.
La mappa non è altro che una stringa infinita che ha keyValuePair salvato come --name = Irfan --designation = SSE --company = My: SP: Own: SP: Company
gli spazi sono sostituiti da ": SP:" per i valori
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
modifica: appena aggiunto un altro metodo per recuperare tutte le chiavi.
getKeySet() {
if [ "$#" != 1 ];
then
exit 1;
fi
mapName=$1;
eval map="\"\$$mapName\""
keySet=`
echo $map |
sed -e "s/=[^ ]*//g" -e "s/\([ ]*\)--/\1/g"
`
}
eval
i dati come se fossero codice bash, e per di più: non riesci a citarli correttamente. Entrambi causano masse di bug e iniezione di codice arbitrario.
Per Bash 3, c'è un caso particolare che ha una soluzione piacevole e semplice:
Se non vuoi gestire molte variabili o le chiavi sono semplicemente identificatori di variabili non validi e il tuo array è garantito per avere meno di 256 elementi , puoi abusare dei valori restituiti dalla funzione. Questa soluzione non richiede alcuna subshell in quanto il valore è prontamente disponibile come variabile, né alcuna iterazione in modo che le prestazioni urlano. Inoltre è molto leggibile, quasi come la versione Bash 4.
Ecco la versione più semplice:
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[$?]}
Ricorda, usa virgolette singole in case
, altrimenti è soggetto a globbing. Veramente utile per hash statici / congelati dall'inizio, ma si potrebbe scrivere un generatore di indici da un filehash_keys=()
array.
Attenzione, il valore predefinito è il primo, quindi potresti voler mettere da parte l'elemento zero:
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
Avvertenza: la lunghezza ora non è corretta.
In alternativa, se vuoi mantenere l'indicizzazione a base zero, puoi prenotare un altro valore di indice e proteggerti da una chiave inesistente, ma è meno leggibile:
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[$?]}
Oppure, per mantenere la lunghezza corretta, compensa l'indice di uno:
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))]}
Puoi usare nomi di variabili dinamici e lasciare che i nomi delle variabili funzionino come le chiavi di una hashmap.
Ad esempio, se hai un file di input con due colonne, nome, credito, come nell'esempio qui sotto, e vuoi sommare il reddito di ogni utente:
Mary 100
John 200
Mary 50
John 300
Paul 100
Paul 400
David 100
Il comando seguente sommerà tutto, utilizzando variabili dinamiche come chiavi, sotto forma di map _ $ {person} :
while read -r person money; ((map_$person+=$money)); done < <(cat INCOME_REPORT.log)
Per leggere i risultati:
set | grep map
L'output sarà:
map_David=100
map_John=500
map_Mary=150
map_Paul=500
Elaborando queste tecniche, sto sviluppando su GitHub una funzione che funziona proprio come un oggetto HashMap , shell_map .
Per creare " istanze HashMap " la funzione shell_map è in grado di creare copie di se stessa con nomi diversi. Ogni nuova copia della funzione avrà una variabile $ FUNCNAME diversa. $ FUNCNAME viene quindi utilizzato per creare uno spazio dei nomi per ciascuna istanza di Map.
Le chiavi della mappa sono variabili globali, nella forma $ FUNCNAME_DATA_ $ KEY, dove $ KEY è la chiave aggiunta alla mappa. Queste variabili sono variabili dinamiche .
Di seguito ne metterò una versione semplificata in modo da poterla utilizzare come esempio.
#!/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
}
Uso:
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!"
Ancora un altro modo non bash-4 (ovvero bash 3, compatibile con 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
stampe:
A1 => aaa
B2 => bbb
C3 => ccc
D4 => zzz
La funzione con case
agisce come un array associativo. Sfortunatamente non può usare return
, quindi deve il echo
suo output, ma questo non è un problema, a meno che tu non sia un purista che evita il fork delle subshell.
Peccato che non ho visto la domanda prima - ho scritto una libreria shell-framework che contiene tra le altre le mappe (array associativi). L'ultima versione può essere trovata qui .
Esempio:
#!/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")"
Ho trovato vero, come già accennato, che il metodo più performante è scrivere chiavi / vals in un file, quindi utilizzare grep / awk per recuperarli. Suona come ogni sorta di I / O non necessario, ma la cache del disco entra in gioco e lo rende estremamente efficiente, molto più velocemente rispetto al tentativo di memorizzarli in memoria usando uno dei metodi sopra (come mostrano i benchmark).
Ecco un metodo rapido e pulito che mi piace:
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`
Se si desidera applicare un valore singolo per chiave, è anche possibile eseguire una piccola azione grep / sed in hput ().
diversi anni fa ho scritto una libreria di script per bash che supportava array associativi tra le altre funzionalità (registrazione, file di configurazione, supporto esteso per argomenti della riga di comando, generazione di aiuto, test di unità, ecc.). La libreria contiene un wrapper per gli array associativi e passa automaticamente al modello appropriato (interno per bash4 ed emula per le versioni precedenti). Si chiamava shell-framework ed era ospitato su origo.ethz.ch ma oggi la risorsa è chiusa. Se qualcuno ne ha ancora bisogno, posso condividerlo con te.
Shell non ha una mappa incorporata come la struttura dei dati, uso una stringa grezza per descrivere elementi come questo:
ARRAY=(
"item_A|attr1|attr2|attr3"
"item_B|attr1|attr2|attr3"
"..."
)
quando si estraggono elementi e i suoi attributi:
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
Questo sembra non intelligente rispetto alla risposta di altre persone, ma facile da capire per le nuove persone da sgusciare.
Ho modificato la soluzione di Vadim con quanto segue:
####################################################################
# 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; }'
}
La modifica è map_get per evitare che restituisca errori se richiedi una chiave che non esiste, anche se l'effetto collaterale è che ignorerà anche silenziosamente le mappe mancanti, ma si adatta meglio al mio caso d'uso poiché ho appena voleva verificare la presenza di una chiave per saltare gli elementi in un ciclo.
Risposta tardiva, ma considera di affrontare il problema in questo modo, usando il builtin bash letto come illustrato nello snippet 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 utilizzato | delimitatore perché gli specificatori dell'intervallo di porte possono richiedere i due punti, ad esempio 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