0Pricing
DevOps Bootcamp · Урок

Передача массивов и ассоциативных отображений между функциями

Безопасно передавайте сложные структуры данных в функции и из них с помощью ссылок на имена и косвенного раскрытия

«Передача массивов и ассоциативных отображений между функциями» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Проектирование функций с локальной областью и кодами возврата
  2. Создание и подключение повторно используемых библиотек Bash
  3. Разбор флагов и аргументов с помощью getopts
  4. Передача массивов и ассоциативных отображений между функциями
← Назад к DevOps Bootcamp