wake-up-neo.net

Mehrdimensionale Arrays in Bash

Ich plane ein Skript, um einige Teile meiner Linux-Systeme zu verwalten, und bin gerade dabei zu entscheiden, ob ich bash oder python verwenden möchte.

Ich würde es vorziehen, dies als Bash-Skript zu tun, einfach weil die Befehle einfacher sind, aber der entscheidende Faktor ist die Konfiguration. Ich muss in der Lage sein, ein mehrdimensionales Array in der Konfigurationsdatei zu speichern, um dem Skript mitzuteilen, was es mit sich selbst tun soll. Das Speichern von einfachen key = value-Paaren in Konfigurationsdateien ist mit bash einfach genug, aber die einzige Möglichkeit, ein mehrdimensionales Array zu erstellen, ist eine zweischichtige Parsing-Engine

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

aber der Marshall/Unmarshall-Code könnte zu einem Bären werden und es ist alles andere als benutzerfreundlich für den nächsten armen Saft, der dies verwalten muss. Wenn ich dies nicht leicht in bash tun kann, schreibe ich einfach die configs in eine xml-Datei und schreibe das Skript in python. 

Gibt es eine einfache Möglichkeit, dies in bash zu tun?

danke an alle.

39
scphantm

Bash unterstützt keine mehrdimensionalen Arrays oder Hashes, und es scheint, dass Sie einen Hash wünschen, bei dem Werte Arrays sind. Diese Lösung ist nicht sehr schön, eine Lösung mit einer XML-Datei sollte besser sein:

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[@]}"
38

Bash hat kein mehrdimensionales Array. Sie können jedoch einen ähnlichen Effekt mit assoziativen Arrays simulieren. Das folgende Beispiel zeigt ein assoziatives Array, das vorgibt, als mehrdimensionales Array verwendet zu werden:

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

Wenn Sie das Array nicht als assoziativ deklarieren (mit -A), funktioniert das obige nicht. Wenn Sie beispielsweise die Zeile declare -A arr weglassen, wird echo anstelle von 2 30 1 ausgegeben, da 0,0, 1,0 und dergleichen als arithmetischer Ausdruck verwendet und als 0 (Wert rechts vom Kommaoperator) ausgewertet wird.

17
Jahid

Das hat bei mir funktioniert.

# 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

Es basiert auf diese Antwort hier

13
Paul Sheldrake

Unabhängig von der verwendeten Shell (sh, ksh, bash, ...) funktioniert der folgende Ansatz für n-dimensionale Arrays ziemlich gut (das Beispiel deckt ein zweidimensionales Array ab). 

Im Beispiel ist das Zeilentrennzeichen (1. Dimension) das Leerzeichen. Zur Einführung eines Feldtrennzeichens (2. Dimension) wird das Standard-Unix-Tool tr verwendet. Zusätzliche Trennzeichen für zusätzliche Abmessungen können auf dieselbe Weise verwendet werden.

Natürlich ist die Leistung dieses Ansatzes nicht sehr gut, aber wenn Leistung kein Kriterium ist, ist dieser Ansatz recht allgemein und kann viele Probleme lösen:

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

Die Ausgabe dieses Beispiels sieht folgendermaßen aus:

1.1     1.2     1.3     
2.1     2.2     
3.1     3.2     3.3     3.4 
8
yaccob

Nach vielen Versuchen und Fehlern finde ich tatsächlich das beste, klarste und einfachste mehrdimensionale Array auf bash, wenn man eine reguläre var verwendet. Ja.

Vorteile: Sie müssen kein großes Array durchlaufen, Sie können einfach "$ var" wiederholen und grep/awk/sed verwenden. Es ist einfach und übersichtlich und Sie können so viele Spalten haben, wie Sie möchten.

Beispiel:

$ 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

Wenn Sie alle in Peru finden wollen

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

Nur grep (sed) im dritten Feld

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

Wenn Sie nur x-Feld möchten

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

Jeder in Peru heißt thomas und gibt einfach seinen Nachnamen zurück

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

Jede Frage, die Sie sich vorstellen können ... superleicht.

So ändern Sie einen Artikel:

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

So löschen Sie eine Zeile, die "x" enthält

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

So ändern Sie ein anderes Feld in derselben Zeile basierend auf einem Wert eines anderen Elements

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

Natürlich funktioniert das Looping auch, wenn Sie das wollen

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

