Matrizes multidimensionais no Bash

Estou planejando um script para gerenciar algumas partes de meus sistemas Linux e estou no ponto de decidir se quero usar bash ou python .

Eu preferiria fazer isso como um script Bash simplesmente porque os comandos são mais fáceis, mas o fator decisivo real é a configuração. Eu preciso ser capaz de armazenar uma multidimensional array no arquivo de configuração para dizer ao script o que fazer com ele mesmo. Armazenar chaves simples = pares de valores em arquivos de configuração é bastante fácil com o bash, mas a única maneira de pensar em fazer uma multidimensional array é um mecanismo de análise de duas camadas, algo como

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

mas o código marshall / unmarshall pode ser um urso e está longe de ser amigável para o próximo coitado que tem que administrar isso. Se eu não puder fazer isso facilmente no bash, simplesmente gravarei as configurações em um arquivo xml e gravarei o script em python.

Existe uma maneira fácil de fazer isso em bash?

Obrigado a todos.

Bash não suporta matrizes multidimensionais, nem hashes, e parece que você quer um hash que os valores são matrizes. Esta solução não é muito bonita, uma solução com um arquivo xml deve ser melhor:

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

Isto é o que funcionou para mim.

 # 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 

É baseado nesta resposta aqui

Independente do shell que está sendo usado (sh, ksh, bash, …) a seguinte abordagem funciona muito bem para arrays n-dimensionais (a amostra cobre um array bidimensional).

Na amostra, o separador de linha (primeira dimensão) é o caractere de espaço. Para introduzir um separador de campo (2ª dimensão) é usada a ferramenta unix standard tr . Separadores adicionais para dimensões adicionais podem ser usados ​​da mesma maneira.

É claro que o desempenho dessa abordagem não é muito bom, mas se o desempenho não for um critério, essa abordagem é bastante genérica e pode resolver muitos 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 

A saída desse exemplo se parece com isto:

 1.1 1.2 1.3 2.1 2.2 3.1 3.2 3.3 3.4 

O Bash não possui um array multidimensional. Mas você pode simular um efeito semelhante com matrizes associativas. A seguir, um exemplo de matriz associativa que pretende ser usada como multidimensional array:

 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 você não declarar o array como associativo (com -A ), o acima não funcionará. Por exemplo, se você omitir a linha declare -A arr , o echo imprimirá 2 3 vez de 0 1 , porque 0,0 , 1,0 e tal será tomado como expressão aritmética e avaliado como 0 (o valor à direita do operador vírgula).

Depois de muita tentativa e erro eu realmente acho que o array multidimensional melhor, mais claro e mais fácil no bash é usar um var regular. Sim.

Vantagens: Você não precisa fazer um loop através de um grande array, você pode apenas fazer um eco em “$ var” e usar grep / awk / sed. É fácil e claro e você pode ter quantas colunas quiser.

Exemplo:

 $ 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 você quiser encontrar todo mundo no peru

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

Apenas grep (sed) no terceiro campo

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

Se você quer apenas o campo x

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

Todo mundo no peru é chamado de Thomas e acaba de retornar seu sobrenome

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

Qualquer consulta que você possa imaginar … supereasy.

Para alterar um item:

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

Para excluir uma linha que contenha “x”

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

Para alterar outro campo na mesma linha com base em um valor de outro item

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

Claro que o looping funciona também se você quiser fazer isso

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

A única coisa que consegui encontrar com isso é que você deve sempre citar o var (no exemplo; var e i) ou as coisas ficarão assim

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

e alguém, sem dúvida, dirá que não funcionará se você tiver espaços em sua input, mas isso pode ser corrigido usando outro delimitador em sua input, por exemplo (usando um caractere utf8 agora para enfatizar que você pode escolher algo que sua input não conter, mas você pode escolher o que for ofc):

 $ 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 você quiser armazenar novas linhas na sua input, você poderia converter a nova linha para outra coisa antes da input e convertê-la novamente na saída (ou não usar bash …). Apreciar!

Expandindo a resposta de Paul – aqui está minha versão de trabalhar com sub-arrays associativos no 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 

Ele trabalha com valores mistos na matriz principal - strings / arrays / assoc. matrizes

A chave aqui é envolver os subarrays entre aspas simples e usar * vez de @ ao armazenar um subarray dentro do array principal para que ele seja armazenado como uma única string separada por espaço: "${SUB_1[*]}"

Em seguida, facilita a análise de uma matriz quando o loop através de valores com IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}

O código acima gera:

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

Estou postando o seguinte porque é uma maneira muito simples e clara de imitar (pelo menos até certo ponto) o comportamento de um array bidimensional no Bash. Ele usa um arquivo here (veja o manual do Bash) e read (um comando Bash builtin):

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

Saída: O 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

A maneira como isso funciona:

  • As linhas no arquivo temporário criado desempenham o papel de vetores unidimensionais, em que os espaços em branco (ou qualquer caractere de separação escolhido; consulte a descrição do comando de read no manual do Bash) separam os elementos desses vetores.
  • Então, usando o comando read com sua opção -a , fazemos um loop sobre cada linha do arquivo (até chegarmos ao final do arquivo). Para cada linha, podemos atribuir os campos desejados (= palavras) a um array, que declaramos logo antes do loop. A opção -r para o comando read evita que as barras invertidas atuem como caracteres de escape, no caso de termos typescript barras invertidas nos physicists.$$ documento aqui physicists.$$ .

Em conclusão, um arquivo é criado como uma matriz 2D e seus elementos são extraídos usando um loop sobre cada linha e usando a capacidade do comando read para atribuir palavras aos elementos de uma matriz (indexada).

Leve melhoria:

No código acima, o arquivo physicists.$$ é dado como input para o loop while, de modo que é de fato passado para o comando read . No entanto, descobri que isso causa problemas quando eu tenho outro comando pedindo input dentro do loop while. Por exemplo, o comando select aguarda a input padrão e, se colocado dentro do loop while, receberá uma input dos physicists.$$ , em vez de solicitar na linha de comando a input do usuário. Para corrigir isso, eu uso a opção -u de read , que permite ler de um descritor de arquivo. Nós só temos que criar um descritor de arquivo (com o comando exec ) correspondente aos physicists.$$ e dar à opção -u de leitura, como no código a seguir:

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

Observe que o descritor de arquivo é fechado no final.

Eu faço isso usando matrizes associativas desde o bash 4 e configurando o IFS para um valor que pode ser definido manualmente.

O objective dessa abordagem é ter matrizes como valores de chaves de matriz associativa.

Para definir o IFS de volta ao padrão, basta desmarcá-lo.

  • unset IFS

Isto é um exemplo:

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

A saída do script abaixo é:

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

Eu tenho uma solução bastante simples, mas inteligente: basta definir o array com variables ​​em seu nome. Por exemplo:

 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 

Não sei se isso ajuda, já que não é exatamente o que você pediu, mas funciona para mim. (O mesmo pode ser alcançado apenas com variables ​​sem o array)

O Bash não suporta array multidimensional, mas podemos implementar usando o array Associate. Aqui os índices são a chave para recuperar o valor. O array associado está disponível na versão 4 do bash .

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