118

Consider this snippet:

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

Here I've set $SOMEVAR to AAA on the first line - and when I echo it on the second line, I get the AAA contents as expected.

But then, if I try to specify the variable on the same command line as the echo:

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

... I do not get BBB as I expected - I get the old value (AAA).

Is this how things are supposed to be? If so, how come then you can specify variables like LD_PRELOAD=/... program args ... and have it work? What am I missing?

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
sdaau
  • 36,975
  • 46
  • 198
  • 278

9 Answers9

115

What you see is the expected behaviour. The trouble is that the parent shell evaluates $SOMEVAR on the command line before it invokes the command with the modified environment. You need to get the evaluation of $SOMEVAR deferred until after the environment is set.

Your immediate options include:

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

Both these use single quotes to prevent the parent shell from evaluating $SOMEVAR; it is only evaluated after it is set in the environment (temporarily, for the duration of the single command).

Another option is to use the sub-shell notation (as also suggested by Marcus Kuhn in his answer):

(SOMEVAR=BBB; echo zzz $SOMEVAR zzz)

The variable is set only in the sub-shell

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • The `eval`-flavored approach is very trouble-prone. It works for this particular simple example, but the effect of arguments being word-split and glob-expanded before being concatenated together into a single string that's fed into the parser tends not to be a behavior people expect / account for in writing their code. Maybe as an alternative... `myfunc() { echo zzz "$SOMEVAR" zzz; }; SOMEVAR=BBB myfunc`? – Charles Duffy Apr 05 '22 at 14:22
43

The Problem, Revisited

Quite frankly, the manual is confusing on this point. The GNU Bash manual says:

The environment for any simple command or function [note that this excludes builtins] may be augmented temporarily by prefixing it with parameter assignments, as described in Shell Parameters. These assignment statements affect only the environment seen by that command.

If you really parse the sentence, what it's saying is that the environment for the command/function is modified, but not the environment for the parent process. So, this will work:

$ TESTVAR=bbb env | fgrep TESTVAR
TESTVAR=bbb

because the environment for the env command has been modified before it executed. However, this will not work:

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

because of when parameter expansion is performed by the shell.

Interpreter Steps

Another part of the problem is that Bash defines these steps for its interpreter:

  1. Reads its input from a file (see Shell Scripts), from a string supplied as an argument to the -c invocation option (see Invoking Bash), or from the user's terminal.
  2. Breaks the input into words and operators, obeying the quoting rules described in Quoting. These tokens are separated by metacharacters. Alias expansion is performed by this step (see Aliases).
  3. Parses the tokens into simple and compound commands (see Shell Commands).
  4. Performs the various shell expansions (see Shell Expansions), breaking the expanded tokens into lists of filenames (see Filename Expansion) and commands and arguments.
  5. Performs any necessary redirections (see Redirections) and removes the redirection operators and their operands from the argument list.
  6. Executes the command (see Executing Commands).
  7. Optionally waits for the command to complete and collects its exit status (see Exit Status).

What's happening here is that builtins don't get their own execution environment, so they never see the modified environment. In addition, simple commands (e.g. /bin/echo) do get a modified ennvironment (which is why the env example worked) but the shell expansion is taking place in the current environment in step #4.

In other words, you aren't passing 'aaa $TESTVAR ccc' to /bin/echo; you are passing the interpolated string (as expanded in the current environment) to /bin/echo. In this case, since the current environment has no TESTVAR, you are simply passing 'aaa ccc' to the command.

Summary

The documentation could be a lot clearer. Good thing there's Stack Overflow!

