Передача массивов и ассоциативных отображений между функциями
Безопасно передавайте сложные структуры данных в функции и из них с помощью ссылок на имена и косвенного раскрытия
«Передача массивов и ассоциативных отображений между функциями» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.
Почему передача массивов в функции — непростая задача
В Bash массивы и ассоциативные массивы не являются полноценными значениями. При попытке передать массив в функцию нельзя просто использовать $my_array — это развернёт только первый элемент.
Рассмотрим неработающую попытку:
my_func "${my_array[@]}"Функция получает плоский список слов. Она не может определить, где заканчивается один элемент и начинается следующий, и полностью теряет структуру массива.
Bash предоставляет два надёжных механизма для передачи сложных структур данных:
- Именованные ссылки (
declare -n) — ссылка на другую переменную по имени (Bash 4.3 и новее) - Косвенное развёртывание (
${!varname[@]}) — доступ к переменной, имя которой хранится в другой переменной (в старых версиях Bash)
В этом уроке рассматриваются оба приёма, чтобы Вы могли писать надёжные, повторно используемые функции, работающие с массивами и отображениями.
Передача массива по имени с помощью именованной ссылки
Самый удобный современный подход — использовать именованную ссылку: Вы передаёте имя массива в виде строки, а внутри функции объявляете локальную переменную, которая на него ссылается.
Основной синтаксис:
declare -n ref="$1"— теперьrefявляется псевдонимом переменной с именем, переданным первым аргументом- Переменную
refможно читать, перебирать и изменять точно так же, как исходный массив
Это позволяет не копировать весь массив и даёт функции возможность работать с массивами любого размера.
#!/usr/bin/env bash
print_array() {
declare -n arr="$1" # nameref: arr -> caller's variable
echo "Array has ${#arr[@]} elements:"
for elem in "${arr[@]}"; do
echo " - $elem"
done
}
fruits=(apple banana cherry)
print_array fruitsИзменение массива вызывающей стороны внутри функции
Поскольку именованная ссылка является настоящим псевдонимом, запись через именованную ссылку изменяет исходную переменную вызывающей стороны. Не требуется возвращать значения с помощью echo или использовать глобальные переменные.
Поэтому именованные ссылки идеально подходят для функций, которые преобразуют массивы на месте — например, нормализуют строки, фильтруют элементы или сортируют их.
#!/usr/bin/env bash
to_uppercase() {
declare -n _arr="$1"
for i in "${!_arr[@]}"; do
_arr[$i]="${_arr[$i]^^}" # ^^ = uppercase in Bash 4+
done
}
words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After: ${words[@]}"Возврат нового массива через именованную ссылку
Функция также может записывать данные в массив, предоставленный вызывающей стороной, принимая его имя в качестве выходного параметра. Вызывающая сторона создаёт пустой массив, передаёт его имя, а функция заполняет массив.
Этот шаблон напоминает классический приём в C: передача указателя на буфер результата, — только здесь всё выполняется непосредственно в оболочке.
#!/usr/bin/env bash
get_even_numbers() {
local -a source=("$@")
# Last argument is the output array name — pop it off
local out_name="${source[-1]}"
unset 'source[-1]'
declare -n _out="$out_name"
_out=() # clear
for n in "${source[@]}"; do
(( n % 2 == 0 )) && _out+=("$n")
done
}
declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"Опасность совпадения имён именованных ссылок
У именованных ссылок есть одна критическая особенность: если локальная переменная-именованная ссылка имеет то же имя, что и переменная вызывающей стороны, Bash входит в бесконечный цикл самоссылки, и сценарий завершается с ошибкой.
Например, если вызывающая сторона назвала свой массив arr, а функция выполняет declare -n arr="$1", именованная ссылка arr указывает сама на себя.
Соглашение, позволяющее избежать этой проблемы: добавляйте символ подчёркивания перед именами внутренних именованных ссылок (например, _arr, _ref, _out). Вызывающие стороны почти никогда не используют такие имена, поэтому совпадения становятся крайне редкими.
#!/usr/bin/env bash
# DANGER: caller uses variable named 'arr'
arr=(one two three)
bad_func() {
declare -n arr="$1" # collision! arr refers to itself
echo "${arr[@]}"
}
good_func() {
declare -n _arr="$1" # underscore prefix avoids collision
echo "${_arr[@]}"
}
# bad_func arr # would hang / error
good_func arrПередача ассоциативных массивов (отображений) по имени
Ассоциативные массивы (declare -A) работают с именованными ссылками точно так же. Передайте имя отображения в виде строки и объявите именованную ссылку внутри функции — Вы получите полный доступ для чтения и записи всех пар «ключ—значение».
Это незаменимо для отображений конфигурации, анализаторов параметров и таблиц поиска, которыми должны совместно пользоваться функции библиотеки.
#!/usr/bin/env bash
print_map() {
declare -n _map="$1"
echo "Map contents:"
for key in "${!_map[@]}"; do
printf ' %-15s => %s\n' "$key" "${_map[$key]}"
done
}
declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"
print_map configОбъединение двух отображений с помощью именованной ссылки
Поскольку именованные ссылки предоставляют функции действующий псевдоним, с их помощью можно аккуратно реализовывать такие инструменты, как объединение отображений. Передайте по имени целевое отображение и одно или несколько исходных отображений; функция переберёт ключи источников и запишет их в целевое отображение.
Именованные ссылки позволяют написать одну универсальную функцию merge_map, работающую с любыми ассоциативными массивами в Вашем сценарии.
#!/usr/bin/env bash
merge_map() {
# merge_map dest src
declare -n _dest="$1"
declare -n _src="$2"
for key in "${!_src[@]}"; do
_dest["$key"]="${_src[$key]}"
done
}
declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)
merge_map defaults overrides
for k in "${!defaults[@]}"; do
echo "$k = ${defaults[$k]}"
doneКосвенное развёртывание для совместимости со старыми версиями Bash
Если необходимо поддерживать Bash 4.0–4.2 (или старые оболочки, установленные по умолчанию в macOS), конструкция declare -n недоступна. Альтернативой является косвенное развёртывание с помощью ${!varname}.
Косвенное развёртывание рассматривает значение varname как имя переменной и возвращает значение этой переменной. Для массивов его комбинируют со специальным синтаксисом, чтобы получить все элементы.
Ограничения по сравнению с именованными ссылками:
- На практике доступно только чтение (для записи необходимо использовать
eval, что создаёт риски внедрения) - Безопасно перебирать ключи ассоциативного массива без
evalнельзя - По возможности используйте именованные ссылки; прибегайте к косвенному развёртыванию только при необходимости
#!/usr/bin/env bash
sum_array() {
local arr_name="$1"
# Indirect: build a temporary copy by expanding arr_name[@]
local -a items
eval "items=( \"\${${arr_name}[@]}\" )"
local total=0
for n in "${items[@]}"; do
(( total += n ))
done
echo "$total"
}
numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"Создание модульной библиотеки сценариев
В профессиональных проектах на Bash вспомогательные функции хранятся в файлах библиотек (например, lib/array_utils.sh) и подключаются сценариями, которым они нужны.
Шаблон выглядит так:
- Каждый файл библиотеки определяет только функции — без побочных эффектов на верхнем уровне
- Защищайтесь от повторного подключения с помощью переменной-флага:
[[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1 - Сценарии-потребители используют
sourceили., чтобы загрузить библиотеку - Функции обмениваются сложными данными через именованные ссылки, сохраняя всю логику внутри себя
В результате получаются сценарии, пригодные для тестирования, повторного использования и сопровождения, а не разовые хаки оболочки.
Практический пример: анализатор параметров, возвращающий отображение
Практический пример: анализатор параметров командной строки, заполняющий ассоциативный массив, предоставленный вызывающей стороной. Вызывающая сторона передаёт имя массива первым аргументом, а затем фактические аргументы "$@".
Этот шаблон позволяет любому сценарию повторно использовать один и тот же анализатор без копирования кода и зависимости от глобальных переменных.
#!/usr/bin/env bash
parse_opts() {
declare -n _opts="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--host) _opts[host]="$2"; shift 2 ;;
--port) _opts[port]="$2"; shift 2 ;;
--user) _opts[user]="$2"; shift 2 ;;
*) echo "Unknown option: $1" >&2; shift ;;
esac
done
}
declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306
echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"Объединение входных и выходных именованных ссылок в одной функции
Функции могут одновременно содержать несколько именованных ссылок — одну для входных данных и одну для выходных. Это позволяет создавать функции чистого преобразования: они читают исходную структуру, вычисляют результат и записывают его в целевую структуру, не затрагивая глобальное состояние.
Использование разных имён с префиксом из символа подчёркивания для каждой именованной ссылки исключает совпадения, даже если переменные вызывающей стороны имеют простые имена.
#!/usr/bin/env bash
filter_map_by_key_prefix() {
# Usage: filter_map_by_key_prefix src_map dest_map prefix
declare -n _src="$1"
declare -n _dst="$2"
local prefix="$3"
_dst=()
for key in "${!_src[@]}"; do
if [[ $key == ${prefix}* ]]; then
_dst["$key"]="${_src[$key]}"
fi
done
}
declare -A settings=(
[db_host]=localhost [db_port]=5432
[cache_ttl]=300 [cache_driver]=redis
[app_debug]=true
)
declare -A db_settings
filter_map_by_key_prefix settings db_settings db_
for k in "${!db_settings[@]}"; do
echo "$k = ${db_settings[$k]}"
doneПроверка знаний: именованные ссылки и передача массивов
Проверьте, насколько хорошо Вы поняли передачу массивов и отображений между функциями в Bash.
Итоги: передача сложных структур данных в Bash
В этом уроке Вы узнали следующее:
- Массивы нельзя передавать по значению — вместо этого передавайте имя переменной в виде строки.
declare -n ref="$varname"создаёт именованную ссылку — действующий псевдоним переменной вызывающей стороны. И чтение, и запись выполняются непосредственно с исходной переменной. Требуется Bash 4.3 или новее.- Совпадение имён именованных ссылок возникает, когда внутренняя и внешняя переменные имеют одно имя. Предотвращайте его, добавляя префикс к внутренним именованным ссылкам (например,
_arr,_map). - Выходные параметры работают за счёт передачи имени пустого массива или отображения результата; функция заполняет его через именованную ссылку.
- Несколько именованных ссылок в одной функции позволяют выполнять чистые преобразования от входных данных к выходным без глобальных побочных эффектов.
- Косвенное развёртывание (
${!varname[@]}+eval) — старый запасной вариант для Bash < 4.3, но он несёт риски внедрения и на практике доступен только для чтения. - Объединяйте эти приёмы с подключаемыми файлами библиотек, чтобы создавать модульные, повторно используемые и тестируемые кодовые базы на Bash.
Часто задаваемые вопросы
Урок «Передача массивов и ассоциативных отображений между функциями» бесплатный?
Да — полный текст урока «Передача массивов и ассоциативных отображений между функциями» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.
Чему я научусь в уроке «Передача массивов и ассоциативных отображений между функциями»?
Безопасно передавайте сложные структуры данных в функции и из них с помощью ссылок на имена и косвенного раскрытия Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DevOps Bootcamp?
Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Передача массивов и ассоциативных отображений между функциями»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DevOps Bootcamp?
Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Проектирование функций с локальной областью и кодами возврата
- Создание и подключение повторно используемых библиотек Bash
- Разбор флагов и аргументов с помощью getopts
- Передача массивов и ассоциативных отображений между функциями