why?
why?

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

for i in {0..3}; do
    var$i="$some_data" # это не работает
done

Но вопрошатель знает про массивы т.к. предлагает переделать в динамические переменные массив. Два слова пульсируют у меня в голове: зачем и как. Как!? Зачем?! Что ты делаешь? Остановись! Это уже больше слов, но я уже себя не контролирую! Массив это по сути и есть динамическая переменная! Вот:

for i in {0..3}; do
    arr[$i]="$some_data"
done

Ничего не напоминает? Или это не достаточно динамично? Нужно что-то подинамичней! Нужна динамика! Тогда такой вариант:

declare -A arr
names=(zero one two three)
for i in {0..3}; do
    arr["${names[$i]}-$i"]="$some_data"
done

Это ассоциативный массив, массив который в качестве идентификатора использует не число а произвольную строку. Т.е. можно хранить данные в формате "key": "value"!

Массивы это именно то что нужно для динамического хранения информации.
Не надо создавать динамические переменные! Это не работет! Это невозможно!

На самом деле возможно, я знаю как минимум 3 способа:

  1. Самый очевидный - declare

for i in {0..3}; do
    declare var$i="$some_data"
done
  1. Хак 1 - read

for i in {0..3}; do
    read var$i <<< "$some_data"
done
  1. Хак 2 - printf

for i in {0..3}; do
    printf -v var$i -- "$some_data"
done

Ну хорошо создали мы эти переменные ну а дальше то что? Как к ним обратиться echo $var$i? Выдаст значение $var и $i но не $var$i. Это не работет! Это невозможно!

На самом деле возможно, я знаю как минимум 2 способа:

  1. Опять declare

declare -n name=var$i
echo $name

Тут надо пояснить, declare с ключем -n создаст переменную "указатель" обращение к этой переменной выведет не её значение а обратится к другой переменной, чьё имя указано в значении данной переменной.

  1. По сути тоже но без declare

name=var$i
echo ${!name}

Тот же эффект.

Ладно, это возможно. Но уж точно это не на столько удобнее массива чтобы пользоваться этим вместо массива. И тем более переделывать готовый массив в это. Я в любом месте скрипта могу сказать:

arr[234]="$cool_data"

Это создаст массив arr и поместит в ячейку с номером(индексом) 234 мои данные! Затем я обращусь к ним так:

echo "${arr[234]}"

С ассоциативным массивом несколько "сложнее", его обязательно надо объявить при помощи declare -A arr и только после этого можно сказать:

arr[description1]="$cool_data"

И затем обратиться к созданной ячейке так:

echo "${arr[description1]}"

А теперь представим на секундочку что у нас нет массивов, только переменные(welcome to sh). Как лупануть по всем?

for i in $var1 $var2 $var3 ...; do
    echo $i
done

Уже на 3-й начинает подташнивать, а если их 100? То ли дело массив:

for item in "${arr[@]}"; do
    echo "$item"
done

A c ассоциативным массивом можно так:

for key "${!arr[@]}"; do
    value=${arr[$key]}
    echo "$key: $value"
done

Но у ассоциативных массивов есть особенность. Сначала возьмём обычный массив:

arr=(
    come
    get
    some
    !
)
$ printf '%s ' "${arr[@]}" '|' "${!arr[@]}"
come get some ! | 0 1 2 3

Значения выводятся по порядку. Теперь создадим ассоциативныи массив:

declare -A arr=(
    [will]=come
     [you]=get
    [dare]=some
       [?]=!
)
$ printf '%s ' "${arr[@]}" '|' "${!arr[@]}"
! get come some | ? you will dare

Результат может удивить т.к. последовательность не совпадает. Почему? Потому что вот.
Массив позволяет делать срезы, т.е. выбрать не все а только некоторые ячейки, например так:

$ printf '%s ' "${arr[@]:1:2}"
get some

Это можно использовать в "таблицах", запишем массив вот так:

arr=(
# столбец  1   2    3     4   строка
         come get  some  \!   # 1
         will you  dare  \?   # 2
         make some noise \!   # 3
)
cn=4 # кол. столбцов
ln=3 # кол. строк

