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

Posted

技术标签:

【中文标题】bash 变量何时导出到子 shell 和/或脚本可访问?【英文标题】:When are bash variables exported to subshells and/or accessible by scripts? 【发布时间】:2019-01-24 23:42:50 【问题描述】:

我对@9​​87654321@ 变量是否导出到子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-scriptother-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下运行的进程是一个全新的程序,只有exported变量存活。

【讨论】:

这很有趣——我从来没有在bash 的上下文中考虑过 fork() 和 exec()。我的理解是否正确:当我调用 (./c.sh) 时,一个 subshel​​l 被分叉,因此 $FOO 在 subshel​​l 中可见。但是那个子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文件

Bash基础——Shell脚本内部常用环境变量

shell脚本中的export 和 source,bash

Bash 脚本从 setup.py 或 PKG-INFO 文件中获取版本并导出为环境变量

Shell、CMD、PowerShell、Bash

Bash编程013——环境变量