Имена динамических переменных в Bash

Я запутался в скрипте bash.

У меня есть следующий код:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

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

Итак, чтобы проиллюстрировать, что я хочу:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Итак, как я должен определить / объявить $magic_way_to_define_magic_variable_$1 и как я должен назвать это в сценарии?

я пытался eval, ${...}, \$${...}, но я все еще в замешательстве.

21 ответ

Решение

Используйте ассоциативный массив с именами команд в качестве ключей.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

Если вы не можете использовать ассоциативные массивы (например, вы должны поддерживать bash 3), вы можете использовать declare создать имена динамических переменных:

declare "magic_variable_$1=$(ls | tail -1)"

и использовать косвенное расширение параметра для доступа к значению.

var="magic_variable_$1"
echo "${!var}"

См. BashFAQ: косвенное обращение - оценка косвенных / ссылочных переменных.

Я искал лучший способ сделать это в последнее время. Ассоциативный массив звучит как излишнее для меня. Посмотри что я нашел:

suffix=bzz
declare prefix_$suffix=mystr

...а потом...

varname=prefix_$suffix
echo ${!varname}

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

В следующих примерах я буду предполагать, что i=37 и что вы хотите псевдоним переменной с именем var_37 чье начальное значение lolilol,

Способ 1. Использование переменной "указатель"

Вы можете просто сохранить имя переменной в косвенной переменной, в отличие от указателя Си. Bash имеет синтаксис для чтения псевдонимов: ${!name} расширяется до значения переменной, имя которой равно значению переменной name, Вы можете думать об этом как о двухэтапном расширении: ${!name} расширяется до $var_37, который расширяется до lolilol,

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

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

1a. Назначение с eval

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