Das einzige, was dabei herausgefunden wurde, ist, dass Sie müssen immer die Var (im Beispiel; sowohl var als auch i) angeben, oder die Dinge werden so aussehen

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

und jemand wird zweifellos sagen, dass es nicht funktionieren wird, wenn Sie Leerzeichen in Ihrer Eingabe haben. Dies kann jedoch durch Verwendung eines anderen Begrenzungszeichens in Ihrer Eingabe behoben werden, z enthalten, aber Sie können wählen, was auch immer ausc):

$ 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

Wenn Sie Zeilenumbrüche in Ihrer Eingabe speichern möchten, können Sie die Zeilenumbrüche vor der Eingabe in etwas anderes konvertieren und bei der Ausgabe wieder zurückwandeln (oder bash nicht verwenden ...). Genießen!

4
n00p

Auf Pauls Antwort eingehen - hier ist meine Version der Arbeit mit assoziativen Sub-Arrays 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

Es arbeitet mit gemischten Werten im Hauptarray - strings/arrays/assoc. Arrays

Der Schlüssel hier ist, die Subarrays in einfache Anführungszeichen zu setzen und * anstelle von @ zu verwenden, wenn ein Subarray innerhalb des Hauptarrays gespeichert wird, damit es als einzelne, durch Leerzeichen getrennte Zeichenfolge gespeichert wird: "${SUB_1[*]}"

Dann ist es einfach, ein Array daraus zu analysieren, wenn Werte mit IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]} durchlaufen werden.

Der Code oben gibt aus:

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

Ich poste folgendes, weil es eine sehr einfache und klare Möglichkeit ist, das Verhalten eines zweidimensionalen Arrays in Bash (zumindest in gewissem Maße) nachzuahmen. Es verwendet eine Here-Datei (siehe das Bash-Handbuch) und read (einen eingebauten Bash-Befehl):

## 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.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person; do
    firstName=${person[0]}
    familyName=${person[1]}
    birthYear=${person[2]}
    echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
    # Do whatever you need with data
done < physicists.$$

## Remove the temporary file
rm physicists.$$

Ausgabe: 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

So funktioniert es:

  • Die Zeilen in der erstellten temporären Datei spielen die Rolle von eindimensionalen Vektoren, wobei die Leerzeichen (oder das von Ihnen gewählte Trennzeichen; siehe Beschreibung des Befehls read im Bash-Handbuch) die Elemente dieser Vektoren voneinander trennen.
  • Mit dem Befehl read und seiner Option -a durchlaufen wir dann jede Zeile der Datei (bis zum Ende der Datei). Für jede Zeile können wir die gewünschten Felder (= Wörter) einem Array zuordnen, das wir unmittelbar vor der Schleife deklariert haben. Die -r-Option des read-Befehls verhindert, dass Backslashes als Escape-Zeichen fungieren, falls wir im here-document physicists.$$ Backslashes eingegeben haben.

Zusammenfassend wird eine Datei als 2D-Array erstellt, und ihre Elemente werden mithilfe einer Schleife über jede Zeile extrahiert und mithilfe des Befehls read den Elementen eines (indizierten) Arrays Wörter zugewiesen.

Leichte Verbesserung:

Im obigen Code wird die Datei physicists.$$ als Eingabe in die while-Schleife eingegeben, sodass sie tatsächlich an den read-Befehl übergeben wird. Ich habe jedoch festgestellt, dass dies Probleme verursacht, wenn ein anderer Befehl zur Eingabe in die while-Schleife auffordert. Zum Beispiel wartet der Befehl select auf die Standardeingabe, und wenn er in die while-Schleife eingefügt wird, wird er die Eingabe von physicists.$$ annehmen, anstatt in der Befehlszeile zur Eingabe von Benutzereingaben aufzufordern . Um dies zu korrigieren, verwende ich die Option -u von read, das das Lesen von einem Dateideskriptor ermöglicht. Wir müssen nur einen Dateideskriptor erstellen (mit dem Befehl exec), der physicists.$$ entspricht, und ihn der -u-Option von read zuweisen, wie im folgenden Code:

## 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.$$ <<EOF
Wolfgang Pauli 1900
Werner Heisenberg 1901
Albert Einstein 1879
Niels Bohr 1885
EOF
nbPhysicists=$(wc -l physicists.$$ | cut -sf 1 -d ' ')     # Number of lines of the here-file specifying the physicists.
exec {id_nuclei}<./physicists.$$     # Create a file descriptor stored in 'id_nuclei'.

