Многомерные массивы в Bash
Я планирую script управлять некоторыми частями своих Linux-систем, и я должен решить, хочу ли я использовать bash или python.
Я бы предпочел сделать это как 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 обратно по умолчанию, просто отмените его.
Это пример:
#!/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