Why can't I specify an environment variable and echo it in the same command line?

想想这个片段:

$ SOMEVAR=AAA
$ echo zzz $SOMEVAR zzz
zzz AAA zzz

在这里,我已经在第一行将 $SOMEVAR设置为 AAA-当我在第二行回显它时,我得到了预期的 AAA内容。

但是,如果我尝试在与 echo相同的命令行上指定变量:

$ SOMEVAR=BBB echo zzz $SOMEVAR zzz
zzz AAA zzz

... 我没有得到我期望的 BBB-我得到了旧的值(AAA)。

事情应该是这样的吗?如果是这样,那么为什么你可以指定像 LD_PRELOAD=/... program args ...这样的变量并让它工作呢?我错过了什么?

39745 次浏览

你看到的是预期的行为。问题在于,父 shell 在使用修改后的环境调用命令之前,会在命令行上计算 $SOMEVAR。您需要将 $SOMEVAR的计算推迟到环境设置之后。

你眼前的选择包括:

  1. SOMEVAR=BBB eval echo zzz '$SOMEVAR' zzz.
  2. SOMEVAR=BBB sh -c 'echo zzz $SOMEVAR zzz'.

这两种方法都使用单引号来防止父 shell 计算 $SOMEVAR; 只有在环境中设置后才计算它(在单个命令执行期间暂时如此)。

另一种选择是使用子 shell 表示法(马库斯 · 库恩在其 回答中也建议使用这种表示法) :

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

The variable is set only in the sub-shell

SOMEVAR=BBB; echo zzz $SOMEVAR zzz

使用; 来分隔位于同一行上的语句。

原因在于,这为一行设置了一个环境变量。但是,echo不做展开,bash做。因此,即使在 echo 命令的上下文中 SOME_VARBBB,变量实际上也是在执行命令之前展开的。

为了看到效果,你可以这样做:

$ SOME_VAR=BBB bash -c 'echo $SOME_VAR'
BBB

这里的变量直到子进程执行时才展开,因此您可以看到更新后的值。如果在父 shell 中再次检查 SOME_VARIABLE,它仍然是 AAA,正如预期的那样。

问题,重新审视

非常坦率地说,手册在这一点上令人困惑。 GNU Bash 手册表示:

任何简单命令或函数的环境[注意,这不包括内置函数]都可以通过在其前面添加参数分配来临时增强,如 Shell 参数中所述。这些赋值语句仅影响该命令所看到的环境。

如果您真正解析这个句子,它的意思是修改了命令/函数的 环境,但没有修改父进程的环境。所以,这将工作:

$ TESTVAR=bbb env | fgrep TESTVAR
TESTVAR=bbb

因为 env 命令的环境在执行之前已经被修改过了:

$ set -x; TESTVAR=bbb echo aaa $TESTVAR ccc
+ TESTVAR=bbb
+ echo aaa ccc
aaa ccc

因为参数扩展是由 shell 执行的。

翻译步骤

另一个问题是其解释器的 Bash 定义了这些步骤:

  1. 从文件(请参阅 Shell 脚本)中从字符串中读取其输入 作为 -c 调用选项的参数提供(请参阅调用 Bash) ,或者从用户的终端。
  2. Breaks the input into words and operators, obeying the quoting rules 这些标记由元字符分隔。 Alias expansion is performed by this step (see Aliases).
  3. 将标记解析为简单命令和复合命令(参见 Shell 命令)。
  4. 执行各种 Shell 扩展(请参阅 Shell 扩展) , 将扩展的标记分解为文件名列表(参见 Filename 展开)和命令和参数。
  5. 执行任何必要的重定向(参见重定向)并删除 the redirection operators and their operands from the argument list.
  6. 执行命令(请参阅执行命令)。
  7. 可以选择等待命令完成并收集其退出 status (see Exit Status).

这里发生的情况是,内置程序没有自己的执行环境,所以他们永远不会看到修改后的环境。此外,简单的命令(例如/bin/echo) 获得一个修改过的环境(这就是 env 示例工作的原因) ,但是 shell 扩展是在步骤 # 4的 目前环境中进行的。

换句话说,不是将‘ aaa $TESTVAR ccc’传递给/bin/echo; 而是将插入的字符串(在当前环境中展开)传递给/bin/echo。在这种情况下,由于当前环境没有 TESTVAR,您只需要向命令传递‘ aaa ccc’。

