Тип данных массива Bash обеспечивает большую гибкость при создании сценариев оболочки.

Bash предоставляет два типа массивов: индексированные массивы и ассоциативные массивы. Индексированные массивы — это стандартные массивы, в которых каждый элемент идентифицируется числовым индексом. В ассоциативных массивах каждый элемент представляет собой пару ключ-значение (аналогично словарям в других языках программирования).

В этом уроке мы начнем со знакомства с индексированными массивами, а затем увидим, чем от них отличаются ассоциативные массивы (у них также мало общего).

К концу этого урока вы развеете все сомнения, которые могли у вас возникнуть относительно массивов в Bash.

А также вы узнаете много интересных вещей, которые можно делать в скриптах оболочки с помощью массивов.

Давайте начнем!

Индексированный массив строк Bash

Начнем с создания индексированного массива строк, где строки представляют собой имена каталогов в системе Linux:

dirs=("/etc" "/var" "/opt" "/tmp")

Сначала давайте посмотрим, что будет напечатано, когда мы выводим на экран значение переменной массива dirs:

$ echo $dirs
/etc 

При печати переменной массива Bash результатом является первый элемент массива.

Другой способ вывести первый элемент массива — обратиться к массиву на основе его индекса.

Индексированные массивы Bash начинаются с нуля, это значит, что для доступа к первому элементу нам придется использовать нулевой индекс.

$ echo ${dirs[0]}
/etc

Интересно, почему мы используем фигурные скобки?

Мы можем понять почему, удалив их и посмотрев, что получится на выходе:

$ echo $dirs[0]
/etc[0]

Bash выводит первый элемент массива, за которым следует [0], поскольку он распознает только $dirs как переменную. Чтобы включить [0] как часть имени переменной, мы должны использовать фигурные скобки.

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

$ echo ${dirs[1]}
/var

Что, если мы хотим получить доступ к последнему элементу массива?

Прежде чем это сделать, нам нужно выяснить, как получить длину массива Bash…

Как определить длину массива Bash?

