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

har*_*on4 83 variables bash eval global-variables

我正在使用这个:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Run Code Online (Sandbox Code Playgroud)

我有一个如下脚本:

#!/bin/bash

e=2

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

test1 
echo "$e"
Run Code Online (Sandbox Code Playgroud)

哪个回报:

hello
4
Run Code Online (Sandbox Code Playgroud)

但是如果我将函数的结果赋给变量,e则不修改全局变量:

#!/bin/bash

e=2

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

ret=$(test1)

echo "$ret"
echo "$e"
Run Code Online (Sandbox Code Playgroud)

返回:

hello
2
Run Code Online (Sandbox Code Playgroud)

我听说过在这种情况下使用eval,所以我这样做test1:

eval 'e=4'
Run Code Online (Sandbox Code Playgroud)

但结果相同.

你能解释一下为什么不修改它吗?我怎么能保存test1函数的回声ret并修改全局变量呢?

Jos*_*lly 85

当您使用命令替换(即$(...)构造)时,您正在创建子shell.子shell从其父shell继承变量,但这只能以一种方式工作 - 子shell无法修改其父shell的环境.您的变量e在子shell中设置,但不在父shell中.将值从子shell传递到其父级有两种方法.首先,您可以输出一些内容到stdout,然后使用命令替换来捕获它:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"
Run Code Online (Sandbox Code Playgroud)

得到:

Hello
Run Code Online (Sandbox Code Playgroud)

对于0-255的数值,您可以使用return该数字作为退出状态:

mysecondfunc() {
    echo "Hello"
    return 4
}

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

echo "$var - num is $num_var"
Run Code Online (Sandbox Code Playgroud)

得到:

Hello - num is 4
Run Code Online (Sandbox Code Playgroud)

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

Tin*_*ino 27

摘要

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

# 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"
Run Code Online (Sandbox Code Playgroud)

根据需要打印:

hello
4
Run Code Online (Sandbox Code Playgroud)

请注意,此解决方案:

  • 也适用于{fd}.
  • local -n如果需要,保留printf %q

唯一不好的副作用是:

  • 它需要一个现代的e=1000.
  • 它更频繁地分叉.
  • 它需要注释(以您的函数命名,添加$?)
  • 它牺牲了文件描述符3.
    • 如果需要,您可以将其更改为另一个FD.
      • $?刚刚替换所有出现bash另一个(更高)的数量.

以下(很长时间,对不起)有希望解释,如何将此配方也用于其他脚本.

问题

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
Run Code Online (Sandbox Code Playgroud)

输出

0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Run Code Online (Sandbox Code Playgroud)

而想要的输出是

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Run Code Online (Sandbox Code Playgroud)

问题的原因

Shell变量(或一般来说,环境)从父进程传递到子进程,但反之亦然.

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

有些人甚至告诉你,这是不可能解决的.这是错误的,但长期以来一直难以解决问题.

如何最好地解决它有几种方法,这取决于您的需求.

这是一个如何做的分步指南.

将变量传递回父shell

有一种方法可以将变量传递给父shell.然而,这是一条危险的道路,因为它使用了_.如果做得不正确,你会冒许多危险的事情.但如果做得好,这是完全安全的,前提是没有错误_capture.

_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
Run Code Online (Sandbox Code Playgroud)

版画

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Run Code Online (Sandbox Code Playgroud)

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

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
Run Code Online (Sandbox Code Playgroud)

版画

; /bin/echo *
Run Code Online (Sandbox Code Playgroud)

这是因为3它引用了所有这些,你可以安全地在shell上下文中重复使用它.

但这是一个痛苦的...

这不仅看起来很难看,而且输入也很多,因此很容易出错.只有一个错误,你注定要失败,对吗?

好吧,我们处于shell级别,所以你可以改进它.只需考虑一下您想要查看的界面,然后就可以实现它.

扩充,shell如何处理事情

让我们退一步思考一些API,它可以让我们轻松表达,我们想做什么.

那么,我们想要做什么eval呢?

我们想将输出捕获到变量中.好的,那么让我们实现一个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="$("$@")"
}
Run Code Online (Sandbox Code Playgroud)

现在,而不是写作

d1=$(d)
Run Code Online (Sandbox Code Playgroud)

我们可以写

capture d1 d
Run Code Online (Sandbox Code Playgroud)

好吧,这看起来我们没有太大变化,因为变量不会再传递回bash父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
Run Code Online (Sandbox Code Playgroud)

printf '%q'这里不仅是多余的,这是容易出错,以在正确的上下文总是repeate.如果在脚本中使用1000次然后添加变量怎么办?你明确地不想改变d()涉及呼叫的所有1000个地点.

所以请d离开,所以我们可以写:

_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
Run Code Online (Sandbox Code Playgroud)

输出

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Run Code Online (Sandbox Code Playgroud)

这看起来非常好.

避免改变 x

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

  • d 需要改变
  • 它需要使用一些内部细节x来传递输出.
    • 请注意,这个阴影(烧伤​​)一个变量命名local -n,所以我们永远不能将这个变回来.
  • 它需要配合 bash

我们也可以摆脱这个吗?

我们当然可以!我们处于一个shell中,因此我们需要完成所有这些工作.