И вы должны проверить вручную, что левая часть является допустимым именем переменной или именем с индексом (что, если это было evil_code #?). В отличие от всех других методов, приведенных ниже, применять его автоматически.

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”

Недостатки:

  • не проверяет правильность имени переменной.
  • eval это зло
  • evalэто зло
  • evalэто зло

1б. Назначение с read

read Встроенный позволяет назначать значения переменной, которой вы даете имя, факт, который можно использовать в сочетании со следующими строками:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

IFS часть и вариант -r убедитесь, что значение присвоено как есть, а опция -d '' позволяет назначать многострочные значения. Из-за этой последней опции команда возвращается с ненулевым кодом выхода.

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

Недостатки:

  • немного неясный;
  • возвращается с ненулевым кодом выхода;
  • добавляет новую строку к значению.

1c. Назначение с printf

Начиная с Bash 3.1 (выпущен в 2005 году), printf Встроенный также может присваивать свой результат переменной, имя которой дано. В отличие от предыдущих решений, это просто работает, никаких дополнительных усилий не требуется, чтобы избежать чего-то, предотвратить расщепление и так далее.

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

Недостатки:

  • Менее портативный (но, хорошо).

Способ 2. Использование "ссылочной" переменной

Начиная с Bash 4.3 (выпущена в 2014 году), declare встроенный имеет опцию -n для создания переменной, которая является "ссылкой на имя" на другую переменную, во многом как ссылки на C++. Как и в методе 1, ссылка хранит имя переменной с псевдонимом, но при каждом обращении к ссылке (либо для чтения, либо для назначения) Bash автоматически разрешает косвенное обращение.

Кроме того, Bash имеет специальный и очень запутанный синтаксис для получения значения самой ссылки, судите сами: ${!ref},

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”

Это не позволяет избежать ошибок, описанных ниже, но, по крайней мере, делает синтаксис простым.

Недостатки:

  • Не портативный

риски

Все эти методы сглаживания представляют несколько рисков. Первый - выполнение произвольного кода каждый раз, когда вы разрешаете косвенное обращение (либо для чтения, либо для назначения). Действительно, вместо имени скалярной переменной, как var_37вы также можете использовать псевдоним массива, например: arr[42], Но Bash оценивает содержимое квадратных скобок каждый раз, когда это необходимо, поэтому псевдонимы arr[$(do_evil)] будет иметь неожиданные эффекты... Как следствие, используйте эти методы только тогда, когда вы контролируете происхождение псевдонима.

function guillemots() {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)

Второй риск - создание циклического псевдонима. Поскольку переменные Bash идентифицируются по имени, а не по области действия, вы можете непреднамеренно создать псевдоним для себя (полагая, что это будет псевдоним переменной из входящей области). Это может произойти, в частности, при использовании общих имен переменных (например, var). Как следствие, используйте эти методы только тогда, когда вы управляете именем переменной с псевдонимом.

function guillemots() {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!

Источник:

Пример ниже возвращает значение $name_of_var

var=name_of_var
echo $(eval echo "\$$var")

Использовать declare

Нет необходимости использовать префиксы, как в других ответах, а также массивы. Используйте толькоdeclare, двойные кавычки и раскрытие параметров.

Я часто использую следующий трюк для анализа списков аргументов, содержащих one to n аргументы отформатированы как key=value otherkey=othervalue etc=etc, Подобно:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip

Но расширение списка argv, например

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

Дополнительные советы

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while (( $# )); do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
  shift
done

Объединение здесь двух высоко оцененных ответов в законченный пример, который, надеюсь, будет полезным и не требует пояснений:

#!/bin/bash

intro="You know what,"
pet1="cat"
pet2="chicken"
pet3="cow"
pet4="dog"
pet5="pig"

# Setting and reading dynamic variables
for i in {1..5}; do
        pet="pet$i"
        declare "sentence$i=$intro I have a pet ${!pet} at home"
done

# Just reading dynamic variables
for i in {1..5}; do
        sentence="sentence$i"
        echo "${!sentence}"
done

echo
echo "Again, but reading regular variables:"
echo $sentence1
echo $sentence2
echo $sentence3
echo $sentence4
echo $sentence5

Вывод:

Знаете что, у меня дома есть домашняя кошка
Знаете что, у меня дома есть домашняя курица
Знаете что, у меня дома домашняя корова
Знаете что, у меня дома есть домашняя собака
Знаете что, у меня есть домашняя свинья дома

Опять же, но читаем обычные переменные:
Знаете что, у меня дома есть домашняя кошка.
Знаете что, у меня дома есть домашняя курица.
Знаете что, у меня дома есть домашняя корова.
Знаете что, у меня есть домашняя собака. дома
Знаете что, у меня дома есть домашняя свинья

Это тоже будет работать

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

В твоем случае

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val

Для zsh (более новые версии Mac OS) вы должны использовать

      real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa

Вместо "!"

Это должно работать:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"

Согласно BashFAQ / 006, вы можете использовать read здесь синтаксис строки для назначения косвенных переменных:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

Использование:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt

Дополнительный метод, который не зависит от того, какая у вас версия оболочки /bash, - это использование envsubst. Например:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)

KISS подход:

      a=1
c="bam"
let "$c$a"=4
echo bam1

приводит к 4

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

Решил это с declare -nкоторый создает ссылку на динамическое значение, это особенно полезно в процессах CI/CD, где требуемые секретные имена службы CI/CD не известны до времени выполнения. Вот как:

# Bash v4.3+
# -----------------------------------------------------------
# Secerts in CI/CD service, injected as environment variables
# AWS_ACCESS_KEY_ID_DEV, AWS_SECRET_ACCESS_KEY_DEV
# AWS_ACCESS_KEY_ID_STG, AWS_SECRET_ACCESS_KEY_STG
# -----------------------------------------------------------
# Environment variables injected by CI/CD service
# BRANCH_NAME="DEV"
# -----------------------------------------------------------
declare -n _AWS_ACCESS_KEY_ID_REF=AWS_ACCESS_KEY_ID_${BRANCH_NAME}
declare -n _AWS_SECRET_ACCESS_KEY_REF=AWS_SECRET_ACCESS_KEY_${BRANCH_NAME}

export AWS_ACCESS_KEY_ID=${_AWS_ACCESS_KEY_ID_REF}
export AWS_SECRET_ACCESS_KEY=${_AWS_SECRET_ACCESS_KEY_REF}

echo $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
aws s3 ls

Вау, большая часть синтаксиса ужасна! Вот одно решение с более простым синтаксисом, если вам нужно косвенно ссылаться на массивы:

#!/bin/bash

foo_1=("fff" "ddd") ;
foo_2=("ggg" "ccc") ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]} ;
done ;

Для более простых случаев использования я рекомендую синтаксис, описанный в Advanced Bash-Scripting Guide.

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

script.sh файл:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

Тестовое задание:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

Согласно help eval:

Выполните аргументы как команду оболочки.


Вы также можете использовать Bash ${!var} косвенное расширение, как уже упоминалось, однако оно не поддерживает извлечение индексов массива.


Для дальнейшего чтения или примеров, проверьте BashFAQ / 006 о косвенности.

Нам неизвестны какие-либо хитрости, которые могут дублировать эту функциональность в оболочках POSIX или Bourne без eval, что может быть трудно сделать безопасно. Итак, рассмотрите это использование на свой страх и риск.

Тем не менее, вы должны пересмотреть использование косвенного обращения согласно следующим примечаниям.

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

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

Для индексированных массивов вы можете ссылаться на них так:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

На ассоциативные массивы можно ссылаться аналогично, но нужно -A включить declare вместо -a,

пока я думаю declare -nпо-прежнему лучший способ сделать это, есть другой способ, о котором никто не упоминал, очень полезный в CI/CD

      function dynamic(){
  export a_$1="bla"
}

dynamic 2
echo $a_2

Эта функция не поддерживает пробелы, поэтому dynamic "2 3"вернет ошибку.

POSIX-совместимый ответ

Для этого решения вам потребуются права чтения/записи на /tmpпапка.
Мы создаем временный файл, содержащий наши переменные, и используем -aфлаг setвстроенный:

$ man set
...
-a Каждая создаваемая или изменяемая переменная или функция получает атрибут экспорта и помечается для экспорта в среду последующих команд.

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

Реализация

      #!/bin/sh
# Give the temp file a unique name so you don't mess with any other files in there
ENV_FILE="/tmp/$(date +%s)"

MY_KEY=foo
MY_VALUE=bar

echo "$MY_KEY=$MY_VALUE" >> "$ENV_FILE"

# Now that our env file is created and populated, we can use "set"
set -a; . "$ENV_FILE"; set +a
rm "$ENV_FILE"
echo "$foo"

# Output is "bar" (without quotes)

Объяснение шагов выше:

      # Enables the -a behavior
set -a

# Sources the env file
. "$ENV_FILE"

# Disables the -a behavior
set +a

Я думаю, это дает именно то, что задал исходный вопрос:

          #!/bin/bash
    
    function grep_search() {
            read magic_variable_${1} < <(ls -1 | tail -1)
            echo magic_variable_$1
            eval echo \$magic_variable_$1
    }

Приведенный выше пример кода повторяет имя переменной, а также содержимое новой переменной для целей отладки. Проверено на bashv. 4.1.2

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

За varname=$prefix_suffix формат, просто используйте:

varname=${prefix}_suffix
Другие вопросы по тегам