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


159

Я запутался в скрипте 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, ${...}, \$${...}, но я до сих пор путаю.


3
Не. Используйте ассоциативный массив для сопоставления имени команды с данными.
chepner

3
ВАР = А; VAL = 333; читать "$ VAR" <<< "$ VAL"; эхо "A = $ A"
Григорий К

Ответы:


150

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

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

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

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

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

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

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

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


5
@DeaDEnD -aобъявляет индексированный массив, а не ассоциативный массив. Если аргумент to не grep_searchявляется числом, он будет рассматриваться как параметр с числовым значением (по умолчанию 0, если параметр не установлен).
chepner

1
Хм. Я использую bash 4.2.45(2)и заявляю, что не перечисляю его как опцию declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Похоже, работает правильно, однако.
13

declare -hв 4.2.45 (2) для меня показывает declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Вы можете перепроверить, что вы используете 4.x, а не 3.2.
chepner

5
Почему не просто declare $varname="foo"?
Бен Дэвис

1
${!varname}намного проще и широко совместимо
Брэд Хейн

227

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

suffix=bzz
declare prefix_$suffix=mystr

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

varname=prefix_$suffix
echo ${!varname}

Если вы хотите объявить глобал внутри функции, можете использовать "Declare -g" в Bash> = 4.2. В ранних версиях bash можно использовать «только для чтения» вместо «объявить», если вы не хотите изменять значение позже. Может быть в порядке для конфигурации или что у вас.
Сэм Уоткинс

7
лучше всего использовать инкапсулированный формат переменной: prefix_${middle}_postfix(т. е. ваше форматирование не будет работать varname=$prefix_suffix)
msciwoj

1
Я застрял с Bash 3 и не мог использовать ассоциативные массивы; как таковой, это было спасение жизни. $ {! ...} не легко гуглить на этом. Я предполагаю, что это просто расширяет имя переменной.
Нил МакГилл,

10
@NeilMcGill: см. «Man bash» gnu.org/software/bash/manual/html_node/… : основная форма раскрытия параметров - это $ {параметр}. <...> Если первый символ параметра является восклицательным знаком (!), Вводится уровень косвенной косвенности. Bash использует значение переменной, сформированной из остальной части параметра, в качестве имени переменной; эта переменная затем раскрывается, и это значение используется в остальной части замещения, а не в значении самого параметра.
Yorik.sar

1
@syntaxerror: вы можете назначать значения столько, сколько захотите, с помощью команды "Declare" выше.
Yorik.sar

48

Помимо ассоциативных массивов, в 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 ''позволяет задавать значения нескольких строк. Из-за этой последней опции команда возвращается с ненулевым кодом выхода.

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

Недостатки:

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

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!

Источник:


1
Это лучший ответ, особенно с учетом ${!varname} метод требует промежуточной переменной для varname.
RichVel

Трудно понять, что за этот ответ не проголосовали выше
Маркос

18

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

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

4
Вложение двух echos с подстановкой команд (которая пропускает кавычки) не требуется. Плюс, опция -nдолжна быть предоставлена echo. И, как всегда, evalнебезопасно. Но все это не нужно , поскольку Bash имеет более безопасный, более четкий и более короткий синтаксис для этой цели: ${!var}.
Maëlan

4

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

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"

4

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

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


3

использование 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

1
Это выглядит как очень чистое решение. Никаких злых нагрудников и бобов, и вы используете инструменты, которые связаны с переменными, а не неясные, казалось бы, не связанные или даже опасные функции, такие как printfилиeval
kvantour

2

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

#!/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 .


2
АБС - это тот, кто известен тем, что демонстрирует плохие практики на своих примерах. Пожалуйста, рассмотрите возможность использования вики bash-hackers или вики Wooledge, в которой есть запись непосредственно по теме BashFAQ # 6 .
Чарльз Даффи

2
Это работает, только если записи в foo_1и foo_2свободны от пробелов и специальных символов. Примеры проблемных записей: 'a b'создадут две записи внутри mine. ''не будет создавать запись внутри mine. '*'развернем до содержимого рабочего каталога. Вы можете предотвратить эти проблемы, цитируя:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Это общая проблема с циклическим перебором любого массива в BASH. Это также может быть решено путем временного изменения IFS (и затем, конечно, его изменения обратно). Приятно видеть, что цитата сработала.
ingyhere

@ingyhere, я умоляю отличаться. Это не общая проблема. Существует стандартное решение: всегда заключать в кавычки [@]конструкции. "${array[@]}"всегда будет расширяться до правильного списка записей без проблем, таких как разбиение слов или расширение *. Кроме того, проблему разделения слов можно обойти, только IFSесли вы знаете любой ненулевой символ, который никогда не появляется внутри массива. Кроме того, буквальное обращение *не может быть достигнуто установкой IFS. Либо вы устанавливаете IFS='*'и разделяете на звезды, либо вы устанавливаете IFS=somethingOtherи *расширяетесь.
Socowi

@Socowi Вы предполагаете, что расширение оболочки нежелательно, и это не всегда так. Разработчики жалуются на ошибки, когда выражения оболочки не раскрываются после цитирования всего. Хорошее решение - знать данные и создавать сценарии соответствующим образом, даже используя |или LFкак IFS. Опять же, общая проблема в циклах заключается в том, что токенизация происходит по умолчанию, так что заключение в кавычки является специальным решением, позволяющим использовать расширенные строки, содержащие токены. (Это либо расширение, либо расширение параметров, либо заключенные в кавычки расширенные строки, но не оба.) Если для чтения переменной var требуется 8 кавычек, shell - неправильный язык.
ingyhere

1

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

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


1

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

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

0

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

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:

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


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


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

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

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

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

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


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.