摘要

文档可以更清晰,好在有 Stack Overflow!

参见

Http://www.gnu.org/software/bash/manual/bashref.html#command-execution-environment

为了得到你想要的,使用

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

理由:

  • 必须用分号或新行将赋值与下一个命令分隔开,否则在下一个命令(echo)执行 参数展开之前不执行赋值。

  • 您需要在 子弹环境中进行赋值,以确保它不会持续超出当前行。

这个解决方案比其他一些建议的方案更短、更简洁、更有效,特别是它没有创建一个新的流程。

这里有一个选择:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz
SOMEVAR=BBB echo zzz $SOMEVAR zzz

SOMEVAR=BBB添加到环境变量中,然后执行 echo zzz $SOMEVAR zzz$SOMEVAR是指预先设置为 AAA的 shell 变量 SOMEVAR

添加分号 SOMEVAR=BBB; echo zzz $SOMEVAR zzz将 shell 变量设置为 BBB,然后在分号 echo zzz $SOMEVAR zzz之后执行命令并生成 zzz BBB zzz

试试这个命令:

SOMEVAR=BBB env | less

看看环境。

让我们看看 POSIX 规范来理解 为什么的行为,不仅在 bash 中,而且在任何兼容的 shell 中都是如此:


2.10.2,Shell 语法规则

从规则7(b)开始,涵盖了赋值优先于简单命令的情况:

If all the characters preceding '=' form a valid name (see the Base Definitions volume of IEEE Std 1003.1-2001, Section 3.230, Name), the token ASSIGNMENT_WORD shall be returned. (Quoted characters cannot participate in forming a valid name.)

[...]

对 NAME 的赋值应按照简单命令中指定的方式进行。

因此,对于符合 POSIX 的 shell,解析这个分配是必需的。


2.9.1,简单命令

  1. 重定向应按照重定向中的说明进行。

  2. 在赋值之前,每个变量的赋值应该扩展到波动展开、参数展开、指令替代展开、算术展开和报价删除。

[...]

If no command name results, variable assignments shall affect the current execution environment. Otherwise, 变量赋值应该导出到命令的执行环境中,并且不应该影响当前的执行环境(特殊的内置环境除外)。 If any of the variable assignments attempt to assign a value to a read-only variable, a variable assignment error shall occur. See Consequences of Shell Errors for the consequences of these errors.

因此: 必须导出在前缀的一部分中给予一个简单命令的赋值,而且不能影响“当前 shell 环境”,除非被调用的命令是一个特殊的内置命令。此外,这些步骤应遵循重定向,这种重定向本质上必须在命令调用过程的后期发生。


2.12,外壳执行环境

除了特殊的内置实用程序(参见特殊的内置实用程序)以外的其他实用程序应该在由以下内容组成的单独环境中调用。这些对象的初始值应该与父 shell 的初始值相同,除非如下所述。

[...]

Variables with the export attribute, 以及那些在命令期间显式导出的, shall be passed to the utility environment variables


因此: 这些变量在 fork 之后和执行被调用的命令之前由 subshell 展开,并且必须——按照规范——单独影响子环境。


现在,为了一些不同的行为:

SOMEVAR=BBB sh -c 'echo "$SOMEVAR"'

... benefits from the sh instance creating shell variables from its environment variables (as required in section 2.5.3 of the POSIX specification) on startup.


顺便说一下,值得注意的是,您所询问的语法是用于 一个简单的命令中的赋值,而不是用于 一个子弹壳中的赋值。您可以像下面这样控制管道中涉及的子 shell 中的赋值:

{ SOMEVAR=BBB; echo "$SOMEVAR"; } | somecommand ...

... 它将赋值放入子 shell 中,运行管道的第一个组件(如果您的 shell 确实在子 shell 中运行该组件,这与 POSIX 有关,参见 spec: 但是,作为扩展,管道中的任何或所有命令都可以在当前环境中执行)。

简单地说,$SOMEVAR被计算为 之前,命令被调用时,命令前面的 SOMEVAR=BBB会修改正在运行的命令的环境。

As Charles Duffy said, you can add an intermediate sh process that will evaluate the variable with a similar syntax but you probably want to do something a bit more elaborate and it'd be useful to know what if you still have troubles with it.