【问题标题】:Passing arguments by reference通过引用传递参数
【发布时间】:2010-10-07 02:52:08
【问题描述】:

我想问是否可以通过引用将参数传递给脚本函数:

即在 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

任何想法将不胜感激。

【问题讨论】:

    标签: bash shell scripting pass-by-reference


    【解决方案1】:

    我找到了一种方法,但我不确定这是否正确:

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

    【讨论】:

      【解决方案2】:

      Bash 没有内置任何类似引用的东西,所以基本上你能够做你想做的事情的唯一方法就是将你想要修改的全局变量的名称传递给函数。即使这样,您也需要eval 声明:

      boo() {
          eval ${1}="new"
      }
      
      SOME_VAR="old"
      echo $SOME_VAR # old
      boo "SOME_VAR"
      echo $SOME_VAR # new
      

      我认为你不能在这里使用间接引用,因为 Bash 会自动访问名称存储在间接引用中的变量的值。它没有给你设置它的机会。

      【讨论】:

      • 这就是我的想法,但是对于 bash 新手来说,像“通过引用”一样传递变量似乎很奇怪,你必须传递带引号的变量名......
      【解决方案3】:

      不应在用户可以设置的字符串上使用 Eval,因为它很危险。像 "string; rm -rf ~" 这样的东西会很糟糕。因此,通常最好找到您不必担心的解决方案。

      但是,正如注释所述,需要 eval 来设置传递的变量。

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

      【讨论】:

      • 但是你可以用它来设置变量,即${!x}=5
      • 当涉及到用户输入时,间接几乎和 eval 一样糟糕,因为 bash 间接处理扩展。 f() { local x=${!1}; }; f 'a[$(echo hi >&2)]'。只需确保输入永远不会作为算术处理,这在 Bash 中可能比大多数人意识到的要难。
      【解决方案4】:

      使用辅助函数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 不能分配数组。

      此外,如果变量恰好声明为localevalprintf 都将不起作用:

      g() { local b; eval $1=bar; }  # WRONG
      g b                            # Conflicts with `local b'
      echo $b                        # b is empty unexpected
      

      即使local bunset,冲突仍然存在:

      g() { local b; unset b; eval $1=bar; }  # WRONG
      g b                                     # Still conflicts with `local b'
      echo $b                                 # b is empty unexpected
      

      【讨论】:

        【解决方案5】:

        来自 Bash 手册页(参数扩展):

        如果参数的第一个字符是感叹号 (!),则 引入了变量间接级别。 Bash 使用的值 由参数的其余部分形成的变量作为 多变的;然后扩展此变量并将该值用于 其余的替换,而不是参数的值 本身。这称为间接扩展。

        因此,引用是变量的名称。这是一个swap 函数使用 不需要临时变量的变量间接:

        function swap()
        {   # 
            # @param VARNAME1 VARNAME2
            #
            eval "$1=${!2} $2=${!1}"
        }
        
        $ a=1 b=2
        $ swap a b
        $ echo $a $b
        2 1
        

        【讨论】:

          【解决方案6】:
          #!/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 变量。

          【讨论】:

            【解决方案7】:

            好的,所以这个问题一直在等待一个“真正的”解决方案,我很高兴地说,我们现在可以完全不使用 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}"
                
            }
            

            运行时的输出:

            referenceTester:
                    the value of my reference is:
                            I am a variable
            passedByRef:
                    the value of my reference is:
                            I am a variable
            passedByRef:
                    value of my reference set to:
                            some other value
            referenceTester:
                    the value of my reference is now:
                            some other value,
                    and the pointed to variable:
                            some other value
            

            【讨论】:

              【解决方案8】:

              这在 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
              

              【讨论】:

                【解决方案9】:

                现在是 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 来为$1命名的变量声明一个nameref,这意味着它不是对$1的引用本身,而是一个变量其名称 $1 持有,即SOME_VAR 在我们的例子中。右侧的值应该只是一个命名现有变量的字符串:无论您如何获取字符串,local -n ref="my_var"local -n ref=$(get_var_name) 之类的东西也可以工作。 declare 也可以在允许/要求的上下文中替换 local。请参阅chapter on Shell Parameters in Bash Reference Manual 了解更多信息。

                这种方法的优点是(可以说)更好的可读性,最重要的是,避免了eval,它的安全陷阱很多并且有据可查。

                【讨论】:

                • 请注意这是在哪个版本的 bash 中引入的,因为我有一个旧版本并且没有此功能。
                • Interwebz 告诉我这个功能是在 Bash 4.3-alpha 中引入的:wiki.bash-hackers.org/commands/builtin/declare
                • 重要的是,本地名称或函数中的任何其他本地变量都不能与参数一致,因为 bash 无法表明您实际上是指来自调用范围的变量。跨度>
                猜你喜欢
                • 2011-08-21
                • 2017-09-25
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2011-12-15
                • 2017-03-21
                相关资源
                最近更新 更多