何时将Bash变量导出到子shell和/或可由脚本访问?

22

我对于bash变量是否会被导出到子shell中并且何时能够被脚本访问感到困惑。到目前为止,我的经验使我相信bash变量会自动传递给子shell。例如:

> FOO=bar
> echo $FOO
bar
> (echo $FOO)
bar

以上内容似乎表明bash变量可以在子Shell中访问。

给定以下脚本:

#! /usr/bin/bash
# c.sh

func()
{
  echo before
  echo ${FOO}
  echo after
}

func

我理解在当前shell上下文中调用脚本会使其可以访问当前shell的变量:

> . ./c.sh 
before
bar
after

如果我不使用“点空格”前缀来调用脚本…

> ./c.sh 
before

after

脚本不是在子shell中调用的吗?如果是这样,并且当前shell的变量对子shell可用(正如我从第一个代码块推断出来的那样),那么为什么以这种方式运行时$FOO不可用于c.sh

同样地,为什么在圆括号中运行c.sh$FOO也不可用——我理解圆括号表示在子shell中运行表达式:

> (./c.sh)
before

after

如果这不会在这篇文章中引起太多问题:如果"./c.sh"和"(./c.sh)"都在当前shell的子shell中运行脚本,那么两种调用方式之间有什么区别呢?


3
一个子shell是从父进程中分叉出来的,因此变量不需要被导出才能在其中可见:子进程总是继承其父进程100%的状态(除了PID本身和使用标志显式打开的文件描述符,指示操作系统在fork时不复制它们)。 - Charles Duffy
6
所以./foo不会在子shell中运行foo:它是一个完全不相关的子进程,在fork()execve()边界之后。 - Charles Duffy
5
(./c.sh) 会派生一个子shell,然后在其中运行一个子进程,因此该子进程是原始shell的孙子而不是直接子代,并且在子代和孙子之间存在一个 execv 的边界(尽管在父代和子代之间不存在)。 - Charles Duffy
2
你标记了 shell,所以我想指出并非所有的 shell 都像 bash 一样处理子 shell。例如,Korn shell 避免为子 shell 创建一个子进程。 - cdarke
1
重新阅读标准,我确实可以看到一些支持您定义的内容,尽管不至于模棱两可以至于迫使我改变自己的术语选择。无论如何,我相信一起阅读我们的评论将会带领读者获得有用的理解。 :) - Charles Duffy
显示剩余2条评论
1个回答

20

(...) 在一个单独的环境中运行 ... ,这最容易通过子Shell实现(在bash、dash和大多数其他POSIX shell中使用)--也就是说,由旧Shell fork() 创建的子进程,但不调用任何execv类函数。因此, 父Shell的整个内存状态都会被复制,包括非导出的Shell变量。对于一个子Shell而言,这通常是你想要的:父Shell的进程镜像副本,而不是替换为一个新的可执行图像,从而使所有状态保持不变。

考虑 (. shell-library.bash; function-from-that-library "$preexisting_non_exported_variable") 作为一个例子:由于有括号,它会fork()一个子shell,但随后直接在该shell内部来源于 shell-library.bash 的内容,而不是用一个单独的可执行文件替换由fork()创建的shell解释器。这意味着function-from-that-library可以看到来自父shell的非导出函数和变量(如果使用execve()将无法看到),并且启动速度较快(因为它不需要像execve()操作期间那样链接、加载和初始化新的shell解释器);但同时也意味着对内存状态、shell配置和进程属性(如工作目录)所做的更改不会修改调用它的父解释器(如果没有子shell并且没有fork()将会这样),所以父shell受到保护,不会被库所做的配置更改影响其后续操作。


相比之下,./other-script 将运行other-script 作为一个完全独立的可执行文件;当子shell被调用后(它不是子shell!),它不会保留非导出变量。具体实现如下:

  • Shell调用fork()创建子进程。在此时,子进程仍然复制了所有非导出变量的状态。
  • 子进程遵循任何重定向(如果是./other-script >>log.out,则子进程将会open("log.out", O_APPEND),然后将描述符fdup()1上,覆盖stdout)。
  • 子进程调用execv("./other-script", {"./other-script", NULL})指示操作系统将其替换为新的other-script实例。成功调用后,以子进程PID运行的进程是一个全新的程序,只有export的变量能够幸存。

3
这很有趣 - 我从未考虑过在bash上下文中使用fork()和exec()。我的理解正确吗:当我调用(./c.sh)时,会分叉出一个子shell,因此在子shell中可以看到$FOO。但是该子shell然后进行了fork()exec()的操作,因此在c.sh的上下文中(它是我键入"(./c.sh)"的shell的一种“孙子进程”),$FOO不再可见? - StoneThrow
1
第二句话应该是“父级的”,不是吗?我不是很确定,所以不只是编辑 ;) - Benjamin W.
3
如果你使用了exec ./other-script(这将不先进行fork()就直接运行exec()),那么other-script会继承已经export的变量,但是不会继承没有export的shell变量。./other-script(exec ./other-script)基本等价,其中( )会fork出一个子shell(保留非export变量),然后exec实际上退出当前shell(销毁非export变量)并在同一进程中运行一个新的shell。 - Gordon Davisson
3
@cdarke说的不是Bash特有的语法,而是POSIX定义的:"[$$]会展开成当前shell进程的十进制ID。在子shell中(参见Shell执行环境),"$"将展开为与当前shell相同的值。" - rici
1
@Harry,fork()会创建一个父进程的副本作为子进程,然后execve()会用你想要作为子进程的程序替换掉这个父进程的副本(当你想要的是不同的程序时;对于子shell,你通常只需要一个没有替换的父进程副本)。 - Charles Duffy
显示剩余12条评论

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