Conjuntos multidimensionales en Bash

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.

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

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í

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 

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!

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 

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.$$ < 

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 de read evita que las barras invertidas actúen como caracteres de escape, en caso de que escribamos barras invertidas en los physicists.$$ 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.$$ < 

Tenga en cuenta que el descriptor de archivo está cerrado al final.

Lo hago usando matrices asociativas desde bash 4 y estableciendo IFS en un valor que se puede definir manualmente.

El objective 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)

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