如何在 bash 中修改函数中的全局变量?

我在研究这个:

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

在这个例子中,我听说过 使用 eval,所以我在 test1中这样做:

eval 'e=4'

但结果是一样的。

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

262194 次浏览

这是因为指令替代是在 subshell 中执行的,所以当 subshell 继承变量时,当 subshell 结束时,对变量的更改就会丢失。

参考文献 :

指令替代 ,用括号分组的命令和异步命令在子 shell 环境中调用,子 shell 环境是 shell 环境的副本

当你使用一个指令替代(即 $(...)结构)时,你正在创建一个子 shell。子 shell 从它们的父 shell 继承变量,但是这只有一种方式: 子 shell 不能修改其父 shell 的环境。

变量 e在子 shell 中设置,但不在父 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

您正在执行的是 test1

$(test1)

在子 shell (子 shell)和 子 shell 不能修改父级中的任何内容中。

您可以在 bash 译自: 美国《科学》杂志网站(http://www.gnu.org/software/bash/Manual/bashref.html)原文地址: http://www.gnu.org/software/bash/Manual/bashref.html中找到它

请检查: 事情的结果是子 shell 这里

也许你可以用一个文件,写到文件内部的函数,从文件后面读取它。我已将 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

当我想删除自动创建的临时文件时,也遇到了类似的问题。我想到的解决方案不是使用指令替代,而是将变量的名称传递给函数,这样就可以得到最终的结果。例如。

#!/usr/bin/env bash


# array that keeps track of tmp-files
remove_later=()


# function that manages tmp-files
new_tmp_file() {
file=$(mktemp)
remove_later+=( "$file" )
# assign value (safe form of `eval "$1=$file"`)
printf -v "$1" -- "$file"
}


# function to remove all tmp-files
remove_tmp_files() { rm -- "${remove_later[@]}"; }


# define trap to remove all tmp-files upon EXIT
trap remove_tmp_files EXIT


# generate tmp-files
new_tmp_file tmpfile1
new_tmp_file tmpfile2

所以,把这个应用到 OP 中,应该是:

#!/usr/bin/env bash
    

e=2
    

function test1() {
e=4
printf -v "$1" -- "hello"
}
    

test1 ret
    

echo "$ret"
echo "$e"

工作,并且对“返回值”没有限制。

你可以用化名:

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

如果使用 {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中没有 bug。

_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不工作或共同的 bash3. x)

避免改变 d()

最后一种解决方案存在一些重大缺陷:

  • d()需要修改
  • 它需要使用 xcapture的一些内部细节来传递输出。
    • 注意,这个阴影(燃烧)一个名为 output的变量, 这样我们就不能把这个传回去了。
  • 它需要与 _passback合作

我们能把这个也扔了吗?

当然,我们可以! 我们是在一个壳,所以有一切,我们需要做到这一点。

如果你仔细观察一下对 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!标记在那里, 因为这里有一个非常糟糕的比赛环境, 你不容易看到的东西:
    • >(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

存档的方法有很多:

  • 您不能使用 shell 管道,因为管道运行在不同的进程中。
  • 可以使用临时文件,
  • 或者类似于锁文件或者 fifo 的东西。这允许等待锁或 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(当然你可以使用其他的) 然后与 >&3一起重复使用。
  • $("${@:2}" 3<&-; _passback x >&3)_passback之后完成, 当子 shell 关闭 STDOUT 时。
  • 所以 printf不能发生在 _passback之前, 不管 _passback需要多长时间。
  • 注意,在完成 命令行是组装的,所以我们不能看到来自 printf的工件, 如何独立实现 printf

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

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

还请注意保护传递给函数的 FD3的 3<&-

让它更通用

_capture包含部件,属于 d(),这是坏的, 从可重用性的角度来看。如何解决这个问题?

那就用最后一种方式再介绍一件事, 一个附加函数,它必须返回正确的内容, 它是根据附有 _的原始函数命名的。

这个函数是在实函数之后调用的,可以进行扩充。 通过这种方式,这可以被理解为一些注释,因此它非常易读:

_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() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }可以消除 _capture()

  • 解决方案通过在内部使用文件描述符(这里是3)来污染它。 如果你碰巧通过了 FD 考试,你要记住这一点。
    请注意,bash4.1及以上版本有 {fd}来使用一些未使用的 FD。
    (也许我回来的时候会在这里添加一个解决方案。)
    请注意,这就是为什么我要把它放在单独的函数中,比如 _capture,因为将它们全部放在一行中是可能的,但是这会使阅读和理解变得越来越困难

  • 也许您还想捕获被调用函数的 STDERR。 或者您甚至希望传入和传出多个文件描述符 从变量到变量。
    我还没有解决方案,但是 这里有一个方法可以抓住不止一个 FD,所以我们可能也可以通过这种方式返回变量。

也不要忘记:

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

从外部命令传递环境变量并不容易。 (不过,使用 LD_PRELOAD=应该是可行的!) 但这是完全不同的东西。

临终遗言

这不是唯一可能的解决方案,它是解决方案的一个例子。

一如既往,您有许多方法来表达 shell 中的内容。 所以请随意改进,找到更好的东西。

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

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

然而,我认为它很容易使用:

  • 只添加4行“库”。
  • 只需为 shell 函数添加一行“注释”即可。
  • 只暂时牺牲一个文件描述符。
  • 而且即使是多年以后,每一步都应该很容易理解。

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

当我不得不在一个 bat 测试用例中模拟多次调用 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命令,以便在一次写操作和一次读操作之间保存磁盘上的内容。

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

#!/bin/bash


e=2


function test1() {
local -n var=$1
var=4
echo "hello"
}


test1 e
echo "$e"

结果如下:

hello
4

我不确定这是否适用于你的终端,但我发现,如果你不提供任何输出,它会自然地被视为一个 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 ))
}

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