See Also

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

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
  • 1
    I had already upvoted this - but I just came back to this question, and this post contains exactly the pointers that I need; many thanks, @CodeGnome! – sdaau Apr 22 '13 at 21:43
  • I don't know if Bash has changed in this area since this answer was posted, but prefixed variable assignments **do** work with builtins now. For example, `FOO=foo eval 'echo $FOO'` prints `foo` as expected. This means you can do things like `IFS="..." read ...`. – Will Vousden Aug 19 '16 at 12:47
  • I think what's happening is that Bash actually modifies its own environment temporarily, and restores it once the command has completed, which can have weird side-effects. – Will Vousden Aug 19 '16 at 12:50
  • So essentially the issue is that, as the Unix Haters' Handbook pointed out decades ago, expansion is fundamentally broken in *nix? – Mason Wheeler Nov 05 '21 at 16:48
  • 1
    @MasonWheeler, I don't agree with "fundamentally broken". There's a specification, and people who understand the specification can write code that works in its context. Is it a spec that anyone would write today, in presence of hindsight? ${deity}, no -- but "badly designed" and "broken" are two different things. I'd call the spec _broken_ if there were reasonable constructs that were literally impossible to represent, but that's not the case -- correctly representing reasonably constructs is often difficult or unwieldy, but not impossible, at least in presence of widely-accepted extensions. – Charles Duffy Apr 05 '22 at 15:13
  • ("widely-accepted extensions" -- meaning arrays, support for reading NUL-delimited streams, facilities for generating `eval`-safe escaping, and the like. BTW, read my "literally impossible to represent" above to mean "literally impossible to represent _in correct, robust code_" -- I don't count unsafe hacks with `eval` to constitute correct representations, f/e). – Charles Duffy Apr 05 '22 at 15:15
  • @CharlesDuffy "I'd call the spec broken if there were reasonable constructs that were literally impossible to represent, but that's not the case." The obvious example of how insanely broken *nix expansion has always been is "rename `*.x` to `*.y`". This is a trivial one-liner in DOS; how many knots do you have to tie yourself in to get it to work in a *nix shell? – Mason Wheeler Apr 05 '22 at 15:30
  • @MasonWheeler If you want to know how to do it in pure Bash, **ask your question as a question and not as a comment.** It can be done in almost any modern version of Bash. Most distros also provide a useful *rename* utility (generally based on sed-like expressions, with or without PCRE) that make this trivial. *NB: Comments are not for extended discssion. You should either open a new question, or move this to chat before it gets flagged.* – Todd A. Jacobs Apr 05 '22 at 15:33
  • @MasonWheeler, it's a trivial one-liner in DOS because DOS leaves glob expansion to the program being run, but I'd argue that that's an even worse design decision than what UNIX does! UNIX globbing may be unexpected at times, but at least it's _consistent_. When you let every program override the standard-C-library builtins to decide how its command line is going to be parsed (not just for globs, but also for string unescaping and the like), there's no ability to learn a set of rules that apply everywhere 100% of the time _because no such rules exist_. – Charles Duffy Apr 05 '22 at 15:36
  • @MasonWheeler, ...btw, I go into detail on why behavior is what it is and how to work around it in [linux wildcard usage in `cp` and `mv`](https://stackoverflow.com/questions/35708506/linux-wildcard-usage-in-cp-and-mv/35708633#35708633). – Charles Duffy Apr 05 '22 at 15:46
  • @CharlesDuffy Yes, DOS leaves glob expansion to the program being run rather than apply a "consistent" rule, because *the precise semantic meaning of the glob is not always "consistent,"* as my example demonstrates. Forcing a one-size-fits-all standard on everything rather than allowing programs to do what makes sense for them individually screws up all the use cases that don't fit the one blessed size. – Mason Wheeler Apr 05 '22 at 15:47
24

To achieve what you want, use

( SOMEVAR=BBB; echo zzz $SOMEVAR zzz )

Reason:

  • You must separate the assignment by semicolon or new line from the next command, otherwise it is not executed before parameter expansion happens for the next command (echo).

  • You need to make the assignment inside a subshell environment, to make sure it does not persist beyond the current line.

This solution is shorter, neater and more efficient than some of the others suggested, in particular it does not create a new process.

Markus Kuhn
  • 983
  • 9
  • 10
  • 3
    For future googlers who wind up here: This is probably the best answer to this question. To complicate it further, if you need the assignment to be available in the environment of the command, you need to export it. The subshell still prevents the assignment from persisting. `(export SOMEVAR=BBB; python -c "from os import getenv; print getenv('SOMEVAR')")` – eaj Feb 26 '16 at 13:33
  • 1
    @eaj To export a shell variable to one single external program call, as in your example, just use `SOMEVAR=BBB python -c "from os import getenv; print getenv('SOMEVAR')"` – Markus Kuhn Dec 07 '19 at 13:37
  • @eaj, ...to expand a bit -- to make your proposed version as efficient as what's given in Markus's comment, you'd need to add an `exec` before the `python` to ensure that the subshell gets consumed, instead of `fork()`ing off Python as a subprocess of the subshell the parens create. – Charles Duffy Apr 05 '22 at 14:23
15

Let's look to the POSIX specification to understand why this behaves as it does, not just in bash but in any compliant shell:


2.10.2, Shell Grammar Rules

From rule 7(b), covering cases where an assignment precedes a simple command:

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.)

[...]

Assignment to the NAME shall occur as specified in Simple Commands.

Thus, parsing this assignment is required for a POSIX-compliant shell.


2.9.1, Simple Commands

  1. Redirections shall be performed as described in Redirection.

  2. Each variable assignment shall be expanded for tilde expansion, parameter expansion, command substitution, arithmetic expansion, and quote removal prior to assigning the value.

[...]

If no command name results, variable assignments shall affect the current execution environment. Otherwise, the variable assignments shall be exported for the execution environment of the command and shall not affect the current execution environment (except for special built-ins). 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.

Thus: An assignment given in part of the prefix to a simple command must be exported, and must not impact the "current shell environment", unless the command being invoked is a special built-in. Moreover, these steps shall follow redirections, which by nature must occur late in the command invocation process.


2.12, Shell Execution Environment

Utilities other than the special built-ins (see Special Built-In Utilities) shall be invoked in a separate environment that consists of the following. The initial value of these objects shall be the same as that for the parent shell, except as noted below.

[...]

Variables with the export attribute, along with those explicitly exported for the duration of the command, shall be passed to the utility environment variables


Thus: These variables are expanded by the subshell after fork and before exec'ing the command being invoked, and must -- by specification -- impact the child's environment alone.


Now, for some different behavior:

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.


It's worth noting, by the way, that the syntax you're asking about is for assignment within a simple command, as opposed to assignment within a subshell. You can control assignment in a subshell involved in a pipeline like so:

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

...which puts the assignment into the subshell running the first component of the pipeline (if your shell is indeed running that component in a subshell, which is undefined behavior inasmuch as POSIX is concerned; from the spec: "as an extension, however, any or all commands in a pipeline may be executed in the current environment").

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
11

The reason is that this sets an environment variable for one line. But, echo does not do the expansion, bash does. Hence, your variable is actually expanded before the command is executed, even though SOME_VAR is BBB in the context of the echo command.

To see the effect, you can do something like:

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

Here the variable is not expanded until the child process executes, so you see the updated value. if you check SOME_VARIABLE again in the parent shell, it's still AAA, as expected.

FatalError
  • 52,695
  • 14
  • 99
  • 116
6

Put simply, the $SOMEVAR is evaluated before the command is called while prepending SOMEVAR=BBB in front of the command modifies the environment of the command you're running.

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.

oguz ismail
  • 1
  • 16
  • 47
  • 69
Adrien Nader
  • 113
  • 1
  • 6
3
SOMEVAR=BBB; echo zzz $SOMEVAR zzz

Use a ; to separate statements that are on the same line.

Kyros
  • 512
  • 2
  • 5
  • 1
    That works, but is not quite the point. The idea is to set the environment for just the one command, not permanently as your solution does. – Jonathan Leffler Jun 07 '12 at 19:32
  • Thanks for that @Kyros; don't know how come I missed that by now :) Still wandering how `LD_PRELOAD` and such can work in front of an executable without a semicolon, though... Many thanks again - cheers! – sdaau Jun 07 '12 at 19:40
  • @JonathanLeffler - indeed, that was the idea; I didn't realize the semicolon makes the change permanent - thanks for noting that! – sdaau Jun 07 '12 at 19:41
  • +1, though the anwer isn't quite the point but it inspires and helps understand `;`. `;`, *"Command separator — used to separate multiple commands that are on the same line."* – Rick May 29 '22 at 15:38
1
SOMEVAR=BBB echo zzz $SOMEVAR zzz

will add SOMEVAR=BBB into the environment variables and then execute echo zzz $SOMEVAR zzz. The $SOMEVAR refers to the shell variable SOMEVAR which you have set to AAA beforehand.

Adding the semi colon SOMEVAR=BBB; echo zzz $SOMEVAR zzz sets the shell variable to BBB and then executes the command after the semi-colon, i.e echo zzz $SOMEVAR zzz and produces zzz BBB zzz.

Try this command:

SOMEVAR=BBB env | less

and look at the environment.

oguz ismail
  • 1
  • 16
  • 47
  • 69
X Tian
  • 766
  • 7
  • 13
0

Here's one alternative:

SOMEVAR=BBB && echo zzz $SOMEVAR zzz
Will
  • 24,082
  • 14
  • 97
  • 108
brian
  • 17
  • 2
  • 1
    Whether you use `&&` or `;` to separate the commands, the assignment persists, which is not OP's desired behavior. Markus Kuhn has the correct version of this answer. – eaj Feb 26 '16 at 13:14