arrays - Conjuntos multidimensionales en Bash
shell multidimensional-array (11)
Estoy planeando un script para administrar algunas piezas de mis sistemas Linux y estoy a punto de decidir si quiero usar bash o python .
Preferiría hacer esto como un script Bash simplemente porque los comandos son más fáciles, pero el factor decisivo es la configuración. Necesito poder almacenar una matriz multidimensional en el archivo de configuración para decirle al script qué hacer consigo mismo. Almacenar pares simples de clave = valor en archivos de configuración es bastante fácil con bash, pero la única forma en que se me ocurre hacer una matriz multidimensional es un motor de análisis de dos capas, algo así como
array=&d1|v1;v2;v3&d2|v1;v2;v3
pero el código marshall / unmarshall podría llegar a ser un oso y está lejos de ser fácil de usar para la próxima pobre savia que tiene que administrar esto. Si no puedo hacer esto fácilmente en bash, simplemente escribiré las configuraciones en un archivo xml y escribiré el script en python.
¿Hay alguna manera fácil de hacer esto en bash?
gracias a todos.
Ampliando la respuesta de Paul: esta es mi versión de trabajar con sub-arreglos asociativos en 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
Funciona con valores mixtos en la matriz principal - strings / arrays / assoc. matrices
La clave aquí es ajustar los subárboles en comillas simples y usar *
lugar de @
cuando se almacena un subarreglo dentro de la matriz principal para que se almacene como una sola cadena separada de espacio: "${SUB_1[*]}"
Luego hace que sea fácil analizar un conjunto de eso al pasar por los valores con IFS='' '' read -a val <<< ${MAIN_ARRAY[$key]}
Los resultados del código anterior:
COUNT: 4
VALUE: name1val name2val
SUBVALUE: name1val
SUBVALUE: name2val
VALUE: name4val name3val
SUBVALUE: name4val
SUBVALUE: name3val
VALUE: string1val
VALUE: string2val
Bash no admite matrices multidimensionales, ni hashes, y parece que quiere un hash que los valores sean matrices. Esta solución no es muy bonita, una solución con un archivo xml debería ser mejor:
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[@]}"
Bash no es compatible con una matriz multidimensional, pero podemos implementar el uso de una matriz asociada. Aquí los índices son la clave para recuperar el valor. Associate array está disponible en la versión bash
4.
#!/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
Bash no tiene matriz multidimensional. Pero puede simular un efecto algo similar con matrices asociativas. El siguiente es un ejemplo de matriz asociativa que pretende ser utilizada como matriz multidimensional:
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
Si no declaras la matriz como asociativa (con -A
), lo anterior no funcionará. Por ejemplo, si omite la línea declare -A arr
, el echo
imprimirá 2 3
lugar de 0 1
, porque 0,0
, 1,0
y tal se tomará como expresión aritmética y se evaluará a 0
(el valor a la derecha) del operador de coma).
Después de muchas pruebas y errores, en realidad encuentro la matriz multidimensional mejor, más clara y más fácil en bash es usar una var. Sí.
Ventajas: no tiene que recorrer una gran matriz, solo puede repetir "$ var" y usar grep / awk / sed. Es fácil y claro y puedes tener tantas columnas como quieras.
Ejemplo:
$ 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
Si quieres encontrar a todos en perú
$ echo "$var" | grep peru
thomas johnson peru
johnny lipp peru
Solo grep (sed) en el tercer campo
$ echo "$var" | sed -n -E ''/(.+) (.+) peru/p''
thomas johnson peru
johnny lipp peru
Si solo quieres el campo x
$ echo "$var" | awk ''{print $2}''
hansen
johnson
abu
johnny
Todos en Perú que se llama Thomas y solo devuelven su apellido
$ echo "$var" |grep peru|grep thomas|awk ''{print $2}''
johnson
Cualquier consulta que se te ocurra ... superosa.
Para cambiar un artículo:
$ var=$(echo "$var"|sed "s/thomas/pete/")
Para eliminar una fila que contiene "x"
$ var=$(echo "$var"|sed "/thomas/d")
Para cambiar otro campo en la misma fila en función de un valor de otro 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
Por supuesto, el bucle también funciona si quieres hacer eso
$ for i in "$var"; do echo "$i"; done
kris hansen oslo
thomas jonson peru
bibi abu johnsonville
johnny lipp peru
Lo único que encontré con esto es que siempre debes citar la var (en el ejemplo: var e i) o las cosas se verán así.
$ for i in "$var"; do echo $i; done
kris hansen oslo thomas jonson peru bibi abu johnsonville johnny lipp peru
y sin duda alguien dirá que no funcionará si tiene espacios en su entrada, sin embargo eso se puede arreglar usando otro delímetro en su entrada, por ejemplo, (usando un carácter utf8 ahora para enfatizar que puede elegir algo que su entrada no contener, pero puede elegir cualquier 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
Si desea almacenar nuevas líneas en su entrada, puede convertir la nueva línea en otra cosa antes de la entrada y convertirla nuevamente en la salida (o no use bash ...). ¡Disfrutar!
Esto es lo que funcionó para mí.
# 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
Está basado en esta respuesta aquí
Estoy publicando lo siguiente porque es una forma muy simple y clara de imitar (al menos hasta cierto punto) el comportamiento de una matriz bidimensional en Bash. Utiliza un archivo aquí (ver el manual Bash) y read
(un comando incorporado 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.$$ <<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.$$
Salida: 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
La forma en que funciona:
- Las líneas en el archivo temporal creado juegan el papel de vectores unidimensionales, donde los espacios en blanco (o cualquier carácter de separación que elija, vea la descripción del comando de
read
en el manual de Bash) separan los elementos de estos vectores. - Luego, utilizando el comando de
read
con su opción-a
, recorremos cada línea del archivo (hasta que lleguemos al final del archivo). Para cada línea, podemos asignar los campos deseados (= palabras) a una matriz, que declaramos justo antes del bucle. La opción-r
del comando deread
evita que las barras invertidas actúen como caracteres de escape, en caso de que escribamos barras invertidas en losphysicists.$$
documento aquíphysicists.$$
.
En conclusión, un archivo se crea como una matriz 2D, y sus elementos se extraen utilizando un ciclo sobre cada línea, y utilizando la capacidad del comando de read
para asignar palabras a los elementos de una matriz (indexada).
Mejoria leve:
En el código anterior, el archivo physicists.$$
se da como entrada al ciclo while, de modo que de hecho pasa al comando de read
. Sin embargo, descubrí que esto causa problemas cuando tengo otro comando que me solicita entrada dentro del ciclo while. Por ejemplo, el comando de select
espera la entrada estándar, y si se coloca dentro del ciclo while, tomará la entrada de los physicists.$$
, en lugar de solicitar en la línea de comandos para la entrada del usuario. Para corregir esto, uso la opción -u
de read
, que permite leer desde un descriptor de archivo. Solo tenemos que crear un descriptor de archivo (con el comando exec
) correspondiente a los physicists.$$
y dárselo a la opción -u
de lectura, como en el siguiente código:
## 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.$$
Tenga en cuenta que el descriptor de archivo está cerrado al final.
Independientemente del shell que se use (sh, ksh, bash, ...) el siguiente enfoque funciona bastante bien para matrices n-dimensionales (la muestra cubre una matriz bidimensional).
En la muestra, el separador de línea (1ª dimensión) es el carácter de espacio. Para introducir un separador de campo (2da dimensión) se usa la herramienta tr
unix estándar. Se pueden usar separadores adicionales para dimensiones adicionales de la misma manera.
Por supuesto, el rendimiento de este enfoque no es muy bueno, pero si el rendimiento no es un criterio, este enfoque es bastante genérico y puede resolver muchos problemas:
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
El resultado de esa muestra se ve así:
1.1 1.2 1.3
2.1 2.2
3.1 3.2 3.3 3.4
Lo hago usando matrices asociativas desde bash 4 y estableciendo IFS
en un valor que se puede definir manualmente.
El objetivo de este enfoque es tener matrices como valores de claves de matriz asociativas.
Para volver a establecer el IFS en modo predeterminado, simplemente desconéctelo.
-
unset IFS
Esto es un ejemplo:
#!/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
El resultado del script a continuación es:
default IFS
x3:x4:x5
x3:x4:x5
IFS=:
x3
x4
x5
x3
x4
x5
or
1
x3
x4
x5
2
x3
x4
x5
Tengo una solución bastante simple pero inteligente: simplemente defina la matriz con variables en su nombre. Por ejemplo:
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
No sé si esto ayuda, ya que no es exactamente lo que pediste, pero funciona para mí. (Lo mismo se puede lograr solo con variables sin la matriz)
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