Чтобы найти длину массива в Bash, нам нужно использовать синтаксис ${#array_name[@]}.

Давайте применим это к нашему примеру:

$ echo ${#dirs[@]}
4

Синтаксис может показаться сложным для запоминания, когда вы видите его впервые…

…но не волнуйтесь, просто потренируйтесь несколько раз, и вы запомните.

Доступ к последнему элементу массива Bash

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

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

${#dirs[@]}-1

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

$ echo ${dirs[${#dirs[@]}-1]}
/tmp

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

Начиная с Bash 4.2 массивы также принимают отрицательные индексы, которые позволяют получать доступ к элементам, начиная с конца массива.

Чтобы проверить вашу версию Bash, используйте следующую команду:

$ bash --version

Для доступа к последнему элементу индексированного массива Bash можно использовать индекс -1 (для Bash 4.2 или более поздней версии). В противном случае используйте следующее выражение ${array_name[${#array_name[@]}-1]}.

$ dirs=("/etc" "/var" "/opt" "/tmp")
$ echo ${dirs[-1]}
/tmp

Как и ожидалось, мы получаем последний элемент.

Как распечатать все значения в массиве Bash

Чтобы вывести все элементы массива, нам по-прежнему нужно использовать квадратные скобки и заменить индекс символом @:

$ echo ${dirs[@]}
/etc /var /opt /tmp

Альтернативой @ является знак *:

$ echo ${dirs[*]}
/etc /var /opt /tmp

Почему существует два способа сделать одно и то же?

В чем разница между * и @ при использовании для вывода всех элементов массива Bash?

Мы увидим это позже, после того как покажем вам, как использовать цикл for для перебора всех элементов массива…

Как обновить элемент массива Bash

Как же нам обновить элемент в нашем массиве?

Мы будем использовать следующий синтаксис:

array_name[index]=new_value

В нашем случае я хочу установить значение второго элемента (индекс равен 1) на «/usr».

$ dirs[1]="/usr"
$ echo ${dirs[@]}
/etc /usr /opt /tmp

Цикл по элементам массива Bash

Давайте выясним, как создать цикл for, который проходит по всем элементам массива:

for dir in ${dirs[@]}; do
    echo "Directory name: $dir"
done

Вывод:

Directory name: /etc
Directory name: /var
Directory name: /opt
Directory name: /tmp

Возвращаясь к разнице между * и @, что произойдет, если мы заменим ${dirs[@]} на ${dirs[*]}?

for dir in ${dirs[*]}; do
    echo "Directory name: $dir"
done

Никакой разницы…

Directory name: /etc
Directory name: /var
Directory name: /opt
Directory name: /tmp

Разница становится очевидной, если мы заключим два выражения в двойные кавычки.

С использованием @

for dir in "${dirs[@]}"; do
    echo "Directory name: $dir"
done

[output]
Directory name: /etc
Directory name: /var
Directory name: /opt
Directory name: /tmp

С использованием *

for dir in "${dirs[*]}"; do
    echo "Directory name: $dir"
done

[output]
Directory name: /etc /var /opt /tmp

Вы можете видеть, что когда мы используем *, наш массив интерпретируется как одно значение.

Цикл For с использованием индексов массива Bash

Давайте попробуем что-нибудь еще…

Мы будем использовать следующее выражение:

${!array_name[@]}

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

Давайте посмотрим, что произойдет, когда мы это сделаем.

$ echo ${!dirs[@]}
0 1 2 3

На этот раз вместо того, чтобы вывести все элементы массива, мы вывели все индексы.

Выражение ${!array_name[@]} используется для печати всех индексов массива Bash.

Как вы можете себе представить, мы можем использовать это для создания цикла for, который вместо того, чтобы проходить по всем элементам массива, проходит по всем индексам массива. От 0 до длины массива минус 1:

for index in ${!dirs[@]}; do
    echo "Directory name: ${dirs[$index]}"
done

Убедитесь, что вывод идентичен тому, который мы видели, пройдясь по всем элементам массива, а не по всем индексам.

Мы также можем распечатать индекс для каждого элемента, если он нам нужен:

for index in ${!dirs[@]}; do
    echo "Index: $index - Directory name: ${dirs[$index]}"
done

Использование Declare для индексированных массивов

Мы создали наш индексированный массив следующим образом:

dirs=("/etc" "/var" "/opt" "/tmp")

Ниже вы можете увидеть еще два способа создания индексированных массивов:

Вариант 1

Определите пустой массив и задайте его элементы по одному:

dirs=()
dirs[0]="/etc"
dirs[1]="/var"
dirs[2]="/opt"
dirs[3]="/tmp"
echo ${dirs[@]}

[output]
/etc /var /opt /tmp

Вариант 2

Использование встроенной команды Bash declare с флагом -a:

declare -a dirs

Добавление элементов в индексированный массив Bash

Чтобы добавить элемент к существующему массиву, можно использовать следующий синтаксис:

existingArray+=("newValue")

Например:

$ dirs=("/etc" "/var" "/opt" "/tmp")
$ dirs+=("/bin")
$ echo ${dirs[@]}
/etc /var /opt /tmp /bin

А как насчет добавления более одного элемента?

Вот как это можно сделать…

$ dirs+=("/bin" "/usr")
$ echo ${dirs[@]}
/etc /var /opt /tmp /bin /usr

Имеет ли это смысл?

Как удалить элемент из массива

Чтобы удалить элемент из массива, можно использовать unset:

$ dirs=("/etc" "/var" "/opt" "/tmp")
$ unset dirs[2]
$ echo ${dirs[@]}
/etc /var /tmp

Обратите внимание, как третий элемент массива (обозначенный индексом 2) был удален из массива.

Вы также можете использовать unset для удаления всего массива:

$ unset dirs
$ echo ${dirs[@]}

Убедитесь, что последняя команда echo не возвращает никаких выходных данных.

Краткое описание операций с массивами Bash

Прежде чем перейти к ассоциативным массивам, я хочу дать вам краткий обзор рассмотренных нами операций с массивами в Bash.

СинтаксисОписание
array=()Создать пустой массив
declare -a массивСоздайте пустой индексированный массив с помощью declare
array=(1 2 3 4 5)Инициализируем массив из пяти элементов
${array[0]}Доступ к первому элементу массива
${array[1]}Доступ ко второму элементу массива
${dirs[${#dirs[@]}-1]}Доступ к последнему элементу массива
${array[@]}Получить все элементы массива
${!array[@]}Получить все индексы массива
array+=(6 7)Добавить два значения в массив
array[2]=10Присвоить значение третьему элементу массива
${#array[@]}Получить размер массива
${#array[n]}Получить длину n-го элемента

Прежде чем продолжить изучение этого урока, попрактикуйтесь во всех командах из этой таблицы.

Многие операции в таблице применимы также к ассоциативным массивам.

Инициализация ассоциативного массива Bash

Ассоциативные массивы можно определить только с помощью команды declare.

Как мы уже видели, для создания индексированного массива можно также использовать следующий синтаксис:

declare -a new_array

Чтобы создать ассоциативный массив, измените флаг, переданный команде declare, используя флаг -A:

$ declare -A new_array
$ new_array=([key1]=value1 [key2]=value2)
$ echo ${new_array[@]}
value2 value1

Обратите внимание, что порядок элементов в ассоциативных массивах Bash не соблюдается в отличие от индексированных массивов.

Если у вас есть массив с большим количеством элементов, также может помочь написание команд, которые назначают пары ключ/значение массиву следующим образом:

new_array=(
    [key1]=value1
    [key2]=value2
)

Как использовать цикл For с ассоциативным массивом Bash

Синтаксис циклов for для ассоциативных массивов практически идентичен тому, что мы видели для индексированных массивов.

Мы будем использовать восклицательный знак для получения ключей массива, а затем выведем каждое значение, сопоставленное с ключом:

for key in ${!new_array[@]}; do
    echo "Key: $key - Value: ${new_array[$key]}"
done

Вывод:

Key: key2 - Value: value2
Key: key1 - Value: value1

Видите ли вы, как каждый ключ используется для извлечения соответствующего значения?

Удалить элемент из ассоциативного массива

Давайте посмотрим, как можно удалить элемент из ассоциативного массива…

Следующая команда удаляет элемент, идентифицированный ключом key1, из ассоциативного массива, который мы определили ранее.

$ unset new_array[key1]

Убедитесь, что вы получаете следующий вывод при выполнении цикла for, который мы видели в предыдущем разделе:

Key: key2 - Value: value2

Чтобы удалить весь массив, можно использовать тот же синтаксис, который мы видели для индексированных массивов:

unset new_array

В следующих нескольких разделах будут показаны некоторые полезные операции, которые вы можете выполнять с массивами Bash в рамках повседневного написания скриптов…

Удалить дубликаты из массива

Вы когда-нибудь задумывались, как удалить дубликаты из массива?

Для этого мы могли бы использовать цикл for, который создает новый массив, содержащий только уникальные значения.

Но вместо этого я хочу найти более лаконичное решение.

Мы будем использовать четыре команды Linux, следуя приведенным ниже инструкциям:

  1. Вывести все элементы массива с помощью echo.
  2. Используйте tr для замены пробелов на новые строки. Это выведет все элементы на отдельных строках.
  3. Отправьте вывод предыдущего шага командам sort и uniq с помощью каналов.
  4. Создайте новый массив из выходных данных команды, созданной на данный момент, используя подстановку команд.

Это исходный массив и выходные данные, описанные до шага 3:

$ numbers=(1 2 3 2 4 6 5 6)
$ echo ${numbers[@]} | tr ' ' '\n' | sort | uniq
1
2
3
4
5
6

Теперь давайте используем подстановку команд, как описано в шаге 4, чтобы назначить этот вывод новому массиву. Мы назовем новый массив unique_numbers:

$ unique_numbers=($(echo ${numbers[@]} | tr ' ' '\n' | sort | uniq))

Следующий цикл for выводит все элементы нового массива:

for number in ${unique_numbers[@]}; do
    echo $number
done

Вывод правильный!

1
2
3
4
5
6

Интересно, работает ли это также для массива строк…

$ words=("bash" "array" "bash" "command" "bash" "shell" "associative")
$ unique_words=($(echo ${words[@]} | tr ' ' '\n' | sort | uniq))
$ for word in ${unique_words[@]}; do echo $word; done

Обратите внимание, что мы записали цикл Bash for в одну строку.

Вот вывод. Это работает и для массива строк…

array
associative
bash
command
shell

В этом примере мы также увидели, как сортировать массив.

Проверьте, содержит ли массив Bash строку

Чтобы проверить, содержит ли массив определенную строку, мы можем использовать echo и tr таким же образом, как мы делали в предыдущем разделе.

Затем мы отправляем вывод команде grep, чтобы подтвердить, соответствует ли какой-либо из элементов массива искомой строке.

Вот как это работает, если, например, мы ищем строку «command»:

$ words=("array" "associative" "bash" "command" "shell")
$ echo ${words[@]} | tr ' ' '\n' | grep "command"
command

Мы можем использовать флаг -q для grep, чтобы избежать печати любого вывода. Единственное, что нам нужно, это код выхода команды, сохраненный в переменной $?.

Затем мы можем использовать оператор if else для проверки значения $?

echo ${words[@]} | tr ' ' '\n' | grep -q "command"

if [ $? -eq 0 ]; then
    echo "String found in the array."
else
    echo "String not found in the array."
fi

Таким образом мы проверяем, есть ли в массиве элемент, равный «command».

Таким же образом мы можем узнать, есть ли ключ у ассоциативного массива Bash.

Мы просто заменим ${words[@]} на ${!words[@]}, чтобы вывести все ключи вместо значений.

Попробуйте!

Массив файлов Bash в каталоге

Я хочу показать вам еще один пример того, как сгенерировать массив из вывода команды.

Это то, что вам определенно пригодится при создании сценариев.

Мы создадим массив из вывода команды ls, выполненной в текущем каталоге:

$ files=($(ls -A))
$ echo ${files[@]}
.hidden_file1 test_file1 test_file2

Еще раз обратите внимание, как мы используем подстановку команд для назначения вывода команды элементам массива.

Как перевернуть массив в Bash

Мы можем использовать команду, очень похожую на ту, которая используется для удаления дубликатов из массива, также и для реверсирования массива.

Единственное отличие состоит в том, что мы также будем использовать команду Linux tac (противоположную cat), чтобы перевернуть строки, которые мы получаем из элементов массива:

$ words=("array" "associative" "bash" "command" "shell")
$ reversed_words=($(echo ${words[@]} | tr ' ' '\n' | tac))
$ echo ${reversed_words[@]}
shell command bash associative array

Имеет ли это смысл?

Как скопировать индексированный массив Bash

Вот как можно скопировать индексированный массив в Bash.

Дан следующий массив:

words=("array" "bash" "command line" "shell")

Я могу создать копию с помощью следующей команды:

array_copy=("${words[@]}") 

С помощью цикла for мы можем подтвердить элементы внутри копии массива:

for element in "${array_copy[@]}"; do
    echo $element
done

[output]
array
bash
command line
shell

Нарезка массива Bash

Иногда вам может понадобиться просто получить часть массива.

Срез — это, по сути, определенное количество элементов, начинающихся с определенного индекса.

Вот общий синтаксис, который вы могли бы использовать:

${array[@]:index:number_of_elements}

Давайте проверим это выражение на следующем массиве:

words=("array" "bash" "command line" "shell")

Два элемента, начиная с индекса 1

$ echo ${words[@]:1:2}
bash command line 

Один элемент, начиная с индекса 0

$ echo ${words[@]:0:1}
array 

Три элемента, начиная с индекса 0

$ echo ${words[@]::3}
array bash command line 

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

$ echo ${words[@]:1}
bash command line shell 

Поиск и замена элемента массива

В какой-то момент вам может понадобиться найти и заменить элемент с определенным значением…

…вот как это можно сделать:

echo ${array[@]/value_to_search/replace_with_this_value}

В нашем массиве я хочу заменить слово bash на слово linux:

$ words=("array" "bash" "command line" "shell")
$ echo ${words[@]/bash/linux}
array linux command line shell 

Очень удобно!

Интересно, сработает ли это, если элемент, который мы хотим заменить, встречается несколько раз…

$ words=("array" "bash" "command line" "shell" "bash")
$ echo ${words[@]/bash/linux}
array linux command line shell linux 

Оно работает!

Как объединить два массива Bash

Я хочу объединить следующие два массива:

commands1=("cd" "cat" "echo" "grep")
commands2=("sort" "rm" "top" "awk")

Я могу создать новый массив в результате слияния двух массивов:

all_commands=("${commands1[@]}" "${commands2[@]}")

Давайте проверим значения и количество элементов в этом массиве:

$ echo ${all_commands[@]}
cd cat echo grep sort rm top awk
$ echo ${#all_commands[@]}
8 

Большой!

Проверьте, пуст ли массив Bash

Зачем проверять массив Bash на пустоту?

Существует множество сценариев, в которых это может быть полезно. Одним из примеров является использование массива для хранения всех ошибок, обнаруженных в вашем скрипте.

В конце вашего скрипта вы проверяете количество элементов в этом массиве и в зависимости от этого выводите сообщение об ошибке или нет.

Мы будем использовать массив с именем errors и оператор Bash if else, который проверяет количество элементов в массиве.

В этом примере я создам массив ошибок с одним элементом:

errors=("File not found")
 
if [ ${#errors[@]} -eq 0 ]; then
    echo "No errors found."
else
    echo "WARNING - Number of errors found: ${#errors[@]}"
fi

При запуске скрипта я получаю следующий вывод:

WARNING - Number of errors found: 1 

Хороший способ отслеживать ошибки в ваших скриптах!

Создать массив Bash из диапазона чисел

Как создать массив, элементами которого являются числа от 1 до 100?

Мы сделаем это следующим образом:

  • Создайте пустой массив.
  • Используйте цикл for, чтобы добавить в массив числа от 1 до 100.
numbers=() 

for value in {1..100}; do
    numbers+=($value)
done 

echo ${numbers[@]} 

Попробуйте и убедитесь, что скрипт выводит числа от 1 до 100.

Мы также можем проверить количество элементов в массиве:

$ echo ${#numbers[@]}
100

Как реализовать логику Push Pop для массивов

Дан индексированный массив строк:

words=("array" "bash" "command line" "shell") 

Я хочу реализовать логику push-pop…

…где push добавляет элемент в конец массива, а pop удаляет последний элемент из массива.

Начнем с push, нам просто нужно будет добавить элемент, как мы видели ранее:

$ words+=("filesystem")
$ echo ${words[@]}
array bash command line shell filesystem 

Логика извлечения получает значение последнего элемента, а затем удаляет его из массива:

$ last_element=${words[-1]}
$ echo $last_element 
filesystem
$ unset words[-1]
$ echo ${words[@]}
array bash command line shell 

Вы также можете обернуть эти команды в две функции Bash, чтобы просто вызывать push() и pop() вместо того, чтобы дублировать приведенный выше код каждый раз, когда он вам нужен.

Ошибка неверного индекса массива

В какой-то момент работы над этим уроком я столкнулся со следующей ошибкой:

./arrays.sh: line 4: dirs: bad array subscript

Я выполнял следующий скрипт:

#!/bin/bash

dirs=("/etc" "/var" "/opt" "/tmp")
echo dirs ${dirs[-1]}

Судя по всему, в четвертой строке сценария не было ничего неправильного.

Как мы видели в одном из предыдущих разделов, индекс -1 можно использовать для доступа к последнему элементу массива.

После небольшого поиска и устранения неисправностей я понял, что проблема была вызвана…

…версия Bash, запущенная на моей машине!

$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
Copyright (C) 2007 Free Software Foundation, Inc.

В версии 3 Bash не поддерживал отрицательные индексы для массивов и, как поясняется в разделе этой статьи «Доступ к последнему элементу массива Bash», возможны альтернативные решения.

Другой вариант — обновить версию Bash, если она поддерживается вашей операционной системой.

Давайте рассмотрим другой сценарий, в котором может возникнуть эта ошибка…

Вот еще один сценарий:

#!/bin/bash

declare -A my_array=([]="a" [key2]="b")

Как вы видите, я использую встроенную функцию declare для создания ассоциативного массива (я использую флаг -A, как описано в одном из разделов выше).

При запуске скрипта я вижу следующую ошибку:

./array_error.sh: line 3: []="a": bad array subscript

На этот раз, как видно из сообщения, ошибка вызвана тем, что я пытаюсь добавить в массив элемент с пустым ключом.

Это еще одна причина, по которой может возникнуть данная ошибка.

Итак, теперь вы знаете две различные причины ошибки «неправильный индекс массива», и если вы видите ее в своих скриптах, у вас есть способ ее понять.

Заключение

Мы так много всего охватили в этой записи блога!

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

Давайте сделаем краткий обзор рассмотренных нами тем. Возможно, вы заметите что-то, к чему захотите вернуться и повторить.

Мы увидели, как:

  • Определите индексированные и ассоциативные массивы.
  • Определите длину массива.
  • Доступ к элементам на основе индексов (для индексированных массивов) и ключей (для ассоциативных массивов).
  • Выведите все элементы, используя @ или *.
  • Обновить элементы массива.
  • Цикл по массиву Bash, используя либо элементы, либо индексы.
  • Создавайте индексированные и ассоциативные массивы с помощью встроенной функции declare .
  • Добавить элементы в существующий массив.
  • Удалить элементы из массива или удалить весь массив.
  • Удалить дубликаты из массива.
  • Проверьте, содержит ли массив элемент, соответствующий определенной строке.
  • Реверс, копирование и получение среза массива.
  • Поиск и замена строки в массивах.
  • Объедините два массива и проверьте, является ли массив пустым.
  • Создайте массив из диапазона чисел.
  • Реализуйте логику push/pop для массивов Bash.
  • Разберитесь с ошибкой «неправильный индекс массива».

Теперь пришло время использовать массивы Bash…

…но прежде чем закончить, я задам вам вопрос, чтобы проверить ваши знания.

Дан следующий массив:

declare -A my_array=([key1]="value1" [key2]="value2" [key3]="value3")
  1. Какой это тип массива? Индексированный или ассоциативный?
  2. Как можно распечатать ключи этого массива?

Удачного написания сценариев! 🙂

Written by Иван Васильков

Системный администратор и DevOps с опытом 10+ лет.