Бывает, что имя нужной переменной заранее неизвестно. Оно складывается на ходу - из окружения, из аргумента, из текущего режима работы скрипта. Скрипт знает, что хочет значение переменной с именем DB_HOST_production, но само это имя собралось из куска DB_HOST_ и переменной, в которой лежит слово production. Как достать значение, когда на руках лишь имя в виде строки, а не сама переменная?
В bash для этого есть косвенное разворачивание параметра - синтаксис с восклицательным знаком внутри фигурных скобок. Он берёт значение одной переменной, трактует его как имя другой и достаёт уже её значение. Звучит как ребус, но на практике закрывает понятную потребность: динамическое обращение к переменным, чьи имена определяются во время выполнения. Разберём, как это работает, чем лучше старого приёма через eval, и почему опытные авторы скриптов чаще тянутся к ассоциативным массивам вместо него.
Восклицательный знак вводит лишний уровень разыменования
Обычное разворачивание просто достаёт значение: написал $имя - получил то, что в нём лежит. Косвенное разворачивание добавляет ещё один шаг. Восклицательный знак сразу после открывающей фигурной скобки говорит оболочке: сначала разверни внутреннее, получи имя, а потом достань значение уже по этому имени.
имя="Борис"
указатель="имя"
echo "${!указатель}"
# Борис - достали значение переменной, чьё имя лежало в указателе
Здесь указатель хранит строку имя. Косвенное разворачивание берёт эту строку, понимает её как название переменной и возвращает значение переменной имя. Получается обращение через посредника - в переменной лежит не сам ответ, а имя того, у кого ответ можно спросить.
Связь живая, а не разовая. Если изменить целевую переменную, косвенное обращение тут же отразит новое значение, потому что разыменование происходит каждый раз заново.
ячейка="данные"
данные=24
echo "${!ячейка}" # 24
данные=387
echo "${!ячейка}" # 387 - значение обновилось
Восклицательный знак обязан стоять вплотную к открывающей скобке. Это синтаксическое требование: именно соседство знака и скобки включает режим косвенности. Стоит вставить пробел или поменять порядок - и смысл конструкции теряется.
Сборка имени на лету раскрывает практический смысл
Сила приёма проявляется, когда имя переменной собирается из частей прямо во время работы скрипта. Классический случай - выбор настройки в зависимости от окружения, где у каждого окружения свой набор переменных с предсказуемым префиксом.
DB_HOST_production="prod-db.example.com"
DB_HOST_staging="staging-db.example.com"
окружение="production"
имя_переменной="DB_HOST_${окружение}"
адрес_базы="${!имя_переменной}"
echo "$адрес_базы"
# prod-db.example.com
Логика читается по шагам. Сначала из префикса и значения переменной окружения собирается строка DB_HOST_production. Затем косвенное разворачивание понимает эту строку как имя переменной и достаёт её содержимое. Скрипт сам, без ветвлений и длинных условий, выбрал нужную настройку по текущему режиму. Один и тот же код обслуживает и боевое окружение, и тестовое - меняется лишь переменная окружения.
Этот же узор пригождается, когда скрипт обходит набор однотипных переменных, чьи имена различаются лишь суффиксом, или когда настройку нужно подтянуть по ключу, пришедшему извне. Везде, где имя переменной определяется на ходу, косвенное разворачивание избавляет от громоздких разветвлений.
Звёздочка превращает косвенность в перечисление имён
У синтаксиса с восклицательным знаком есть близкий родственник, который легко спутать, хотя делает он совсем другое. Если после префикса поставить звёздочку, конструкция перестаёт разыменовывать и начинает перечислять - возвращает имена всех переменных, начинающихся с заданного префикса.
DB_HOST_production="prod-db.example.com"
DB_HOST_staging="staging-db.example.com"
echo "${!DB_HOST_*}"
# DB_HOST_production DB_HOST_staging - имена, а не значения
Разница принципиальна. Без звёздочки конструкция берёт значение переменной как имя и достаёт значение по нему - это настоящая косвенность. Со звёздочкой она ищет все переменные с подходящим началом имени и возвращает их названия, разделённые разделителем полей. Это уже не разыменование, а сканирование пространства имён, удобное, когда нужно перебрать группу связанных переменных, не зная заранее их полного списка.
Тот же приём работает и для перечисления индексов или ключей массива, что делает его полезным при обходе ассоциативных массивов целиком.
Косвенность только читает, но не присваивает
Важное ограничение, на которое натыкаются почти все. Косвенное разворачивание умеет лишь доставать значение. Записать значение в переменную, имя которой лежит в другой переменной, оно не способно - это инструмент чтения, а не присваивания.
указатель="цель"
# ${!указатель}="новое" # так НЕ работает, это не присваивание
Для косвенной записи нужны другие средства. Безопасный и читаемый путь - встроенная команда printf с флагом записи в переменную, имя которой берётся из другой переменной.
указатель="цель"
printf -v "$указатель" '%s' "новое значение"
echo "$цель" # новое значение
Этот способ предпочтительнее старого приёма через команду eval, которая разбирает и выполняет собранную строку как код. eval всемогуща, но именно поэтому опасна: если в имя или значение просочится посторонний текст из ненадёжного источника, он будет исполнен как команда - прямая дорога к нежелательному выполнению чужого кода. Команда printf с флагом записи делает то же безопаснее, без повторного разбора строки.
Старый способ через eval и почему от него ушли
До появления удобного синтаксиса косвенность изображали через eval и двойной знак доллара с экранированием. Выглядело это громоздко и читалось тяжело.
# Архаичный способ - работает, но хрупок и нечитаем
var2="имя"
eval value=\$$var2
Современный синтаксис с восклицательным знаком вытеснил эту конструкцию, потому что он понятнее, не требует экранирования и не открывает дыру для исполнения произвольного кода. Старый приём остаётся в наследии скриптов, но писать так заново смысла нет.
Ссылка на имя как более чистая альтернатива
В bash версии 4.3 и новее появился ещё более выразительный механизм - ссылка на имя, создаваемая через объявление с флагом ссылки. Она делает одну переменную псевдонимом другой: чтение и запись через псевдоним идут прямиком в целевую переменную.
declare -n ссылка=цель
цель="исходное значение"
echo "$ссылка" # исходное значение
ссылка="новое значение" # пишем через псевдоним
echo "$цель" # новое значение - оригинал изменился
Ссылки на имя особенно хороши внутри функций. Они позволяют функции напрямую менять переменную вызывающего кода, переданную по имени, что решает старую боль с возвратом результата в нужную переменную без глобальных ухищрений. Это считается предпочтительным способом косвенности там, где не нужна совместимость со старыми версиями оболочки. Косвенное разворачивание через восклицательный знак остаётся для случаев, когда важна переносимость или когда нужно именно прочитать значение по собранному имени.
Когда косвенность - правильный выбор, а когда ловушка
Тут стоит сказать прямо, и многие руководства на этом сходятся: косвенным разворачиванием легко злоупотребить. Оно делает код труднее для чтения, а частая нужда в нём - обычно признак того, что задача просится под ассоциативный массив. Массив с ключами решает ту же проблему чище и нагляднее, чем плодить переменные с префиксами в именах и доставать их косвенно.
# Вместо префиксов в именах и косвенного доступа - ассоциативный массив
declare -A DB_HOST
DB_HOST[production]="prod-db.example.com"
DB_HOST[staging]="staging-db.example.com"
окружение="production"
echo "${DB_HOST[$окружение]}"
# prod-db.example.com - то же, но яснее
Сравнение говорит само за себя. Тот же результат достигается без косвенности, без сборки имён из кусков, без восклицательных знаков. Код читается как обычное обращение к словарю по ключу, и намерение автора видно сразу.
Практический свод укладывается в несколько правил. Косвенное разворачивание через восклицательный знак уместно, когда имена переменных приходят извне или собираются на ходу, а переписать всё на массив нельзя - например, при работе с чужими переменными окружения. Перечисление через звёздочку удобно для обхода групп связанных переменных. Для косвенной записи берут printf с флагом, а не опасный eval. А ссылка на имя - самый чистый путь там, где доступна свежая версия оболочки, особенно внутри функций.
Но первый вопрос, который стоит себе задать, увидев тягу к косвенности, - не просится ли тут ассоциативный массив. Чаще всего просится. Косвенное разворачивание - мощный инструмент, и именно поэтому пользоваться им стоит сдержанно, лишь там, где более простые средства не справляются. Гибкость, купленная ценой читаемости, окупается далеко не всегда.