从子shell设置父shell的变量

82

我该如何在子shell中设置一个变量,使得它能够影响到父shell?

a=3
(a=4)
echo $a

1
类似这样的内容:http://stackoverflow.com/questions/15383082/pass-variable-from-a-child-to-parent-in-ksh/15383353#15383353 - Guru
11个回答

84
子shell的整个意义在于它不会影响调用会话。在bash中,子shell是一个子进程,其他shell可能有所不同,但即使在这种情况下,子shell中的变量设置也不会影响调用者。根据定义。
你需要一个子shell吗?如果只需要一个组,请使用大括号:
a=3
{ a=4;}
echo $a

给出4(注意其中的空格)。或者,将变量值写入stdout并在调用者中捕获它。
a=3
a=$(a=4;echo $a)
echo $a

避免使用反引号``,它们已被弃用,可能难以阅读,并且在某些情况下已知会引起问题。

谢谢您指出花括号的用法。我之前看到过它们,但从未知道它们的作用或使用方法。好建议! - Christian
1
有没有办法在管道中使用它?例如,{ false; a=$?;} | true 将无法将全局变量 a 设置为所需的值 :( - ArchimedesMP
@ArchimedesMP:管道是使用标准流在进程之间传递数据的,这里没有涉及到任何进程(除了 shell)。你想要实现什么? - cdarke
这个解决方法怎么样?https://stackoverflow.com/a/15383353/6466510 - andreagalle
空格有什么问题? - undefined
显示剩余2条评论

37

有一个GDB-Bash变量的技巧:

gdb --batch-silent -ex "attach $$" -ex 'set bind_variable("a", "4", 0)'; 

虽然这总是在全局范围内设置一个变量,而不仅仅是父级作用域


37
虽然不太危险,但我认为在实际脚本中使用这个应该和 fork bomb 一样频繁。 - chepner
我必须使用..."attach $PPID"...来使它工作 - $$ 包含当前 shell 的 PID。 - dingalapadum
@dingalapadum 不在 OP 的括号内。可能子 shell 在同一进程中运行,然后重置变量。 - BeniBela
() 子 shell 中,$$ 保持不变。实际子 shell 的 PID 可以通过 $BASHPID 查看。 - ivan_pozdeev

27

不行。子Shell无法访问其父进程的环境变量。(至少在Bash提供的抽象层级内是如此。你可以尝试使用 gdb 或者踩JMP指令等方法来偷偷访问,但我并不建议这样做。)

一个可行的替代方案是,子Shell将赋值语句写入一个临时文件供其父进程读取:

a=3
(echo 'a=4' > tmp)
. tmp
rm tmp
echo "$a"

1
不太美观,但在某些情况下是更好的选择。谢谢。 - Benoit Duffez
2
文件方法在以下情况下非常有用,即您正在使用子shell来收集多个变量的值,而父shell稍后将需要这些值,特别是在不使用子shell很困难的情况下。 - aroth
1
我不得不使用这个方法,因为我在主脚本的开头使用了“set -u”和“set -e”,但我需要运行一些命令来设置一个变量,该变量以非零退出代码退出(从而触发“-e”以终止脚本)。 - dan_linder
1
这个程序能否使用文件描述符更加优雅地实现?子 shell 和父 shell 是否都可以访问文件描述符? - opinion_no9
1
我正在寻找一种更优雅的方式,类似于这种方法。如何使用文件描述符来实现?是否与khachik在下面的回答中写入输出流的方式相同?我想这只限于一个变量。 - midnite
显示剩余3条评论

23

如果问题与 while 循环有关,修复的方法之一是使用进程替换:

    var=0
    while read i;
    do
      # perform computations on $i
      ((var++))
    done < <(find . -type f -name "*.bin" -maxdepth 1)

如图所示:https://dev59.com/3WYr5IYBdhLWcg3wYZKD#13727116


1
问题在于进程替换(Process Substitution)仅适用于bash,而不是sh(POSIX)。 - virtualdj

11

通过阅读 @ruakh(谢谢)的答案,并考虑到评论中提出的文件描述符解决方案,我得到了以下想法:

a=3    
. <(echo a=4; echo b=5)
echo $a
echo $b
  • 它允许同时返回不同的变量(这可能是接受答案的子shell变体中的一个问题)。
  • 不需要迭代,
  • 不需要处理临时文件。
  • 接近OP提出的语法。

结果:

4
5

启用xtrace后,可以看到我们正在从为子shell输出创建的文件描述符中获取源:
+ a=3
+ . /dev/fd/63 # <-- the file descriptor ;)
++ echo a=4
++ echo b=5
++ a=4
++ b=5
+ echo 4
4
+ echo 5
5

我很喜欢它。 - scravy

9
要在父脚本调用的脚本中更改变量,可以在脚本前加“.”来调用该脚本。
(编辑-解释) 在大多数 shell 中,“.”是“source”的别名。source 命令只是将另一个文件的文本插入到正在执行的脚本中的位置。在这个问题的上下文中,这个答案避免了一个子 shell。
a=3
echo $a    
. ./calledScript.sh
echo $a

在calledScript.sh中
a=4

预期输出
3
4

嗨,@Carl。这个可以运行,但能解释一下答案吗? - Ajeetkumar
@Ajeetkumar,请查看以下链接:https://unix.stackexchange.com/a/114301 。该链接提供了一个快捷方式,可用于源命令 https://ss64.com/bash/source.html。 - Carl Bosch
2
calledScript.sh 在当前 shell 中运行,因此它不符合“子 shell”的条件。 - ivan_pozdeev
明白了,ivan_pozdeev。从技术上讲,我并没有按照问题的要求进行回答。 - Carl Bosch
如果您在源代码中运行Shell脚本,则不再有子Shell。 - erikbstack

7

不要从父Shell访问变量,改变命令的顺序并使用进程替代

a=3
echo 5 | (read a)
echo $a

打印出 3

a=3
read a < <(echo 5)
echo $a

输出 5

另一个例子:

let i=0 
seq $RANDOM | while read r
              do 
                let i=r 
              done
echo $i

let i=0
while read r 
do 
  let i=r 
done < <(seq $RANDOM)
echo $i

另外,当作业控制处于非活动状态时(例如在脚本中),您可以使用 lastpipe shell 选项来实现相同的结果,而无需改变命令的顺序:

#!/bin/bash
shopt -s lastpipe
let i=0
seq $RANDOM | while read r
              do 
                let i=r
              done
echo $i

使用 read 可以传递多个变量。子 shell 继承父 shell 的变量,根据需要进行修改,并在退出之前将它们打印出来。这样就可以轻松地隔离大量的代码,例如循环体。 - Ale

4
你可以将子shell中的值输出,并将子shell的输出赋值给调用脚本中的变量:
# subshell.sh
echo Value

# caller
myvar=$(subshell.sh)

如果子shell还有更多的输出,你可以通过将变量值和其他消息重定向到不同的输出流来进行分离:
# subshell.sh
echo "Writing value" 1>&2
echo Value

# caller
myvar=$(subshell.sh 2>/dev/null) # or to somewhere else
echo $myvar

或者,您可以在子shell中输出变量赋值,在调用程序中进行计算,并避免使用文件交换信息:

# subshell.sh
echo "a=4"

# caller
# export $(subshell.sh) would be more secure, since export accepts name=value only.
eval $(subshell.sh)
echo $a

我能想到的最后一种方法是使用退出代码,但这仅涵盖了整数值的交换(在有限的范围内),并且打破了解释退出代码的约定(成功为0,非0为其他情况)。

3

虽然从子shell中获取多个变量比较困难,但是你可以在函数内部设置多个变量而不使用全局变量。

你可以将一个变量的名称传递到一个使用local -n的函数中,将其转换为一个特殊的变量,称为nameref

myfunc() {
    local -n OUT=$1
    local -n SIDEEFFECT=$2
    OUT='foo'
    SIDEEFFECT='bar'
}

myfunc A B
echo $A
> foo
echo $B
> bar

这是我最终使用的技术,而不是设置多个变量来获取子shell FOO=$(myfunc) 的工作。


3

除非您可以将所有io应用到管道并使用文件句柄,否则在$(command)和任何其他子进程中无法完成基本的变量更新。

然而,常规文件是bash正常顺序处理的全局变量。注意:由于竞态条件,这种简单方法不适用于并行处理。

创建一个像这样的设置/获取/默认函数:

globalVariable() { # NEW-VALUE
    # set/get/default globalVariable
    if [ 0 = "$#" ]; then
        # new value not given -- echo the value
        [ -e "$aRam/globalVariable" ] \
            && cat "$aRam/globalVariable" \
            || printf "default-value-here"
    else
        # new value given -- set the value
        printf "%s" "$1" > "$aRam/globalVariable"
    fi
}

"

"$aRam"是存储值的目录。我喜欢它成为一个RAM磁盘,以提高速度和易变性:

"
aRam="$(mktemp -td $(basename "$0").XXX)" # temporary directory
mount -t tmpfs ramdisk "$aRam" # mount the ram disk there
trap "umount "$aRam" && rm -rf "$aRam"" EXIT # auto-eject

读取数值:

v="$(globalVariable)" # or part of any command

设置值:

globalVariable newValue # newValue will be written to file

取消设置该值的方法:

rm -f "$aRam/globalVariable"

访问函数的唯一真正原因是应用默认值,因为给定不存在的文件时,cat会出错。它还可以应用其他的get/set逻辑。否则,它根本不需要。

一个丑陋的read方法避免了cat的不存在文件错误:

v="$(cat "$aRam/globalVariable 2>/dev/null")"

这个程序很酷的特性是,在程序运行时,你可以打开另一个终端并查看文件的内容。

由于文件访问未同步,容易出现竞态条件。 - ivan_pozdeev
很棒的答案!值得点赞。 - shrike

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接