Теперь с помощью вот такой нехитрой функции мы можем получить любую строку.

line(){ echo "${arr[@]:$(( ($1-1) * cn )):$cn}"; }

$ line 2
will you dare ?

Со столбцами немного сложней, придется использовать цикл:

clmn(){ for ((i=$(($1-1)); i<$((ln*cn)); i+=$cn)); { echo "${arr[@]:$i:1}"; }; }

$ clmn 2
get
you
some

Теперь попробуем выбрать ячейку указав номер строки и столбца:

both(){ echo "${arr[@]:$(( (($1-1) * cn) + ($2-1) )):1}"; }

$ both 2 3 # 2-я строка, 3-й столбец
dare

Ассоциативный массив можно использовать для сортировки(удаления дублей) как-то так:

unsorted=( one one one one two two two three )
declare -A sorted
for item in "${unsorted[@]}"; {
    ((sorted[$item]++))
}

printf '%s ' "${unsorted[@]}"
one one one one two two two three

$ printf '%s ' "${!sorted[@]}"
two three one

for key in "${!sorted[@]}"; { printf '%s(%s) ' $key "${sorted[$key]}"; }
two(3) three(1) one(4)

И еще много всяких штук можно придумать с массивами.
Попробуйте провернуть всё это используя динамические переменные!?
Вероятно это возможно но я не знаю способов и даже думать не хочу об этом.

Расставим точки над Ё.
Когда хочется создать динамическую переменную, используйте массив!
Динамические переменные существуют и в каких-то экзотических случаях их можно использовать.

Тут еще много массивного bash'а: sshto, kube-dialog, piu-piu

Творите, выдумывайте, пробуйте!)

Лайки, пальцы.

