为什么不能指定环境变量并在同一命令行中回显它?


91

请考虑以下代码段:

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

在这里,我将第一行设置$SOMEVARAAA-在第二行中回显它时,AAA将按预期获得内容。

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

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

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

这是应该的样子吗?如果是这样,那您怎么能指定like这样的变量LD_PRELOAD=/... program args ...并使其起作用呢?我想念什么?


2
当您使分配成为单独的语句时,或者在具有其自己的环境的情况下调用脚本时,它都起作用,但在当前环境中为命令添加前缀时,则不起作用。有趣!
托德·雅各布斯

1
原因LD_PRELOAD工程是变量被设置在程序的环境-没有它的命令行。
暂停,直到另行通知。

Answers:


103

您看到的是预期的行为。问题在于父外壳程序$SOMEVAR在使用修改后的环境调用命令之前会在命令行上求值。您需要获得$SOMEVAR推迟评估,直到设置了环境。

您的直接选择包括:

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

两者都使用单引号防止父外壳评估$SOMEVAR; 仅在环境中设置它之后(临时,在单个命令的持续时间内)才对它进行评估。

另一个选择是使用sub-shell表示法(也由Marcus Kuhn在他的回答中建议):

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

该变量仅在子外壳中设置


太好了,@ JonathanLeffler-非常感谢您的解释;干杯!
sdaau 2012年

@ markus-kuhn的添加很难高估。
Alex Che

37

再探问题

坦率地说,手册在这一点上令人困惑。在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调用选项的参数提供的字符串(请参见Invoking Bash)或从用户终端读取其输入。
  2. 遵循“报价”中描述的报价规则,将输入分为单词和运算符。这些标记由元字符分隔。别名扩展是通过此步骤执行的(请参阅别名)。
  3. 将标记解析为简单命令和复合命令(请参阅Shell命令)。
  4. 执行各种shell扩展(请参阅Shell Expansions),将扩展的令牌分为文件名列表(请参见Filename Expansion)以及命令和参数。
  5. 执行任何必要的重定向(请参阅重定向),并从参数列表中删除重定向运算符及其操作数。
  6. 执行命令(请参阅执行命令)。
  7. (可选)等待命令完成并收集其退出状态(请参阅退出状态)。

这里发生的是,内建函数没有自己的执行环境,因此他们永远看不到修改后的环境。另外,简单的命令(例如/ bin / echo)确实获得了修改后的环境(这就是env示例起作用的原因),但是在步骤4中,shell扩展正在当前环境中进行。

换句话说,您没有将'aaa $ TESTVAR ccc'传递给/ bin / echo; 您正在将插值字符串(在当前环境中已扩展)传递到/ bin / echo。在这种情况下,由于当前环境没有TESTVAR,您只需将'aaa ccc'传递给命令。

概要

文档可能会更加清晰。好东西有堆栈溢出!

也可以看看

http://www.gnu.org/software/bash/manual/bashref.html#Command-Execution-Environment


我已经对此表示赞同-但我只是回到了这个问题,这篇文章恰好包含了我所需要的指针。非常感谢,@ CodeGnome!
sdaau

自发布此答案以来,我不知道Bash在该区域是否发生了变化,但是带前缀的变量分配现在可以与内建函数一起使用。例如,按预期FOO=foo eval 'echo $FOO'打印foo。这意味着您可以做类似的事情IFS="..." read ...
Will Vousden

我认为正在发生的事情是Bash实际上临时修改了自己的环境,并在命令完成后将其还原,这可能会产生奇怪的副作用。
Will Vousden

22

要实现您想要的,请使用

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

原因:

  • 您必须以分号或换行将分配与下一个命令分开,否则在下一个命令(echo)发生参数扩展之前不会执行该分配。

  • 您需要在子Shell环境中进行分配,以确保分配不会在当前行之外持续存在。

该解决方案比其他建议的解决方案更短,更整洁,更有效,特别是它不会创建新的过程。


3
对于将来到这里的Google员工:这可能是这个问题的最佳答案。为了使其更加复杂,如果您需要分配在命令环境中可用,则需要导出它。子外壳仍然可以防止分配持久化。(export SOMEVAR=BBB; python -c "from os import getenv; print getenv('SOMEVAR')")
eaj 2016年

@eaj要将shell变量导出到单个外部程序调用中,如您的示例所示,只需使用SOMEVAR=BBB python -c "from os import getenv; print getenv('SOMEVAR')"
Markus Kuhn

10

原因是这为一行设置了一个环境变量。但是,echo不做扩展,bash做。因此,您的变量实际上是扩大了执行命令之前,即使SOME_VARBBB在echo命令的情况下。

要查看效果,您可以执行以下操作:

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

在此变量直到子进程执行后才展开,因此您会看到更新后的值。如果SOME_VARIABLE再次在父外壳中签入,则仍然是AAA,如预期的那样。


1
+1可以正确解释为什么它不能按书面要求工作,并提供可行的解决方法。
乔纳森·勒夫勒2012年

1
SOMEVAR=BBB; echo zzz $SOMEVAR zzz

用一个 ; 分隔同一行中的语句。


1
那行得通,但意义不大。想法是只为一个命令设置环境,而不是像解决方案那样永久设置环境。
乔纳森·莱夫勒2012年

感谢@Kyros; 不知道为什么我现在错过了:)仍然徘徊LD_PRELOAD在没有分号的情况下如何在可执行文件前面运行,这样...非常感谢-干杯!
sdaau 2012年

@JonathanLeffler-的确是这个主意;我没意识到分号使更改永久生效-感谢您注意!
sdaau 2012年

1

这是一种选择:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz

无论使用命令&&还是;分隔命令,分配都会保留,这不是OP的期望行为。Markus Kuhn具有此答案的正确版本。
eaj 2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.