Когда переменных в скрипте становится много и они логически связаны, рождается соблазн плодить имена с приставками: адрес сервера один, адрес сервера два, адрес базы. Скрипт обрастает похожими именами, в которых легко запутаться, а обойти их разом нет простого способа. Между тем bash давно предлагает аккуратное решение - массивы, и не один их вид, а два. Индексные хранят упорядоченный список, ассоциативные - пары ключ-значение, как словарь в других языках.

Разница между ними не сводится к синтаксису. Она в самой природе ключа: у одних это целое число и позиция в списке, у других - произвольная строка, по которой достаётся значение. Понимание, какой вид под какую задачу, избавляет от россыпи переменных с приставками и делает код читаемым. Разберём оба типа, их объявление, обход и ловушки, на которых спотыкаются при работе с ними.

Индексный массив хранит упорядоченный список по числовым позициям

Индексный массив - простейший и самый привычный. Элементы лежат в нём по порядку, каждому соответствует целое число-позиция, отсчёт идёт с нуля. Это поведение знакомо по массивам почти любого языка.

фрукты=("яблоко" "банан" "вишня")
echo "${фрукты[0]}"      # яблоко - первый элемент
echo "${фрукты[@]}"      # все элементы разом
echo "${#фрукты[@]}"     # 3 - длина массива

Объявить индексный массив можно несколькими путями. Прямое присваивание списка в скобках - самый ходовой. Можно объявить и явно через команду объявления с флагом строчной буквы, но чаще обходятся без этого. Более того, индексный массив возникает неявно - стоит присвоить значение по числовому индексу несуществующей переменной, и она становится массивом.

declare -a овощи          # явное объявление
сервера[0]="веб"          # неявное: переменная стала массивом

У индексных массивов есть любопытное свойство - разреженность. Индексы не обязаны идти подряд без пропусков. Можно присвоить значения позициям ноль и пять, оставив промежуток пустым, и массив это спокойно стерпит. Пропущенные позиции просто не существуют, что иногда удобно, а иногда становится источником сюрпризов при подсчёте длины.

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

Ассоциативный массив достаёт значение по строковому ключу

Ассоциативный массив устроен иначе. Вместо числовых позиций у него строковые ключи, и значение достаётся по имени ключа, а не по номеру. В других языках это зовут словарём, хэш-таблицей или картой. Каждый ключ уникален и указывает на своё значение.

declare -A человек
человек[имя]="Иван"
человек[возраст]="30"
человек[город]="Москва"
echo "${человек[имя]}"      # Иван - достали по ключу

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

Тут кроется важнейшее отличие в объявлении, на котором спотыкаются почти все. Ассоциативный массив невозможно создать неявно. Его обязательно объявлять заранее командой объявления с флагом заглавной буквы A. Без этого объявления присваивание по строковому ключу не создаст ассоциативный массив - оболочка истолкует строковый ключ как арифметическое выражение, сведёт его к нулю, и получится обычный индексный массив с единственным элементом в нулевой позиции.

# БЕЗ declare -A - тихая ошибка
цвета[red]="#ff0000"        # red воспринят как ноль!
цвета[green]="#00ff00"      # green тоже ноль, перезаписал

# С declare -A - всё верно
declare -A цвета
цвета[red]="#ff0000"
цвета[green]="#00ff00"

Эта ошибка коварна тем, что не выдаёт себя сообщением. Скрипт просто работает не так, как задумано: все значения схлопываются в одну позицию, потому что строковые ключи без объявления превратились в нули. Заглавная буква в флаге объявления - граница между ассоциативным и индексным массивом, и забыть её значит молча получить не тот тип.

Ассоциативные массивы доступны начиная с четвёртой версии bash. В более старых оболочках их попросту нет, что стоит учитывать при переносе скриптов.

Обход массива требует особого синтаксиса для ключей и значений

Перебрать массив целиком - частая задача, и здесь синтаксис разводит ключи и значения. Все значения достаются через знак собачки в квадратных скобках. А вот все ключи или индексы - через восклицательный знак перед именем массива, что легко спутать с косвенным разворачиванием, хотя смысл тут другой.

declare -A сервера=( [веб]="192.168.1.10" [база]="192.168.1.20" )

# Обход по ключам и значениям
for ключ in "${!сервера[@]}"; do
    echo "$ключ -> ${сервера[$ключ]}"
done

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

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

Ассоциативный массив не хранит порядок вставки

Ещё одна ловушка, на которую натыкаются, ожидая от ассоциативного массива поведения упорядоченного списка. Порядок элементов в нём не сохраняется. Ни порядок вставки, ни алфавитный, ни какой-либо предсказуемый.

declare -A цвета=( [red]="#ff0000" [green]="#00ff00" [blue]="#0000ff" )
echo "${цвета[@]}"
# вывод может идти в любом порядке, не в порядке вставки

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

Когда порядок важен, есть два пути. Либо отсортировать ключи отдельной командой сортировки перед обходом, либо вообще использовать индексный массив, у которого порядок позиций жёстко определён. Это разделение по требованию к порядку - один из главных критериев выбора между двумя типами.

Добавление, удаление и просмотр устройства массива

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

declare -A пример
пример[ключ1]="значение1"
пример+=( [новый_ключ]="новое значение" )    # добавление пары

Удаление элемента или всего массива делается командой снятия значения. А чтобы увидеть полное устройство массива - его тип, все ключи и значения разом - служит команда объявления с флагом печати. Она показывает, индексный массив перед нами или ассоциативный, что бесценно при отладке, когда непонятно, какой тип на самом деле получился.

declare -p сервера        # печатает тип, ключи и значения массива

Команда печати - первое, к чему стоит обращаться, если массив ведёт себя странно. Она сразу выдаёт, не превратился ли ассоциативный массив в индексный из-за забытого объявления, и какие ключи в нём реально лежат.

Что выбрать под конкретную задачу

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

Ассоциативный массив - для данных вида ключ-значение, где элементы осмысленно именованы, а не пронумерованы: настройки по именам, соответствия стран и столиц, кэш по ключам. Он требует обязательного объявления командой с флагом заглавной A, иначе молча выродится в индексный. Порядок в нём не сохраняется из-за хэш-таблицы внутри, и при нужде в порядке ключи сортируют отдельно.

Главное, что стоит унести: тип ключа определяет всё. Целое число и позиция - индексный массив. Произвольная строка и имя - ассоциативный, но только с явным объявлением. Стоит запомнить, что заглавная A в объявлении - это рубильник между двумя типами, а восклицательный знак при обходе даёт ключи, а не значения, и работа с массивами перестаёт преподносить тихие сюрпризы. Один массив со строковыми ключами чище и понятнее, чем десяток переменных с приставками в именах.