【问题标题】:How to modify a global variable within a function in bash?如何在bash中修改函数内的全局变量?
【发布时间】:2014-06-27 05:11:22
【问题描述】:

我正在处理这个:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

我有一个如下脚本:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

返回:

hello
4

但是如果我将函数的结果赋值给一个变量,全局变量e是不会被修改的:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

返回:

hello
2

在这种情况下我听说过the use of eval,所以我在test1中做了这个:

eval 'e=4'

但结果相同。

你能解释一下为什么它没有被修改吗?如何在ret 中保存test1 函数的回声并修改全局变量?

【问题讨论】:

  • 你需要回你好吗?你可以回显 $e 让它返回。还是回显您想要的所有内容,然后解析结果?

标签: bash variables global-variables eval


【解决方案1】:

这是因为命令替换是在子shell中执行的,所以当子shell继承变量时,对它们的更改会在子shell结束时丢失。

Reference:

命令替换、用括号分组的命令和异步命令在作为 shell 环境副本的子 shell 环境中调用

【讨论】:

  • @JohnDoe 我不确定这是否可能。您可能需要重新考虑脚本的设计。
  • 哦,但是我需要在函数中分配一个全局数组,如果没有,我必须重复很多代码(重复函数的代码-30行- 15次-一个每次通话-)。没有别的办法,不是吗?
【解决方案2】:

当您使用命令替换(即$(...) 构造)时,您正在创建一个子shell。子shell 从它们的父shell 继承变量,但这只适用于一种方式:子shell 不能修改其父shell 的环境。

您的变量e 设置在子shell 中,而不是父shell。有两种方法可以将值从子外壳传递到其父外壳。首先,您可以将某些内容输出到 stdout,然后使用命令替换来捕获它:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

以上输出:

Hello

对于 0 到 255 范围内的数值,可以使用return 将该数字作为退出状态传递:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

这个输出:

Hello - num is 4

【讨论】:

  • 感谢您的指点,但我必须返回一个字符串数组,并且在函数内我必须将元素添加到两个全局字符串数组。
  • 您意识到,如果您只运行该函数而不将其分配给变量,则其中的所有全局变量都会更新。为什么不直接在函数中更新字符串数组,然后在函数完成后将其分配给另一个变量,而不是返回一个字符串数组?
  • @JohnDoe:您不能从函数返回“字符串数组”。您所能做的就是打印一个字符串。但是,您可以这样做:setarray() { declare -ag "$1=(a b c)"; }
【解决方案3】:

你在做什么,你正在执行test1

$(test1)

在子外壳(子外壳)和子外壳不能修改父外壳中的任何内容

你可以在 bash 中找到它manual

请检查:事情导致子shell here

