bash 变量何时导出到子 shell 和/或脚本可访问?
Posted
技术标签:
【中文标题】bash 变量何时导出到子 shell 和/或脚本可访问?【英文标题】:When are bash variables exported to subshells and/or accessible by scripts? 【发布时间】:2019-01-24 23:42:50 【问题描述】:我对@987654321@ 变量是否导出到子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中运行脚本,那么这两种调用方式有什么区别?)
【问题讨论】:
一个子shell是从父进程中派生出来的,所以一个变量不需要导出在其中可见:子进程总是继承其父进程的100%状态(除了 PID 本身,以及使用指示操作系统不要在 fork 上复制它们的标志显式打开的文件描述符)。 所以./foo
不在子shell 中运行foo
:这是一个完全不相关的子进程,不仅在fork()
后面,而且在execve()
边界后面。
...而(./c.sh)
分叉出一个子shell,然后从它内部运行一个子进程,因此子进程是原始shell 的孙子进程而不是直接子进程,并且您有孩子和孙子之间的execv
边界(尽管父母和孩子之间没有)。
您标记了shell
,所以我想指出并非所有shell 都以与bash
相同的方式处理子shell。例如,Korn shell 避免为子 shell 创建子进程。
@cdarke, ...我宁愿说 ksh 实现了(...)
的“独立环境”语义而不使用子shell 尽可能(当它变得不可能遵守 POSIX 语义时创建一个子shell,就会创建一个子shell;暗示(...)
根本不使用它们是不准确的)。阅读以上内容作为编辑我的答案以不再声明(...)
请求子shell(相对于请求最容易用子shell 实现的独立环境)的请求是公平的。
【参考方案1】:
(...)
在单独的环境中运行...
,使用子shell 最容易实现(并在bash、dash 和大多数其他POSIX-y shell 中实现)——也就是说,由@ 创建的子shell 987654323@ing 旧 shell,但不调用任何 execv
-family 函数。因此,父级的整个内存状态被复制,包括未导出的 shell 变量。而对于子 shell,这正是您通常想要的:只是父 shell 进程映像的副本,而不是被新的可执行映像替换,从而保持其所有状态。
以(. shell-library.bash; function-from-that-library "$preexisting_non_exported_variable")
为例:由于括号,它是fork()
sa 子shell,但它随后会获取shell-library.bash
的内容直接在该shell 中,而不替换创建的shell 解释器通过那个fork()
和一个单独的可执行文件。这意味着function-from-that-library
可以从父 shell 中看到未导出的函数和变量(如果它是 execve()
'd 则不能),并且启动速度更快(因为它不需要在execve()
操作期间发生的链接、加载和以其他方式初始化新的 shell 解释器);但它对内存状态、shell 配置和进程属性(如工作目录)所做的更改也不会修改调用它的父解释器(如果没有子 shell 并且它不是 fork()
'd),因此可以保护父 shell 免受库所做的可能修改其后续操作的配置更改。
相比之下,./other-script
将other-script
作为完全独立的可执行文件运行;在调用子 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
ed变量存活。
【讨论】:
这很有趣——我从来没有在bash
的上下文中考虑过 fork() 和 exec()。我的理解是否正确:当我调用 (./c.sh)
时,一个 subshell 被分叉,因此 $FOO
在 subshell 中可见。但是那个子shell然后是fork()
s和exec()
s ./c.sh
,因此在c.sh
的上下文中(这是我输入“(./c.sh)
”的shell的“孙进程”),@987654351 @ 不再可见?
第二句应该是“parent”,不是吗?不完全确定,因此不仅仅是编辑;)
另外,如果你使用exec ./other-script
(它首先运行 exec() 没有分叉),另一个脚本会继承导出的变量,但不会继承非导出的 shell 变量。 ./other-script
大部分相当于(exec ./other-script)
,其中( )
fork 一个子shell(保留未导出的变量),然后exec
有效地退出当前shell(销毁未导出的变量)并运行一个新的shell 在同一个进程中。
注意bash
的特殊性,子shell 中的$$
给出了父进程的PID,而不是当前子shell 进程。
@cdarke:这不是 bashism;它由 Posix 定义:“[$$
] 扩展为被调用 shell 的十进制进程 ID。在子 shell(请参阅 shell 执行环境)中,'$' 应扩展为与当前 shell 相同的值。”
以上是关于bash 变量何时导出到子 shell 和/或脚本可访问?的主要内容,如果未能解决你的问题,请参考以下文章
Bash—source命令&export命令&bashrc文件