Matrici multidimensionali in Bash

Sto pianificando uno script per gestire alcuni pezzi dei miei sistemi Linux e sono al punto di decidere se voglio usare bash o python .

Preferirei farlo come script Bash semplicemente perché i comandi sono più semplici, ma il vero fattore decisivo è la configurazione. Devo essere in grado di memorizzare un array multidimensionale nel file di configurazione per dire allo script cosa fare con se stesso. Memorizzare semplici coppie chiave = valore nei file di configurazione è abbastanza facile con bash, ma l’unico modo in cui posso pensare di fare un array multidimensionale è un motore di analisi a due strati, qualcosa del tipo

array=&d1|v1;v2;v3&d2|v1;v2;v3 

ma il codice marshall / unmarshall potrebbe diventare un orso e non è facile da usare per il prossimo povero fiacco che deve amministrarlo. Se non riesco a farlo facilmente in bash scriverò semplicemente le configurazioni in un file xml e scriverò lo script in python.

C’è un modo semplice per farlo in bash?

grazie a tutti.

Bash non supporta matrici multidimensionali, né hash, e sembra che tu voglia un hash che i valori siano matrici. Questa soluzione non è molto bella, una soluzione con un file xml dovrebbe essere migliore:

 array=('d1=(v1 v2 v3)' 'd2=(v1 v2 v3)') for elt in "${array[@]}";do eval $elt;done echo "d1 ${#d1[@]} ${d1[@]}" echo "d2 ${#d2[@]} ${d2[@]}" 

Questo è ciò che ha funzionato per me.

 # Define each array and then add it to the main one SUB_0=("name0" "value0") SUB_1=("name1" "value1") MAIN_ARRAY=( SUB_0[@] SUB_1[@] ) # Loop and print it. Using offset and length to extract values COUNT=${#MAIN_ARRAY[@]} for ((i=0; i<$COUNT; i++)) do NAME=${!MAIN_ARRAY[i]:0:1} VALUE=${!MAIN_ARRAY[i]:1:1} echo "NAME ${NAME}" echo "VALUE ${VALUE}" done 

È basato su questa risposta qui

Indipendentemente dalla shell utilizzata (sh, ksh, bash, …) il seguente approccio funziona piuttosto bene per gli array n-dimensionali (il campione copre un array bidimensionale).

Nel campione il separatore di riga (1a dimensione) è il carattere dello spazio. Per l’introduzione di un separatore di campo (2a dimensione) viene utilizzato lo strumento unix standard tr . Separatori aggiuntivi per ulteriori dimensioni possono essere utilizzati allo stesso modo.

Naturalmente le prestazioni di questo approccio non sono molto buone, ma se le prestazioni non sono un criterio questo approccio è abbastanza generico e può risolvere molti problemi:

 array2d="1.1:1.2:1.3 2.1:2.2 3.1:3.2:3.3:3.4" function process2ndDimension { for dimension2 in $* do echo -n $dimension2 " " done echo } function process1stDimension { for dimension1 in $array2d do process2ndDimension `echo $dimension1 | tr : " "` done } process1stDimension 

L’output di quell’esempio è simile a questo:

 1.1 1.2 1.3 2.1 2.2 3.1 3.2 3.3 3.4 

Bash non ha array multidimensionale. Ma puoi simulare un effetto un po ‘simile con gli array associativi. Quello che segue è un esempio di array associativo che finge di essere usato come array multidimensionale:

 declare -A arr arr[0,0]=0 arr[0,1]=1 arr[1,0]=2 arr[1,1]=3 echo "${arr[0,0]} ${arr[0,1]}" # will print 0 1 

Se non si dichiara l’array come associativo (con -A ), quanto sopra non funzionerà. Ad esempio, se si omette la riga declare -A arr , l’ echo stamperà 2 3 invece di 0 1 , poiché 0,0 , 1,0 e tale sarà preso come espressione aritmetica e valutato a 0 (il valore a destra dell’operatore virgola).

Dopo un sacco di tentativi ed errori ho trovato la migliore, più chiara e semplice matrice multidimensionale su bash è quella di usare una variabile regolare. Sì.

Vantaggi: non è necessario eseguire il ciclo di un grande array, è sufficiente echo “$ var” e utilizzare grep / awk / sed. È facile e chiaro e puoi avere tutte le colonne che vuoi.

Esempio:

 $ var=$(echo -e 'kris hansen oslo\nthomas jonson peru\nbibi abu johnsonville\njohnny lipp peru') $ echo "$var" kris hansen oslo thomas johnson peru bibi abu johnsonville johnny lipp peru 

