Многомерные массивы в Bash

Я планирую script управлять некоторыми частями своих Linux-систем, и я должен решить, хочу ли я использовать bash или .

Я бы предпочел сделать это как Bash script просто потому, что команды проще, но реальным решающим фактором является конфигурация. Мне нужно иметь возможность хранить многомерный массив в файле конфигурации, чтобы сообщить script, что делать с самим собой. Хранение простых ключей = значений в конфигурационных файлах достаточно просто с помощью bash, но единственный способ, которым я могу думать о многомерном массиве, - это двухуровневый механизм синтаксического анализа, что-то вроде

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

но код marshall/unmarshall может стать медвежьим и его далеко не удобным для следующего бедного сока, который должен управлять этим. Если я не могу сделать это легко в Bash, я просто напишу конфиги в xml файл и напишу script в python.

Есть ли простой способ сделать это в bash?

Спасибо всем.

Ответ 1

Bash не поддерживает многомерные массивы, а также хеширует, и кажется, что вы хотите, чтобы хэш представлял собой массивы. Это решение не очень красивое, решение с XML файлом должно быть лучше:

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

Ответ 2

Bash не имеет многомерного массива. Но вы можете имитировать несколько схожий эффект с ассоциативными массивами. Ниже приведен пример ассоциативного массива, претендующего на использование в качестве многомерного массива:

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

Если вы не объявите массив как ассоциативный (с -A), вышеуказанное не будет работать. Например, если вы опустите строку declare -A arr, echo будет печатать 2 3 вместо 0 1, потому что 0,0, 1,0 и такие будут приняты как арифметическое выражение и оценены как 0 ( значение справа от оператора запятой).

Ответ 3

Это то, что сработало для меня.

# 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

Он основан на этом ответе здесь

Ответ 4

Независимо от используемой оболочки (sh, ksh, bash,...) следующий подход хорошо работает для n-мерных массивов (образец охватывает двумерный массив).

В образце разделитель строк (1-е измерение) является символом пробела. Для введения разделителя полей (2-го измерения) используется стандартный инструмент unix tr. Таким же образом можно использовать дополнительные разделители для дополнительных размеров.

Конечно, производительность этого подхода не очень хорошо, но если производительность не является критерием, этот подход является довольно общим и может решить многие проблемы:

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

Результат этого примера выглядит следующим образом:

1.1     1.2     1.3     
2.1     2.2     
3.1     3.2     3.3     3.4 

Ответ 5

После большого количества проб и ошибок я нахожу лучший, самый ясный и простой многомерный массив на bash, чтобы использовать обычный var. Да.

Преимущества: вам не нужно перебирать большой массив, вы можете просто эхо "$ var" и использовать grep/awk/sed. Это легко и понятно, и вы можете иметь столько столбцов, сколько хотите.

Пример:

$ 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

Если вы хотите найти всех в Перу

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

Только grep (sed) в третьем поле

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

Если вам нужно только поле x

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

Каждый в Перу, который называется thomas, и просто возвращает свое последнее имя

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

Любой запрос, который вы можете придумать... supereasy.

Чтобы изменить элемент:

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

Чтобы удалить строку, содержащую "x"

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

Чтобы изменить другое поле в той же строке на основе значения из другого элемента

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

Конечно, цикл также работает, если вы хотите сделать это

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

Единственное, что было найдено, это то, что вы всегда должны указывать var (в примере, как var, так и i), или все будет выглядеть так:

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