## Extract the needed data
declare -a person     # Create an indexed array (necessary for the read command).                                                                                 
while read -ra person -u "${id_nuclei}"; do
    firstName=${person[0]}
    familyName=${person[1]}
    birthYear=${person[2]}
    echo "Physicist ${firstName} ${familyName} was born in ${birthYear}"
    # Do whatever you need with data
done

## Close the file descriptor
exec {id_nuclei}<&-
## Remove the temporary file
rm physicists.$$

Beachten Sie, dass der Dateideskriptor am Ende geschlossen ist.

1
Giuseppe

Wenn Sie ein Bash-Skript verwenden und es übersichtlich halten möchten, empfehlen Sie, die Daten in strukturiertem JSON abzulegen, und verwenden Sie dann leichtes Tool jq in Ihrem Bash-Befehl, um das Array zu durchlaufen. Zum Beispiel mit folgendem Datensatz:

[

    {"specialId":"123",
    "specialName":"First"},

    {"specialId":"456",
    "specialName":"Second"},

    {"specialId":"789",
    "specialName":"Third"}
]

Sie können diese Daten mit einem Bash-Skript und jq wie folgt durchlaufen:

function loopOverArray(){

    jq -c '.[]' testing.json | while read i; do
        # Do stuff here
        echo "$i"
    done
}

loopOverArray

Ausgänge:

{"specialId":"123","specialName":"First"}
{"specialId":"456","specialName":"Second"}
{"specialId":"789","specialName":"Third"}
1
Dustin

Hier finden Sie viele Antworten zum Erstellen mehrdimensionaler Arrays in bash.

Und ohne Ausnahme sind alle stumpf und schwer zu bedienen.

Wenn MD-Arrays ein erforderliches Kriterium sind, ist es Zeit, eine Entscheidung zu treffen:

Verwenden Sie eine Sprache, die MD-Arrays unterstützt

Ich bevorzuge Perl. Die meisten würden sich wahrscheinlich für Python entscheiden. Entweder funktioniert.

Speichern Sie die Daten an einer anderen Stelle

JSON und jq wurden bereits vorgeschlagen. XML wurde ebenfalls vorgeschlagen, obwohl für Ihre Verwendung JSON und jq wahrscheinlich einfacher wären.

Es scheint jedoch, dass Bash nicht die beste Wahl für das ist, was Sie tun müssen.

Manchmal lautet die richtige Frage nicht "Wie mache ich X in Werkzeug Y?", Sondern "Welches Werkzeug ist am besten für X geeignet?"

0
Jared Still

Bash unterstützt kein mehrdimensionales Array, wir können es jedoch mit einem assoziierten Array implementieren. Hier sind die Indizes der Schlüssel zum Abrufen des Werts. Ein assoziiertes Array ist in bash Version 4 verfügbar.

#!/bin/bash

declare -A arr2d
rows=3
columns=2

for ((i=0;i<rows;i++)) do
    for ((j=0;j<columns;j++)) do
        arr2d[$i,$j]=$i
    done
done


for ((i=0;i<rows;i++)) do
    for ((j=0;j<columns;j++)) do
        echo ${arr2d[$i,$j]}
    done
done
0
rashok

Ich habe eine ziemlich einfache, aber intelligente Lösung: Definieren Sie das Array einfach mit Variablen im Namen. Zum Beispiel:

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

Ich weiß nicht, ob das hilft, da es nicht genau das ist, wonach Sie gefragt haben, aber es funktioniert für mich. (Das gleiche kann nur mit Variablen ohne das Array erreicht werden.)

0
testiner

Ich mache dies mit assoziativen Arrays since bash 4 und setzeIFSauf einen Wert, der manuell definiert werden kann.

Der Zweck dieses Ansatzes besteht darin, Arrays als Werte assoziativer Array-Schlüssel zu haben.

Um IFS auf die Standardeinstellungen zurückzusetzen, setzen Sie es einfach auf.

  • unset IFS

Dies ist ein Beispiel:

#!/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

Die Ausgabe des folgenden Skripts lautet:

default IFS
  x3:x4:x5
  x3:x4:x5
IFS=:
  x3
  x4
  x5
  x3
  x4
  x5

 or

  1
    x3
    x4
    x5
  2
    x3
    x4
    x5
0
rocksteady