【讨论】:

    【解决方案4】:

    也许您可以使用文件,在函数内部写入文件,然后从文件中读取。我已将e 更改为一个数组。在这个例子中,当读回数组时,空格用作分隔符。

    #!/bin/bash
    
    declare -a e
    e[0]="first"
    e[1]="secondddd"
    
    function test1 () {
     e[2]="third"
     e[1]="second"
     echo "${e[@]}" > /tmp/tempout
     echo hi
    }
    
    ret=$(test1)
    
    echo "$ret"
    
    read -r -a e < /tmp/tempout
    echo "${e[@]}"
    echo "${e[0]}"
    echo "${e[1]}"
    echo "${e[2]}"
    

    输出:

    hi
    first second third
    first
    second
    third
    

    【讨论】:

      【解决方案5】:

      当我想自动删除我创建的临时文件时,我遇到了类似的问题。我想出的解决方案不是使用命令替换,而是将变量的名称(应该采用最终结果)传递给函数。例如

      #! /bin/bash
      
      remove_later=""
      new_tmp_file() {
          file=$(mktemp)
          remove_later="$remove_later $file"
          eval $1=$file
      }
      remove_tmp_files() {
          rm $remove_later
      }
      trap remove_tmp_files EXIT
      
      new_tmp_file tmpfile1
      new_tmp_file tmpfile2
      

      所以,在你的情况下,这将是:

      #!/bin/bash
      
      e=2
      
      function test1() {
        e=4
        eval $1="hello"
      }
      
      test1 ret
      
      echo "$ret"
      echo "$e"
      

      有效,对“返回值”没有限制。

      【讨论】:

        【解决方案6】:

        您始终可以使用别名:

        alias next='printf "blah_%02d" $count;count=$((count+1))'
        

        【讨论】:

          【解决方案7】:

          如果您使用 {fd}local -n,这需要 bash 4.1。

          我希望其余的应该在 bash 3.x 中工作。由于printf %q,我不完全确定——这可能是 bash 4 的功能。

          总结

          您的示例可以修改如下以存档所需的效果:

          # Add following 4 lines:
          _passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
          passback() { _passback "$@" "$?"; }
          _capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
          capture() { eval "$(_capture "$@")"; }
          
          e=2
          
          # Add following line, called "Annotation"
          function test1_() { passback e; }
          function test1() {
            e=4
            echo "hello"
          }
          
          # Change following line to:
          capture ret test1 
          
          echo "$ret"
          echo "$e"
          

          根据需要打印:

          hello
          4
          

          注意这个解决方案:

          • 也适用于e=1000
          • 如果您需要$?,请保留$?

          唯一不好的副作用是:

          • 它需要一个现代的bash
          • 它更频繁地分叉。
          • 它需要注解(以您的函数命名,并添加_
          • 它牺牲了文件描述符 3。
            • 如果需要,您可以将其更改为另一个 FD。
              • _capture 中,只需将所有出现的3 替换为另一个(更高的)数字。

          以下内容(很长,对此感到抱歉)希望能解释如何将此配方应用到其他脚本中。

          问题

          d() { let x++; date +%Y%m%d-%H%M%S; }
          
          x=0
          d1=$(d)
          d2=$(d)
          d3=$(d)
          d4=$(d)
          echo $x $d1 $d2 $d3 $d4
          

          输出

          0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
          

          而想要的输出是

          4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
          

          问题原因

          Shell 变量(或一般而言,环境)从父进程传递到子进程,但反之则不然。

          如果您进行输出捕获,这通常在子shell 中运行,因此传递回变量很困难。

          有些人甚至告诉你,这是不可能修复的。这是错误的,但这是一个众所周知的难以解决的问题。

          有几种方法可以最好地解决它,这取决于您的需求。

          这里是一步一步的指导如何做到这一点。

          将变量传回父shell

          有一种方法可以将变量传递回父 shell。然而,这是一条危险的道路,因为它使用了eval。如果操作不当,您将面临许多邪恶的风险。但如果处理得当,这是绝对安全的,前提是bash 中没有错误。

          _passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
          
          d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
          
          x=0
          eval `d`
          d1=$d
          eval `d`
          d2=$d
          eval `d`
          d3=$d
          eval `d`
          d4=$d
          echo $x $d1 $d2 $d3 $d4
          

          打印

          4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
          

          请注意,这也适用于危险的事情:

          danger() { danger="$*"; passback danger; }
          eval `danger '; /bin/echo *'`
          echo "$danger"
          

          打印

          ; /bin/echo *
          

          这是由于 printf '%q',它引用了所有内容,您可以在 shell 上下文中安全地重复使用它。

          但这是一种痛苦..

          这不仅看起来难看,而且打字很多,所以容易出错。只要一个错误,你就完蛋了,对吧?

          嗯,我们处于外壳级别,因此您可以改进它。只要想一个你想看到的接口,然后你就可以实现它。

          增强,shell 如何处理事物

          让我们退后一步,想想一些 API,它可以让我们轻松表达我们想要做什么。

          那么,我们想用d() 函数做什么?

          我们希望将输出捕获到一个变量中。 好的,那么让我们为此实现一个 API:

          # This needs a modern bash 4.3 (see "help declare" if "-n" is present,
          # we get rid of it below anyway).
          : capture VARIABLE command args..
          capture()
          {
          local -n output="$1"
          shift
          output="$("$@")"
          }
          

          现在,不用写了

          d1=$(d)
          

          我们可以写

          capture d1 d
          

          好吧,这看起来我们没有太大变化,因为变量没有从 d 传回父 shell,我们需要输入更多内容。

          但是现在我们可以将 shell 的全部功能投入其中,因为它很好地包装在一个函数中。

          考虑一个易于重用的界面

          第二件事是,我们希望保持干燥(不要重复自己)。 所以我们绝对不想输入类似

          x=0
          capture1 x d1 d
          capture1 x d2 d
          capture1 x d3 d
          capture1 x d4 d
          echo $x $d1 $d2 $d3 $d4
          

          这里的x 不仅是多余的,而且总是在正确的上下文中重复容易出错。如果你在一个脚本中使用它 1000 次然后添加一个变量会怎样?您绝对不想更改涉及调用 d 的所有 1000 个位置。

          所以把x放开,我们可以写:

          _passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
          
          d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
          
          xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
          
          x=0
          xcapture d1 d
          xcapture d2 d
          xcapture d3 d
          xcapture d4 d
          echo $x $d1 $d2 $d3 $d4
          

          输出

          4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
          

          这看起来已经很不错了。 (但仍有local -n 在其他常见的bash 3.x 中不起作用)

          避免更改d()

          最后一个解决方案有一些很大的缺陷:

          • d() 需要修改
          • 它需要使用xcapture 的一些内部细节来传递输出。
            • 请注意,这会遮蔽(烧毁)一个名为 output 的变量, 所以我们永远不能把这个传回去。
          • 需要配合_passback

          我们也可以摆脱这个吗?

          当然可以!我们在一个 shell 中,所以我们需要完成这一切。

          如果您更接近对eval 的调用,您会看到,我们在此位置拥有 100% 的控制权。 “内部”eval 我们在一个子外壳中, 这样我们就可以做我们想做的一切,而不必担心对父母的外壳做坏事。

          是的,很好,所以让我们添加另一个包装器,现在直接在 eval 中:

          _passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
          # !DO NOT USE!
          _xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; }  # !DO NOT USE!
          # !DO NOT USE!
          xcapture() { eval "$(_xcapture "$@")"; }
          
          d() { let x++; date +%Y%m%d-%H%M%S; }
          
          x=0
          xcapture d1 d
          xcapture d2 d
          xcapture d3 d
          xcapture d4 d
          echo $x $d1 $d2 $d3 $d4
          

          打印

          4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    
          

          然而,这也有一些主要缺点:

          • !DO NOT USE! 标记在那里, 因为这里有一个非常糟糕的比赛条件, 你不能轻易看到:
            • &gt;(printf ..) 是后台作业。所以可能还是 在_passback x 运行时执行。
            • 如果您在printf_passback 之前添加sleep 1;,您可以自己查看。 _xcapture a d; echo 然后分别输出xa
          • _passback x 不应该是_xcapture 的一部分, 因为这使得重复使用该配方变得困难。
          • 此外,我们这里还有一些未定义的分叉($(cat)), 但由于这个解决方案是!DO NOT USE!,所以我选择了最短的路线。

          但是,这表明我们可以做到,无需修改 d()(也无需修改 local -n)!

          请注意,我们根本不需要_xcapture, 因为我们可以在eval 中写下所有内容。

          但是这样做通常不是很可读。 如果你几年后回到你的剧本, 您可能希望能够轻松地再次阅读它。

          修复比赛

          现在让我们修复竞态条件。

          诀窍可能是等到printf 关闭它的STDOUT,然后输出x

          有很多方法可以存档:

          • 您不能使用外壳管道,因为管道在不同的进程中运行。
          • 可以使用临时文件,
          • 或类似锁定文件或fifo 的东西。这允许等待锁定或先进先出,
          • 或不同的通道,输出信息,然后以正确的顺序组合输出。

          遵循最后一条路径可能看起来像(注意它最后是printf,因为这在这里效果更好):

          _passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
          
          _xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
          
          xcapture() { eval "$(_xcapture "$@")"; }
          
          d() { let x++; date +%Y%m%d-%H%M%S; }
          
          x=0
          xcapture d1 d
          xcapture d2 d
          xcapture d3 d
          xcapture d4 d
          echo $x $d1 $d2 $d3 $d4
          

          输出

          4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
          

          为什么这是正确的?

          • _passback x 直接与 STDOUT 对话。
          • 但是,由于需要在内部命令中捕获 STDOUT, 我们首先使用 '3>&1' 将其“保存”到 FD3(当然,您可以使用其他) 然后用 &gt;&amp;3 重复使用它。
          • $("${@:2}" 3&lt;&amp;-; _passback x &gt;&amp;3)_passback 之后结束, 当 subshel​​l 关闭 STDOUT 时。
          • 所以printf 不能在_passback 之前发生, 不管_passback 需要多长时间。
          • 注意printf命令在完成前不会执行 命令行已组装,因此我们无法看到来自printf 的人工制品, 独立于printf 的实现方式。

          因此首先执行_passback,然后执行printf

          这解决了竞争,牺牲了一个固定的文件描述符 3。 当然,您可以在这种情况下选择另一个文件描述符, FD3 在你的 shellscript 中不是免费的。

          还请注意3&lt;&amp;-,它保护 FD3 被传递给函数。

          让它更通用

          _capture 包含部分,属于d(),这很糟糕, 从可重用性的角度来看。如何解决?

          好吧,通过引入另外一件事来做这种绝望的方式, 一个附加函数,它必须返回正确的东西, 它以带有_的原始函数命名。

          这个函数是在真正的函数之后调用的,可以扩充事物。 这样,this 就可以作为一些注解来读取,所以可读性非常好:

          _passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
          _capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
          capture() { eval "$(_capture "$@")"; }
          
          d_() { _passback x; }
          d() { let x++; date +%Y%m%d-%H%M%S; }
          
          x=0
          capture d1 d
          capture d2 d
          capture d3 d
          capture d4 d
          echo $x $d1 $d2 $d3 $d4
          

          仍然打印

          4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
          

          允许访问返回码

          只有一点点缺失:

          v=$(fn)$? 设置为 fn 返回的内容。所以你可能也想要这个。 不过,它需要一些更大的调整:

          # This is all the interface you need.
          # Remember, that this burns FD=3!
          _passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
          passback() { _passback "$@" "$?"; }
          _capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
          capture() { eval "$(_capture "$@")"; }
          
          # Here is your function, annotated with which sideffects it has.
          fails_() { passback x y; }
          fails() { x=$1; y=69; echo FAIL; return 23; }
          
          # And now the code which uses it all
          x=0
          y=0
          capture wtf fails 42
          echo $? $x $y $wtf
          

          打印

          23 42 69 FAIL
          

          还有很大的改进空间

          • _passback() 可以用passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; } 消除

          • _capture() 可以用capture() { eval "$({ out="$("${@:2}" 3&lt;&amp;-; "$2_" &gt;&amp;3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3&gt;&amp;1; echo "(exit $ret)")"; } 消除

          • 该解决方案通过在内部使用文件描述符(此处为 3)来污染它。 如果您碰巧通过了 FD,则需要牢记这一点。
            注意bash 4.1 及以上有{fd} 使用一些未使用的FD。
            (也许我会在这里添加一个解决方案。)
            请注意,这就是为什么我习惯将它放在单独的函数中,例如 _capture,因为将所有这些都放在一行中是可能的,但它会变得越来越难以阅读和理解

          • 也许您也想捕获被调用函数的 STDERR。 或者您甚至想传入和传出多个文件描述符 从变量到变量。
            我还没有解决方案,但是here is a way to catch more than one FD,所以我们也可以通过这种方式传回变量。

          也不要忘记:

          这必须调用一个shell函数,而不是一个外部命令。

          没有简单的方法可以将环境变量从外部命令中传递出去。 (不过,使用LD_PRELOAD= 应该是可能的!) 但这完全不同。

          遗言

          这不是唯一可能的解决方案。这是解决方案的一个示例。

          与往常一样,您可以通过多种方式在 shell 中表达事物。 因此,请随时改进并找到更好的东西。

          这里提出的解决方案远非完美:

          • 几乎没有经过测试,所以请原谅拼写错误。
          • 还有很大的改进空间,见上文。
          • 它使用了现代 bash 的许多功能,因此可能很难移植到其他 shell。
          • 可能还有一些我没有想到的怪癖。

          但是我认为它很容易使用:

          • 仅添加 4 行“库”。
          • 只需为您的 shell 函数添加 1 行“注释”。
          • 暂时只牺牲一个文件描述符。
          • 即使多年后,每个步骤也应该很容易理解。

          【讨论】:

          • 你太棒了
          • 我这辈子从来没有见过从这么多角度做出如此广泛的回复。我向你鞠躬@Tino
          【解决方案8】:

          解决这个问题的方法是,将值存储在一个临时文件中,并在需要时对其进行读取/写入,而无需引入复杂的函数并大量修改原始函数。

          当我不得不在 bats 测试用例中模拟多次调用的 bash 函数时,这种方法对我有很大帮助。

          例如,您可以:

          # Usage read_value path_to_tmp_file
          function read_value {
            cat "${1}"
          }
          
          # Usage: set_value path_to_tmp_file the_value
          function set_value {
            echo "${2}" > "${1}"
          }
          #----
          
          # Original code:
          
          function test1() {
            e=4
            set_value "${tmp_file}" "${e}"
            echo "hello"
          }
          
          
          # Create the temp file
          # Note that tmp_file is available in test1 as well
          tmp_file=$(mktemp)
          
          # Your logic
          e=2
          # Store the value
          set_value "${tmp_file}" "${e}"
          
          # Run test1
          test1
          
          # Read the value modified by test1
          e=$(read_value "${tmp_file}")
          echo "$e"
          

          缺点是您可能需要多个临时文件用于不同的变量。此外,您可能需要发出 sync 命令以在一次写入和读取操作之间将内容持久保存在磁盘上。

          【讨论】:

            【解决方案9】:

            假设local -n 可用,下面的脚本让函数test1 修改一个全局变量:

            #!/bin/bash
            
            e=2
            
            function test1() {
              local -n var=$1
              var=4
              echo "hello"
            }
            
            test1 e
            echo "$e"
            

            它给出以下输出:

            hello
            4
            

            【讨论】:

              【解决方案10】:

              我不确定这是否适用于您的终端,但我发现如果您不提供任何输出,它自然会被视为 void 函数,并且可以进行全局变量更改。 这是我使用的代码:

              let ran1=$(( (1<<63)-1)/3 ))
              let ran2=$(( (1<<63)-1)/5 ))
              let c=0
              function randomize {
                  c=$(( ran1+ran2 ))
                  ran2=$ran1
                  ran1=$c
                  c=$(( c > 0 ))
              }
              

              这是一个简单的游戏随机化器,可以有效地修改所需的变量。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2022-10-04
                • 2017-03-08
                • 1970-01-01
                • 2012-10-23
                • 1970-01-01
                相关资源
                最近更新 更多