Динамические имена переменных в Bash
Я запутался в bash script.
У меня есть следующий код:
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
и как мне его называть в script?
Я пробовал eval
, ${...}
, \$${...}
, но я все еще запутался.
Ответы
Ответ 1
Используйте ассоциативный массив с именами команд в качестве ключей.
# 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: косвенное обращение - оценка косвенных/ссылочных переменных.
Ответ 2
Я искал лучший способ сделать это недавно. Ассоциативный массив звучал как переборщик для меня. Посмотрите, что я нашел:
suffix=bzz
declare prefix_$suffix=mystr
... и затем...
varname=prefix_$suffix
echo ${!varname}
Ответ 3
Пример ниже возвращает значение $name_of_var
var=name_of_var
echo $(eval echo "\$$var")
Ответ 4
Помимо ассоциативных массивов, в 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'
Downsides:
- не проверяет правильность имени переменной.
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'
Downsides:
- Менее портативный (но хорошо).
Способ 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!
Источник:
Ответ 5
Это должно работать:
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"
Ответ 6
Это тоже будет работать
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
Ответ 7
Вау, большая часть синтаксиса ужасна! Вот одно решение с более простым синтаксисом, если вам нужно косвенно ссылаться на массивы:
#!/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.
Ответ 8
Для индексированных массивов вы можете ссылаться на них следующим образом:
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
нужно включить -a
на declare
.
Ответ 9
Я хочу иметь возможность создать имя переменной, содержащей первый аргумент команды
файл 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 внутри параметров часто выполняется неправильно и в неподходящих ситуациях для решения проблем, которые имеют лучшие решения. Это нарушает разделение кода и данных и, как таковое, ставит вас на скользкий путь к ошибкам и проблемам безопасности. Непрямость может сделать ваш код менее прозрачным и трудным для понимания.
Ответ 10
Согласно BashFAQ/006, вы можете использовать синтаксис read
with here для назначения косвенных переменных:
function grep_search() {
read "$1" <<<$(ls | tail -1);
}
Использование:
$ grep_search open_box
$ echo $open_box
stack-overflow.txt
Ответ 11
Используйте 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 "[email protected]"; do declare "${v%=*}=${v#*=}"; done
Дополнительные советы
# parse argv leading key=value parameters
for v in "[email protected]"; do
case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv leading key=value parameters
while (( $# )); do
case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
shift
done
Ответ 12
для формата varname=$prefix_suffix
, просто используйте:
varname=${prefix}_suffix