Se vuoi trovare tutti in Perù

 $ echo "$var" | grep peru thomas johnson peru johnny lipp peru 

Solo grep (sed) nel terzo campo

 $ echo "$var" | sed -n -E '/(.+) (.+) peru/p' thomas johnson peru johnny lipp peru 

Se vuoi solo il campo x

 $ echo "$var" | awk '{print $2}' hansen johnson abu johnny 

Tutti in Perù si chiama thomas e restituiscono il suo cognome

 $ echo "$var" |grep peru|grep thomas|awk '{print $2}' johnson 

Qualche domanda a cui puoi pensare … superlativo.

Per cambiare un object:

 $ var=$(echo "$var"|sed "s/thomas/pete/") 

Per eliminare una riga che contiene “x”

 $ var=$(echo "$var"|sed "/thomas/d") 

Per modificare un altro campo nella stessa riga in base a un valore di un altro elemento

 $ var=$(echo "$var"|sed -E "s/(thomas) (.+) (.+)/\1 test \3/") $ echo "$var" kris hansen oslo thomas test peru bibi abu johnsonville johnny lipp peru 

Naturalmente il loop funziona anche se lo si vuole fare

 $ for i in "$var"; do echo "$i"; done kris hansen oslo thomas jonson peru bibi abu johnsonville johnny lipp peru 

L’unico trucco che abbiamo trovato con questo è che devi sempre citare la var (nell’esempio; sia var che i) o le cose appariranno come questa

 $ for i in "$var"; do echo $i; done kris hansen oslo thomas jonson peru bibi abu johnsonville johnny lipp peru 

e qualcuno dirà senza dubbio che non funzionerà se hai degli spazi nel tuo input, tuttavia ciò può essere risolto usando un altro delimitatore nel tuo input, ad esempio (usando un utf8 char ora per sottolineare che puoi scegliere qualcosa che il tuo input non contenere, ma puoi scegliere qualunque cosa c:)

 $ var=$(echo -e 'field one☥field two hello☥field three yes moin\nfield 1☥field 2☥field 3 dsdds aq') $ for i in "$var"; do echo "$i"; done field one☥field two hello☥field three yes moin field 1☥field 2☥field 3 dsdds aq $ echo "$var" | awk -F '☥' '{print $3}' field three yes moin field 3 dsdds aq $ var=$(echo "$var"|sed -E "s/(field one)☥(.+)☥(.+)/\1☥test☥\3/") $ echo "$var" field one☥test☥field three yes moin field 1☥field 2☥field 3 dsdds aq 

Se vuoi memorizzare newline nel tuo input, puoi convertire il newline in qualcos’altro prima dell’input e convertirlo di nuovo in output (o non usare bash …). Godere!

