Передача аргументов по ссылке - PullRequest
36 голосов
/ 12 февраля 2009

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

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

Любые мысли приветствуются.

Ответы [ 9 ]

23 голосов
/ 13 февраля 2013

со страницы руководства 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
16 голосов
/ 18 мая 2010

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

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

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

12 голосов
/ 12 февраля 2009

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

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

11 голосов
/ 12 февраля 2009

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

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

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

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

4 голосов
/ 22 сентября 2016

Хорошо, поэтому этот вопрос уже некоторое время ждет «реального» решения, и я рад сообщить, что теперь мы можем сделать это, не используя 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
2 голосов
/ 10 октября 2014
#!/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.

2 голосов
/ 13 февраля 2009

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

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

$ y=four
$ four=4
$ echo ${!y}
4
$ foo() { x=$1; echo ${!x}; }
$ foo four
4
0 голосов
/ 05 июля 2017

Это то, что у меня работает в оболочке 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
...