Передача аргументов по ссылке

Я хочу спросить, можно ли передать аргументы функции скрипта по ссылке:

то есть сделать что-то похожее на 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

Любые мысли будут оценены.

Ответ 1

Это 2018 год, и этот вопрос заслуживает обновления. По крайней мере, в Bash, начиная с Bash 4.3-alpha, вы можете использовать nameref для передачи аргументов функции по ссылке:

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 для объявления nameref к переменной с именем $1, что означает, что это не ссылка на сам $1, а скорее на переменную, имя которой содержит $1, т.е. в нашем случае SOME_VAR. Значение в правой части должно быть просто строкой с именем существующей переменной: не имеет значения, как вы получаете строку, поэтому такие вещи, как local -n ref="my_var" или local -n ref=$(get_var_name) тоже будет работать. declare также может заменить local в контекстах, которые позволяют/требуют этого. См. Главу о параметрах оболочки в Справочном руководстве Bash для получения дополнительной информации.

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

Ответ 2

Из man-страницы 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

Ответ 3

Используйте вспомогательную функцию 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

Ответ 4

Я нашел способ сделать это, но я не уверен, насколько это правильно:

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...

Ответ 5

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

boo() {
    eval ${1}="new"
}

SOME_VAR="old"
echo $SOME_VAR # old
boo "SOME_VAR"
echo $SOME_VAR # new

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

Ответ 6

Хорошо, поэтому этот вопрос уже давно ждет "реального" решения, и я рад сказать, что теперь мы можем выполнить это без использования 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

Ответ 7

Eval никогда не следует использовать в строке, которую пользователь может установить, потому что это опасно. Что-то вроде "string; rm -rf ~" будет плохой. Поэтому обычно лучше всего найти решения, в которых вам не нужно беспокоиться об этом.

Тем не менее, eval будет необходим для установки переданных переменных, как отметил комментарий.

$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4

Ответ 8

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

Имя переменной передается в fuction о PETS и SEP, а строка для добавления передается обычным способом как значение. "$ {! 1}" относится к содержимому глобальной переменной PETS. Вначале эта переменная пуста и contens добавляется каждый раз, когда мы вызываем функцию. Символ разделителя можно выбрать по мере необходимости. Начальные строки "eval" обновляют переменную PETS.

Ответ 9

Это то, что работает для меня на Ubuntu bash shell

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