Espansione della risposta di Paolo: ecco la mia versione del lavoro con i sub-array associativi in ​​bash:

 declare -A SUB_1=(["name1key"]="name1val" ["name2key"]="name2val") declare -A SUB_2=(["name3key"]="name3val" ["name4key"]="name4val") STRING_1="string1val" STRING_2="string2val" MAIN_ARRAY=( "${SUB_1[*]}" "${SUB_2[*]}" "${STRING_1}" "${STRING_2}" ) echo "COUNT: " ${#MAIN_ARRAY[@]} for key in ${!MAIN_ARRAY[@]}; do IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]} echo "VALUE: " ${val[@]} if [[ ${#val[@]} -gt 1 ]]; then for subkey in ${!val[@]}; do subval=${val[$subkey]} echo "SUBVALUE: " ${subval} done fi done 

Funziona con valori misti nell'array principale: stringhe / matrici / assoc. array

La chiave qui è di racchiudere i sottotitoli tra virgolette singole e usare * invece di @ quando si memorizza un sottoarray all'interno dell'array principale in modo che venga memorizzato come una singola stringa separata da spazio: "${SUB_1[*]}"

Quindi semplifica l'analisi di un array al di fuori di ciò durante il looping dei valori con IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}

Il codice sopra riportato:

 COUNT: 4 VALUE: name1val name2val SUBVALUE: name1val SUBVALUE: name2val VALUE: name4val name3val SUBVALUE: name4val SUBVALUE: name3val VALUE: string1val VALUE: string2val 

Sto postando quanto segue perché è un modo molto semplice e chiaro per imitare (almeno in parte) il comportamento di un array bidimensionale in Bash. Usa un file qui (vedi il manuale di Bash) e read (un comando incorporato di Bash):

 ## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique) cat > physicists.$$ < 

Uscita: Il Physicist Wolfgang Pauli was born in 1900 Physicist Werner Heisenberg was born in 1901 Physicist Albert Einstein was born in 1879 Physicist Niels Bohr was born in 1885

Il modo in cui funziona:

  • Le linee nel file temporaneo creato svolgono il ruolo di vettori unidimensionali, in cui gli spazi vuoti (o qualsiasi altro carattere di separazione che scegli, vedi la descrizione del comando di read nel manuale di Bash) separano gli elementi di questi vettori.
  • Quindi, usando il comando di read con la sua opzione -a , eseguiamo il looping su ogni riga del file (finché non raggiungiamo la fine del file). Per ogni riga, possiamo assegnare i campi desiderati (= parole) a un array, che abbiamo dichiarato subito prima del ciclo. L'opzione -r del comando di read impedisce ai backslash di agire come caratteri di escape, nel caso in cui abbiamo digitato i backslash nei physicists.$$ documento qui physicists.$$ .

In conclusione, un file viene creato come un array 2D, ei suoi elementi vengono estratti usando un loop su ogni riga e usando la capacità del comando read di assegnare parole agli elementi di una matrice (indicizzata).

Lieve miglioramento:

Nel codice precedente, i physicists.$$ del file physicists.$$ sono dati come input per il ciclo while , in modo tale che venga effettivamente passato al comando di read . Tuttavia, ho scoperto che questo causa problemi quando ho un altro comando che chiede l'input all'interno del ciclo while . Ad esempio, il comando select attende input standard e, se inserito all'interno del ciclo while , prenderà input dai physicists.$$ , invece di richiedere la riga di comando per l'input dell'utente. Per correggere ciò, utilizzo l'opzione -u di read , che consente di leggere da un descrittore di file. Dobbiamo solo creare un descrittore di file (con il comando exec ) corrispondente ai physicists.$$ e darlo all'opzione -u di lettura, come nel seguente codice:

 ## Store the "two-dimensional data" in a file ($$ is just the process ID of the shell, to make sure the filename is unique) cat > physicists.$$ < 

Si noti che il descrittore di file è chiuso alla fine.

Lo faccio usando gli array associativi da bash 4 e impostando IFS su un valore che può essere definito manualmente.

Lo scopo di questo approccio è di disporre di matrici come valori delle chiavi di un array associativo.

Al fine di riportare IFS al valore predefinito, basta disinserirlo.

  • unset IFS

Questo è un esempio:

 #!/bin/bash set -euo pipefail # used as value in asscciative array test=( "x3:x4:x5" ) # associative array declare -A wow=( ["1"]=$test ["2"]=$test ) echo "default IFS" for w in ${wow[@]}; do echo " $w" done IFS=: echo "IFS=:" for w in ${wow[@]}; do for t in $w; do echo " $t" done done echo -e "\n or\n" for w in ${!wow[@]} do echo " $w" for t in ${wow[$w]} do echo " $t" done done unset IFS unset w unset t unset wow unset test 

L’output dello script seguente è:

 default IFS x3:x4:x5 x3:x4:x5 IFS=: x3 x4 x5 x3 x4 x5 or 1 x3 x4 x5 2 x3 x4 x5 

Ho una soluzione abbastanza semplice ma intelligente: basta definire la matrice con le variabili nel suo nome. Per esempio:

 for (( i=0 ; i<$(($maxvalue + 1)) ; i++ )) do for (( j=0 ; j<$(($maxargument + 1)) ; j++ )) do declare -a array$i[$j]=((Your rule)) done done 

Non so se questo aiuti dal momento che non è esattamente quello che hai chiesto, ma funziona per me. (Lo stesso potrebbe essere ottenuto solo con variabili senza l'array)

Bash non supporta array multidimensionali, ma possiamo implementare usando l’array Associate. Qui gli indici sono la chiave per recuperare il valore. L’array associato è disponibile in bash versione 4.

 #!/bin/bash declare -A arr2d rows=3 columns=2 for ((i=0;i 
 echo "Enter no of terms" read count for i in $(seq 1 $count) do t=` expr $i - 1 ` for j in $(seq $t -1 0) do echo -n " " done j=` expr $count + 1 ` x=` expr $j - $i ` for k in $(seq 1 $x) do echo -n "* " done echo "" done