如果你看起来更接近d()你的电话,你可以看到,我们在这个位置有100%的控制权."内部" d()我们在子壳中,所以我们可以做我们想做的一切,而不用担心对父母的外壳做坏事.

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

_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
Run Code Online (Sandbox Code Playgroud)

版画

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    
Run Code Online (Sandbox Code Playgroud)

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

  • output标记是有,因为在这个非常糟糕的比赛条件,你可以不看很容易:
    • _passback是一份后台工作.所以在eval运行时它仍然可以执行.
    • 你可以自己看看,如果你添加eval之前eval!DO NOT USE!. >(printf ..)然后输出_passback xsleep 1;分别输出.
  • printf不应该是其中的一部分_passback,因为这使得重用该配方变得困难.
  • 此外,我们在这里有一些不需要的分叉(_xcapture a d; echo),但是这个解决方案是x我采用了最短的路线.

但是,这表明,我们可以做到,而无需修改a!

请注意,我们根本不需要_passback x,因为我们可能已经写好了_xcapture.

但是这样做通常不是很易读.如果你在几年内回到你的脚本,你可能希望能够再次阅读它而不会有太多麻烦.

修复比赛

现在让我们来解决竞争状况.

诀窍可能是等到$(cat)关闭它的STDOUT,然后输出!DO NOT USE!.

存档的方法有很多种:

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

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

_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
Run Code Online (Sandbox Code Playgroud)

输出

4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Run Code Online (Sandbox Code Playgroud)

为什么这是正确的?

  • local -n 直接与STDOUT交谈.
  • 但是,由于STDOUT需要在内部命令中捕获,我们首先将它"保存"到FD3(当然,你可以使用其他的)和'3>&1',然后重复使用它_xcapture.
  • eval后结束printf,当子shell关闭STDOUT.
  • 所以x不可能在之前发生printf,无论_passback x需要多长时间.
  • 请注意,在>&3组装完整的命令行之前不会执行该命令,因此我们无法$("${@:2}" 3<&-; _passback x >&3)独立地看到伪像如何_passback实现.

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

这解决了竞争,牺牲了一个固定文件描述符3.当然,你可以选择另一个文件描述符,你的shellcript中FD3不是免费的.

还请注意_passback哪个保护FD3传递给该功能.

使它更通用

printfprintf从可重用性的角度来看,它包含属于哪个部分,这些部分很糟糕.怎么解决这个?

好吧,通过引入一个额外的功能,一个额外的功能,它必须返回正确的东西,这是以原始函数printf附加命名的绝对方式.

这个函数在真实函数之后被调用,并且可以增加事物.这样,这可以作为一些注释读取,因此它非常易读:

_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
Run Code Online (Sandbox Code Playgroud)

仍然打印

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Run Code Online (Sandbox Code Playgroud)

允许访问返回码

只有一点点缺失:

_passback设置printf3<&-返回的内容.所以你可能也想要这个.但它需要更大的调整:

# 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
Run Code Online (Sandbox Code Playgroud)

版画

23 42 69 FAIL
Run Code Online (Sandbox Code Playgroud)

仍有很大的改进空间

  • 该解决方案在内部使用它来污染文件描述符.如果您需要在脚本中使用它,则需要非常小心,不要使用它.也许有一种方法可以摆脱这种情况并用动态(免费)文件描述符替换它.

  • 也许你想要捕获被调用函数的STDERR.或者你甚至想要从变量传入和传出多个filedescriptor.

也别忘了:

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

没有简单的方法可以将环境变量传递出外部命令.(_capture虽然它应该是可能的!)但这就完全不同了.

最后的话

这不是唯一可行的解​​决方案.这是解决方案的一个例子.

一如既往,你有很多方法可以在shell中表达事物.所以随意改进并找到更好的东西.

这里介绍的解决方案远非完美:

  • 这根本不是什么问题,所以请原谅错别字.
  • 有很多改进的空间,见上文.
  • 它使用了现代的许多功能d(),因此可能难以移植到其他外壳.
  • 可能还有一些我没想过的怪癖.

不过我觉得它很容易使用:

  • 只添加4行"库".
  • 为shell函数添加一行"注释".
  • 暂时牺牲一个文件描述符.
  • 即使数年之后,每一步都应该易于理解.

  • 你太棒了 (4认同)
  • 我一生中从未见过从如此多的角度进行如此广泛的答复。我向你鞠躬@Tino (3认同)

Ash*_*kan 13

也许你可以使用一个文件,在函数内写入文件,然后从文件中读取.我已经改成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]}"
Run Code Online (Sandbox Code Playgroud)

输出:

hi
first second third
first
second
third
Run Code Online (Sandbox Code Playgroud)


Pra*_*ord 12

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

$(test1)

在子shell(子shell)中,子shell无法修改父级中的任何内容.

你可以在bash 手册中找到它

请检查:事情在这里产生子壳


Elm*_*der 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
Run Code Online (Sandbox Code Playgroud)

因此,在您的情况下,将是:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"
Run Code Online (Sandbox Code Playgroud)

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

  • 非常感谢@kvantour!我完全同意你的建议,尤其是阵列解决方案,它更加干净。您想做出改变吗?我目前没时间处理它... (2认同)