Передача аргументов по ссылке
-
22-08-2019 - |
Вопрос
Хочу спросить, можно ли передавать аргументы функции скрипта по ссылке:
то естьсделать что-то, что на C++ будет выглядеть так:
void boo(int &myint) { myint = 5; }
int main() {
int t = 4;
printf("%d\n", t); // t->4
boo(t);
printf("%d\n", t); // t->5
}
Итак, в BASH я хочу сделать что-то вроде:
function boo ()
{
var1=$1 # now var1 is global to the script but using it outside
# this function makes me lose encapsulation
local var2=$1 # so i should use a local variable ... but how to pass it back?
var2='new' # only changes the local copy
#$1='new' this is wrong of course ...
# ${!1}='new' # can i somehow use indirect reference?
}
# call boo
SOME_VAR='old'
echo $SOME_VAR # -> old
boo "$SOME_VAR"
echo $SOME_VAR # -> new
Любые мысли будут оценены по достоинству.
Решение
На дворе 2018 год, и этот вопрос заслуживает обновления.По крайней мере, в Bash, начиная с Bash 4.3-альфа, вы можете использовать имена для передачи аргументов функции по ссылке:
function boo()
{
local -n ref=$1
ref='new'
}
SOME_VAR='old'
echo $SOME_VAR # -> old
boo SOME_VAR
echo $SOME_VAR # -> new
Важнейшими моментами здесь являются:
Передача имени переменной в boo, а не ее значения:
boo SOME_VAR
, нетboo $SOME_VAR
.Внутри функции, используя
local -n ref=$1
объявить имяref к переменной, названной$1
, то есть это не ссылка на$1
себе, а скорее переменной Чье имя$1
держится, т.е.SOME_VAR
в нашем случае.Значение в правой части должно быть просто строкой, именующей существующую переменную:не имеет значения, как вы получите строку, поэтому такие вещи, какlocal -n ref="my_var"
илиlocal -n ref=$(get_var_name)
тоже будет работать.declare
также можно заменитьlocal
в контекстах, которые позволяют/требуют этого.Видеть глава о параметрах оболочки в справочном руководстве Bash Чтобы получить больше информации.
Преимуществом этого подхода является (возможно) лучшая читаемость и, самое главное, отсутствие eval
, чьи ошибки безопасности многочисленны и хорошо документированы.
Другие советы
На странице руководства Bash (расширение параметров):
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion.
Следовательно, ссылка — это имя переменной.Вот swap
Функция с использованием переменной. Крайнация, которая не требует временной переменной:
function swap()
{ #
# @param VARNAME1 VARNAME2
#
eval "$1=${!2} $2=${!1}"
}
$ a=1 b=2
$ swap a b
$ echo $a $b
2 1
Используйте вспомогательную функцию upvar
:
# Assign variable one scope above the caller.
# Usage: local "$1" && upvar $1 value [value ...]
# Param: $1 Variable name to assign value to
# Param: $* Value(s) to assign. If multiple values, an array is
# assigned, otherwise a single value is assigned.
# NOTE: For assigning multiple variables, use 'upvars'. Do NOT
# use multiple 'upvar' calls, since one 'upvar' call might
# reassign a variable to be used by another 'upvar' call.
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
upvar() {
if unset -v "$1"; then # Unset & validate varname
if (( $# == 2 )); then
eval $1=\"\$2\" # Return single value
else
eval $1=\(\"\${@:2}\"\) # Return array
fi
fi
}
И используйте его вот так изнутри Newfun()
:
local "$1" && upvar $1 new
Для возврата нескольких переменных используйте другую вспомогательную функцию. upvars
.Это позволяет передавать несколько переменных в одном вызове, избегая тем самым возможных конфликтов, если одна upvar
вызов изменяет переменную, используемую в другом последующем upvar
вызов.
Видеть: http://www.fvue.nl/wiki/Bash:_Passing_variables_by_reference для вспомогательной функции upvars
и дополнительную информацию.
Проблема с:
eval $1=new
это небезопасно, если $1
случается, содержит команду:
set -- 'ls /;true'
eval $1=new # Oops
Было бы лучше использовать printf -v
:
printf -v "$1" %s new
Но printf -v
не может присваивать массивы.
Более того, оба eval
и printf
не будет работать, если переменная будет объявлена local
:
g() { local b; eval $1=bar; } # WRONG
g b # Conflicts with `local b'
echo $b # b is empty unexpected
Конфликт остается, даже если local b
является unset
:
g() { local b; unset b; eval $1=bar; } # WRONG
g b # Still conflicts with `local b'
echo $b # b is empty unexpected
Я нашел способ сделать это, но не уверен, насколько это правильно:
Newfun()
{
local var1="$1"
eval $var1=2
# or can do eval $1=2 if no local var
}
var=1
echo var is $var # $var = 1
newfun 'var' # pass the name of the variable…
echo now var is $var # $var = 2
Поэтому мы передаем имя переменной, а не значение, а затем используем eval...
Bash не имеет встроенных ссылок, поэтому единственный способ сделать то, что вы хотите, — это передать функции имя глобальной переменной, которую вы хотите изменить.И даже тогда вам понадобится eval
заявление:
boo() {
eval ${1}="new"
}
SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new
Я не думаю, что здесь можно использовать косвенные ссылки, поскольку Bash автоматически обращается к значению переменной, имя которой хранится в косвенной ссылке.Это не дает вам возможности установить его.
Итак, этот вопрос уже некоторое время ждет «реального» решения, и я рад сообщить, что теперь мы можем сделать это вообще без использования eval.
А ключ помнить, что нужно объявить ссылку как в вызывающем, так и в вызывающем, по крайней мере, в моем примере:
#!/bin/bash
# NOTE this does require a bash version >= 4.3
set -o errexit -o nounset -o posix -o pipefail
passedByRef() {
local -n theRef
if [ 0 -lt $# ]; then
theRef=$1
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theRef}"
# now that we have a reference, we can assign things to it
theRef="some other value"
echo -e "${FUNCNAME}:\n\tvalue of my reference set to:\n\t\t${theRef}"
else
echo "Error: missing argument"
exit 1
fi
}
referenceTester() {
local theVariable="I am a variable"
# note the absence of quoting and escaping etc.
local -n theReference=theVariable
echo -e "${FUNCNAME}:\n\tthe value of my reference is:\n\t\t${theReference}"
passedByRef theReference
echo -e "${FUNCNAME}:\n\tthe value of my reference is now:\n\t\t${theReference},\n\tand the pointed to variable:\n\t\t${theVariable}"
}
# run it
referenceTester
Eval никогда не следует использовать для строки, которую может установить пользователь, поскольку это опасно.Что-то вроде «строка;rm -rf ~» будет плохо.Поэтому, как правило, лучше найти решения, при которых вам не придется об этом беспокоиться.
Однако, как отмечается в комментарии, для установки переданных переменных потребуется eval.
$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
#!/bin/bash
append_string()
{
if [ -z "${!1}" ]; then
eval "${1}='$2'"
else
eval "${1}='${!1}''${!3}''$2'"
fi
}
PETS=''
SEP='|'
append_string "PETS" "cat" "SEP"
echo "$PETS"
append_string "PETS" "dog" "SEP"
echo "$PETS"
append_string "PETS" "hamster" "SEP"
echo "$PETS"
Выход:
cat
cat|dog
cat|dog|hamster
Структура вызова этой функции:
append_string name_of_var_to_update string_to_add name_of_var_containing_sep_char
Имя переменной передается в функцию по PETS и SEP, а добавляемая строка передается обычным способом в качестве значения.«${!1}» относится к содержимому глобальной переменной PETS.Вначале эта переменная пуста, и содержимое добавляется каждый раз, когда мы вызываем функцию.Символ-разделитель можно выбрать по необходимости.Начальные строки «eval» обновляют переменную PETS.
Это то, что у меня работает в оболочке Ubuntu bash
#!/bin/sh
iteration=10
increment_count()
{
local i
i=$(($1+1))
eval ${1}=\$i
}
increment_count iteration
echo $iteration #prints 11
increment_count iteration
echo $iteration #prints 12