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


104

我正在与此:

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'

但是结果一样。

您能解释一下为什么未修改吗?如何保存test1函数的回显ret并修改全局变量?


您需要返回问候吗?您可以回显$ e使其返回。还是回显您想要的所有内容,然后解析结果?

Answers:


98

当使用命令替换(即$(...)构造)时,您正在创建一个子shell。子Shell从其父Shell继承变量,但这只能以一种方式起作用-子Shell无法修改其父Shell的环境。您的变量e是在子外壳程序中设置的,而不是在父外壳程序中设置的。有两种方法可以将值从子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

感谢您的意思,但是我必须返回一个字符串数组,并且必须在函数中向两个全局字符串数组添加元素。
harrison4

3
您意识到,如果只运行函数而不将其分配给变量,则其中的所有全局变量都会更新。为什么不只返回函数中的字符串数组,而是在函数完成后将其分配给另一个变量,而不是返回字符串数组?

@JohnDoe:您不能从函数返回“字符串数组”。您所能做的就是打印一个字符串。但是,您可以执行以下操作:setarray() { declare -ag "$1=(a b c)"; }
rici 2014年

34

如果使用{fd}或,则需要bash 4.1 local -n

我希望其余的应该在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变量(或一般而言,环境)从父进程传递到子进程,但反之则不。

如果您进行输出捕获,则此操作通常在子外壳中运行,因此很难传递回变量。

甚至有人告诉你,不可能修复。这是错误的,但这是一个长期以来难以解决的问题。

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

这是有关操作方法的分步指南。

将变量传递回父母的外壳

有一种方法可以将变量传递回父母的外壳。但是,这是一条危险的路径,因为使用eval。如果做得不当,您会冒很多邪恶的危险。但是,如果操作正确,只要中没有bug,这是绝对安全的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上下文中安全地重用它。

但这是一个痛苦。

这不仅看起来难看,而且键入起来也很多,所以容易出错。只有一个错误,您注定要失败,对吗?

好吧,我们处于外壳程序级别,因此您可以对其进行改进。只需考虑您想要看到的接口,然后就可以实现它。

增强,外壳如何处理事物

让我们退后一步,考虑一些API,这些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的功能了,因为它很好地包装在函数中。

考虑一个易于重用的界面

第二件事是,我们要变得干燥(不要重复自己)。所以我们绝对不想键入类似

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这里不仅是多余的,它的错误倾向始终在正确的上下文repeate。如果在脚本中使用它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运行时执行。
    • 如果您添加一个sleep 1;before printf或,则您可以自己查看_passback_xcapture a d; echo然后分别输出xa先输出。
  • _passback x不应该成为其中的一部分_xcapture,因为这使得它很难重用那几招。
  • 另外,我们在这里($(cat))有一些未分类的叉子,但是由于此解决方案是!DO NOT USE!我走的最短路线。

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

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

但是,这样做通常不是很容易理解。而且,如果您在几年后回到自己的脚本中,则可能希望能够再次阅读它而没有太多麻烦。

修理比赛

现在,让我们解决竞赛条件。

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

有很多方法可以对此进行存档:

  • 您不能使用外壳管道,因为管道在不同的进程中运行。
  • 一个可以使用临时文件,
  • 或类似锁定文件或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独立地看到如何printf实现的伪像。

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

这样就解决了竞争问题,牺牲了一个固定文件描述符3。当然,在FD3在您的shellscript中不可用的情况下,您当然可以选择另一个文件描述符。

另请注意,3<&-它保护FD3传递给该功能。

使它更通用

_captured()从可重用性的角度来看,包含的部分属于,这是不好的。如何解决呢?

好吧,通过引入更多的东西(一个附加的函数,必须返回正确的东西)来以绝望的方式进行操作,该函数以_附带原始功能的名字命名。

该函数在实函数之后被调用,并且可以扩充事物。这样,就可以将其作为一些注释来阅读,因此非常易读:

_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<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }

  • 该解决方案通过内部使用来污染文件描述符(此处为3)。如果碰巧通过FD,则需要牢记这一点。
    请注意,bash4.1及更高版本必须{fd}使用一些未使用的FD。
    (也许我来后会在这里添加解决方案。)
    请注意,这就是为什么我习惯将其放在单独的函数中,例如_capture,因为将所有这些填充到一行中是可能的,但是却使阅读和理解变得越来越困难

  • 也许您也想捕获被调用函数的STDERR。或者,您甚至想在变量之间传入和传出多个文件描述符。
    我还没有解决方案,但是这是一种捕获多个FD的方法,因此我们也可以以这种方式传递变量。

也不要忘记:

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

没有简单的方法可以从外部命令中传递环境变量。(LD_PRELOAD=尽管应该有可能!)但是,这是完全不同的事情。

最后的话

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

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

这里介绍的解决方案还远远不够完美:

  • 它几乎根本不是睾丸,所以请原谅错别字。
  • 有很多改进的余地,请参见上文。
  • 它使用了来自modern的许多功能bash,因此可能很难移植到其他shell。
  • 可能有一些我没想到的怪癖。

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

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

2
您真棒
Eliran Malka

14

也许您可以使用文件,在函数内部写入文件,然后从文件中读取文件。我已更改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

13

您在做什么,正在执行test1

$(test1)

在sub-shell(child shell)中,而child shell无法修改parent中的任何内容

您可以在bash 手册中找到它

请检查:事情在这里导致子外壳


7

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

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

有效并且对“返回值”没有任何限制。


1

这是因为命令替换是在子Shell中执行的,因此,尽管子Shell继承了变量,但在子Shell结束时,对变量的更改将丢失。

参考

在与Shell环境重复的子Shell环境中调用命令替换,用括号分组的命令和异步命令


@JohnDoe我不确定是否有可能。您可能需要重新考虑脚本的设计。
2014年

哦,但是我需要在一个函数中添加一个全局数组,否则,我将不得不重复很多代码(将函数的代码重复-30行-15次-每次调用一次)。没有别的办法了,不是吗?
harrison4

1

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

当我不得不在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命令以在一次写操作和读操作之间将内容保留在磁盘上。


-1

您始终可以使用别名:

alias next='printf "blah_%02d" $count;count=$((count+1))'
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.