и кто-то, без сомнения, скажет, что это не сработает, если у вас есть пробелы на вашем входе, однако это можно устранить, используя другой разделитель на вашем входе, например (используя utf8 char теперь, чтобы подчеркнуть, что вы можете выбрать что-то ваш вход не будет содержать, но вы можете выбрать любой из 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

Если вы хотите сохранить новые строки на своем входе, вы можете преобразовать новую строку в нечто другое перед входом и преобразовать обратно на выходе (или не использовать bash...). Наслаждайтесь!

Ответ 6

Расширение ответа Павла - вот моя версия работы с ассоциативными подмассивами в 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

Он работает со смешанными значениями в основном массиве - strings/array/assoc. массивы

Ключ здесь состоит в том, чтобы обернуть подмассивы в одинарных кавычках и использовать * вместо @ при сохранении подмассива внутри основного массива, чтобы он мог быть сохранен как отдельная строка, разделенная пробелами: "${SUB_1[*]}"

Затем это упрощает анализ синтаксиса из этого массива при прохождении значений с помощью IFS=' ' read -a val <<< ${MAIN_ARRAY[$key]}

Вышеуказанный код:

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

Ответ 7

Я использую ассоциативные массивы с bash 4 и устанавливая IFS значение, которое можно определить вручную.

Целью этого подхода является наличие массивов в качестве значений ассоциативных ключей массива.

Чтобы установить IFS обратно по умолчанию, просто отмените его.

  • unset IFS

Это пример:

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

Вывод script ниже:

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

 or

  1
    x3
    x4
    x5
  2
    x3
    x4
    x5

Ответ 8

Я пишу следующее, потому что это очень простой и понятный способ имитировать (по крайней мере, до некоторой степени) поведение двумерного массива в Bash. Он использует здесь файл (см. Руководство по Bash) и read (встроенная команда 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.$$

Вывод: 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

Как это работает:

  • Строки во временном созданном файле играют роль одномерных векторов, где пробелы (или любой другой символ разделения, который вы выбираете; см. Описание команды read в руководстве по Bash) разделяют элементы этих векторов.
  • Затем, используя команду read с -a, мы зацикливаемся на каждой строке файла (пока не достигнем конца файла). Для каждой строки мы можем присвоить нужные поля (= слова) массиву, который мы объявили непосредственно перед циклом. Опция -r команды read позволяет -r выступать в качестве escape-символов, если мы ввели обратные слэши в physicists.$$ приведенных здесь.

В заключение, файл создается как 2D -a массив, и его элементы извлекаются с использованием цикла над каждой строкой и использованием способности команды read назначать слова элементам (индексированного) массива.

Незначительное улучшение:

В приведенном выше коде, файл physicists.$$ Дается в качестве вклада в while циклы, так что это на самом деле передается на read команды. Тем не менее, я обнаружил, что это вызывает проблемы, когда у меня есть другая команда задайте внутри в while цикла. Например, select команду ждет стандартного ввода, и если их поместить внутри в while цикла, он будет принимать входные данные из physicists.$$, а не вызвав в командной строке для ввода данных пользователя. Чтобы исправить это, я использую -u read, которая позволяет читать из файлового дескриптора. Нам нужно только создать файловый дескриптор (с командой exec), соответствующий physicists.$$ и передать его опции чтения -u, как -u в следующем коде:

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

Обратите внимание, что дескриптор файла закрывается в конце.

Ответ 9

Если вы хотите использовать сценарий bash и сделать его легким для чтения, порекомендуйте поместить данные в структурированный JSON, а затем использовать облегченный инструмент jq в команде bash для итерации по массиву. Например, со следующим набором данных:

[

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

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

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

Вы можете перебрать эти данные с помощью bash-скрипта и jq следующим образом:

function loopOverArray(){

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

loopOverArray

Выходы:

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

Ответ 10

У меня есть довольно простой, но умный обходной путь: просто определите массив с переменными в его имени. Например:

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

Не знаю, помогает ли это, поскольку это не совсем то, что вы просили, но это работает для меня. (То же самое может быть достигнуто только с переменными без массива)

Ответ 11

Bash не поддерживает многомерный массив, но мы можем реализовать его с помощью Associate array. Здесь индексы являются ключом для получения значения. Ассоциированный массив доступен в 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

Ответ 12

Здесь можно найти много ответов для создания многомерных массивов в bash.

И все без исключения тупые и сложные в использовании.

Если MD-массивы являются обязательными критериями, пришло время принять решение:

Используйте язык, который поддерживает массивы MD

Я предпочитаю Perl. Большинство, вероятно, выберет Python. Либо работает.

Храните данные в другом месте

JSON и jq уже были предложены. Также был предложен XML, хотя для вас JSON и jq, вероятно, будут проще.

Казалось бы, что Bash может быть не лучшим выбором для того, что вам нужно делать.

Иногда правильный вопрос не "Как сделать X в инструменте Y?", А скорее "Какой инструмент лучше всего сделать X?"

Ответ 13

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