Комментарии (33)


  1. DrinkFromTheCup
    01.09.2022 02:17
    +1

    У меня есть подозрение, зачем народ хочет динамических переменных. Причём, в отрыве от bash (и привязки к конкретным средствам разработки вообще).

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

    Но оно физически не будет работать как надо. Глупый интерпретатор не умеет думать ни за кодера, ни за пользователя.

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

    В худшем - мы тупо загадим себе память колоссальным количеством бессмысленных переменных. Чехарду в которых потом нам же и дебажить. А такую бессистемную мешанину дебажить ОЧЕНЬ больно...

    Я уже лет восемь топчусь на этом этапе. Слишком туп для ООП, слишком аккуратен для такого шлакокода. Но не все настолько хорошо себя знают...


    1. Sap_ru
      01.09.2022 04:12
      +1

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

      А так, да - динамические имена переменных нужны именно в тех случаях, когда задача требует объектов, а объектов нет.


      1. DrinkFromTheCup
        01.09.2022 07:56
        +2

        Хммммммм.

        С одной стороны, Java есть везде, от SMORT-кофейников до суперкомпьютеров.
        .net в том или ином виде доступен на любой пользовательской станции.
        А если хочется чего-то совсем легковесного - думаю, более продвинутые пользователи Линукса добавят ещё 1-3 варианта именно легковесных.

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

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

        ...

        Пару месяцев назад тут как-то шутили:

        И пафосно-натужно доказали, что даже такой изврат сделать - МОЖНО!

        Однако, я не припомню, чтобы хоть для какого-то целеполагания, кроме как brain flex'а и, может быть, вечно бдящих специалистов по безопасности, это оказалось пригодно...


        1. vaniacer Автор
          01.09.2022 14:43
          +1

          Да, с одной стороны сборка кибика Рубика методом 'разборки и сборки в нужной последовательности' выглядит просто и привлекательно. С другой, какой там рекорд скорости сборки КР? Несколько секунд? Т.е. если действительно УМЕЕШЬ собирать то получается гораздо проще и быстрей.


          1. DrinkFromTheCup
            02.09.2022 00:20

            Именно. Но мы же не требуем от мастера вязания крючком, чтобы он КР собрал быстро. Хотя у него тоже руки очень скилловые.


        1. Sap_ru
          01.09.2022 16:55

          А с каких порт администратор сервера стал "рядовым пользователем"? И зачем ему тащить Java на сервер ради вспомогательных скриптов?


          1. DrinkFromTheCup
            02.09.2022 00:03

            Ну не рядовым, тут я погорячился, Вы правы. Но уж точно не очень скилловым. Спросить проще, чем докопаться самому, - и на одних вопросах скилл не так уж и хорошо прокачивается...

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

            Не верю.


    1. saboteur_kiev
      01.09.2022 15:31

      Не знаю причем тут ООП, но вот после perl, очень не хватает ассоциативных массивов. Хорошо что они уже появились в bash. Плохо что их нет в Posix shell


  1. thatsme
    01.09.2022 03:12
    +2

    eval существует со времён до появления bash. Динамически генерировать переменные проблемой никогда не было. Т.е. shell изначально предназначен для кодогенерации.

    Для чего это нужно? Вот я сломался здесь. Кодогенерацию использую уже 25 лет, а на вопрос зачем ответить не могу. Проще?

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

    #MANDATORY - список переменных создаваемых из аргументов командной строки и требуемых для выполнения скрипта. 
    #Пример --hostname abc.de, превращаетша в переменную HOSTNAME содержащую abc.de
    #SOFT - опции с дополнительным параметром
    #SOFT_SINGLE - булевские флаги (появление в списке аргументов == истина)
    
    export MANDATORY="HOSTNAME MAC"
    export SOFT="IP GATEWAY NETMASK NAMESERVER TMPSZ ROOTSZ OPTSZ VARSZ SWAPSZ KSOUT EXCDR BONDSLAVES OS"
    export SOFT_SINGLE="NOIPV6 NOOPT BONDING"
    
    chk_args "$@" # магия с eval тут
    
    if [ "${IP}x" != "x" -o "${GATEWAY}x" != "x"  -o "${NAMESERVER}x" != "x" ]
    then
      export MANDATORY="${MANDATORY} IP GATEWAY NAMESERVER NETMASK"
      export BOOTPROTO="static"
      unset SOFT
      chk_args "$@" # и тут
    else
      export BOOTPROTO="dhcp"
    fi

    А в шаблоне кикстарт файла тоже, например для определения имён дисков и в последствии автоматического создания томов на этапе инсталляции. Т.е. генерация происходит в самом кикстарте на этапе инсталляции ...

    export EXCLUDE_DRIVES=XXXEXCDRVXXX
    
    if [ "${EXCLUDE_DRIVES}x" == "x" ]
    then
      set $(list-harddrives)
    else
      set $(list-harddrives|egrep -v "${EXCLUDE_DRIVES}")
    fi
    
    export args=( "$@" )
    
    let argc=$#
    let i=0
    let drives=0
    
    driveorder=""
    
    while [ $i -lt $argc ]
    do
      let drives++
      drivename="drive${drives}"
      drive=${args[$i]}
      export ${drivename}=$drive
      let i+=2
      if [ "${driveorder}x" == "x" ]
      then
        driveorder="$(eval echo \$$drivename)"
      else
        driveorder="${driveorder},$(eval echo \$$drivename)"
      fi
    
    # .....
    # где-то ниже по коду
    echo "clearpart --all --initlabel --drives=${driveorder}"                            >> /tmp/part-include
    echo "bootloader --location=mbr --append=\"rhgb quiet\" --driveorder=${driveorder}"  >> /tmp/part-include
    echo "zerombr"                                                                       >> /tmp/part-include
    
    # .........
    # где-то ниже по коду
    if [ $drives -gt 1 ]
    then
      pvlist=()
      let j=1
      let i=2
      while [ $j -lt $drives ]
      do
        if [ "${args[$i]}x" != "x" ]
        then
          pv="pv.0$(( j + 1 ))"
          echo "part $pv --size=512 --grow --ondisk=${args[$i]}"                           >> /tmp/part-include
          pvlist=(${pvlist[@]} $pv)
    # ....
    # где-то ниже по коду
    if [ ${#pvlist[@]} -ne 0 ]
      then
        echo "volgroup ${VGNAMEPFX}.data ${pvlist[@]}"                                                  >> /tmp/part-include
        if [ ${NOOPT} -eq 0 ]
        then
          echo "logvol /opt --vgname=${VGNAMEPFX}.data  --fstype=xfs  --size=$optsz --name=opt"         >> /tmp/part-include
        fi
    

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

    Это наверняка можно всё было сделать ещё 10-ю способами. Но мне так было проще.


    1. Sap_ru
      01.09.2022 04:16

      eval предлагает выстрелить себе в ногу в случае наличия в записываемых данных кавычек и спецсимволов. Запись через eval в переменную строку с символами $"'\ это тот ещё квест.


    1. vaniacer Автор
      01.09.2022 09:31

      evil


    1. saboteur_kiev
      01.09.2022 15:33

      У вас очень очень сложный пример для eval. Ведь проще было процитировать скрипт из статьи

      $ for i in {0..3}; do
          var$i="$some_data" # это не работает
      done
      var0=: command not found
      ...

      и показать как он может заработать с eval

      $ for i in {0..3}; do
          eval var$i="some_data" # это работает
      done
      
      $ echo $var1
      some_data


      1. vaniacer Автор
        01.09.2022 18:32

        $ time for i in {0..100000}; do eval var$i="$i"; done
        
        real  0m1,265s
        user  0m1,251s
        sys	  0m0,012s
        
        $ time for i in {0..100000}; do arr[$i]="$i"; done
        
        real  0m0,646s
        user  0m0,642s
        sys   0m0,004s


        1. DrinkFromTheCup
          02.09.2022 00:14

          Ну, да. Но мы-то изначально захотели встать на путь Хаоса Неделимого БЕЗ каких-то оговорок о производительности.

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

          Там выше мне пеняли, что не везде есть возможность или желание впереть ради одной такой манипуляции аж Java. Но будет ли она одна? И если так хочется перформанса и удобства без лишних зависимостей - ну уж SQL то в том или ином виде на хосте быть должен... Хотяаааа... не помню, не знаю, можно ли прямо в нём такое отчебучить, не скатываясь в ещё более тяжкий изврат...

          (%!., куда я лезу со своим скиллом как у молодого окуня...)


  1. Sap_ru
    01.09.2022 04:06
    +1

    Статья интересная, но посыл, предпосылки и выводы в корне неверные.

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

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

    Другой частый случай, когда даже ужасный "var$i" может понадобиться, это имитация объектов. Например, скрипт должен одновременно работать с несколькими конфигурациями (у меня он контейнерами рулит). Для этого нужно уметь одинаковым способом читать и обрабатывать несколько наборов переменных. А как это сделать без дублирования кода? Используя префиксы переменных! В выполняющую действие функцию передается префикс, а она уже сама читает/пишет все нужные переменные.

    Почему не массив? Потому, что мне нужно имея префикс USER получить из функции переменные USER1_FOLDER1_NAME, USER1_FOLDER1_TIMESTAMP и т.п, где номер изменяется, и я не знаю сколько там этих фолдеров и юзеров будет. Как вы такое решать будете? Через ассоциативные массивы? Можно, но и без того немалый уровень ада в скрипте становится только больше.

    Прочее не eval? Потому, что локальные/глобальные переменные и спецсимволы с кавычками в значениях. Это и глюки и уязвимости.

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


    1. vaniacer Автор
      01.09.2022 09:35
      +1

      Для этого отлично подойдёт ассоциативный массив.


      1. Sap_ru
        01.09.2022 16:56

        А чтобы вернуть множество значений из функции и раскидать по переменным?


        1. vaniacer Автор
          01.09.2022 18:35
          -1

          Тут уже вопрос общей архитектуры скрипта, может это и не нужно?


          1. Sap_ru
            01.09.2022 18:38

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


            1. vaniacer Автор
              01.09.2022 18:48

              Эм, заметка была про массивы и переменные а не про конфигурацию, инициализацию и функции...
              А если функция(и) в нескольких скриптах используется, делают "библиотеку(и)" функций и сорсят в скрипты.


              1. Sap_ru
                02.09.2022 17:46

                И как вы потом из этих скриптов будете множество значений возвращать-то? Вот тут вам и понадобятся динамические переменные. Муа-ха-ха!


                1. vaniacer Автор
                  03.09.2022 14:13

                  Куда возвращать? Зачем? Мы явно о разных вещах говорим. Напомню начальный посыл статьи, на всякий случай. Не надо создавать переменные типа var$i="$data" ведь есть готовый аналог arr[$i]="$data"


  1. weirded
    01.09.2022 06:48
    +2

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


    1. DrinkFromTheCup
      01.09.2022 08:20
      +1

      Ну. Допустим.

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

      Сунуть в переменную окружения стринг, в вызываемой программе распарсить.

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

      Если же по каким-то причинам ожидается, что массив МОЖЕТ и ДОЛЖЕН измениться посреди обработки (т. е. одной или даже несколькими переменными окружения тут не отделаешься либо надо слишком часто забирать её значения и постоянно пересверять) - и это почему-то НОРМАЛЬНО, то тут надо брать диаграммы логики работы и хорошо-хорошо думать, сначала как до такого докатились, потом как от этого избавиться.

      Наверное, я глупость написал. Но я правда не понимаю, как так то. Какое-то пояснение тут определённо поможет впредь чушь не пороть.


    1. saboteur_kiev
      01.09.2022 15:34

      через переменную окружения можно прокинуть json с любой структурой, лишь бы влез по памяти =)


  1. randomsimplenumber
    01.09.2022 09:15

    Наверное, существуют системы, где есть bash, принципиально отсутствует python/perl/php, и одновременно есть необходимость в подобных извращениях.


    1. NN1
      01.09.2022 09:19
      +1

      Докер. Цель уложиться в как можно более мелкий образ.

      Вот и выпиливается всё, что можно выпилить по максимуму.


      1. vaniacer Автор
        01.09.2022 09:40

        Применительно к описанной ситуации получается наоборот впиливание. Массив уже есть, он наполнен данными и все эти данные дублируются в переменные. Зачем?


      1. randomsimplenumber
        02.09.2022 07:56
        -1

        Цель уложиться в как можно более мелкий образ.

        Чтобы что? Занять первое место на специальной олимпиаде по сборке мелких образов? ;) Если на этом образе будет строиться что то полезное - вряд ли+- 10 мб будет иметь значение. А стоимость поддержки - будет.


    1. Sap_ru
      01.09.2022 17:02

      Очень часто программа Python оказывается сильно больше скрипта на bash. К тому же она будет не одним файлом, а несколькими (либо отдельные приседания нужно).
      Bash очень крут при работе с пайпами и прочим. Питоновская программа для жонглирования процессами и пользователями получилась намного (раз в 10) больше и требовала специальной сборки, чтобы представлять собой один файл. И её нельзя было быстро и криво поправить прямо в момент использования (ибо упаковано). Но сделать на питоне несомненно можно намного более крутые штуки.

      И на целевой системе может не быть достаточно свежей версии Python. Или наоборот, прошло пять лет и скрипт использует что-то из depricated. Bash же отлично запускает скрипты 15-летней давности.


      1. randomsimplenumber
        02.09.2022 10:22
        -1

        Очень часто программа Python оказывается сильно больше скрипта на bash.

        скрипта однострочника. Если программа делает какую то обработку данных, то на bash, скорее всего, это будет простыня кода. С зависимостями от внешних программ.

        на целевой системе может не быть достаточно свежей версии Python

        Если система протухшая outdated, значит активной разработки под неё не ведётся. Закопайте стюардессу ;) Она 10 лет жила без этой программы.


        1. Sap_ru
          02.09.2022 17:47
          +1

          Сказочно далеки вы от народа и энтерпрайза...


          1. randomsimplenumber
            02.09.2022 18:16

            В каждом монастыре свой дресс-код ;) Кому-то велят запиливать новые фичи в старую систему? Штош, таков путь.