368

If I run

export TEST=foo
echo $TEST

It outputs foo.

If I run

TEST=foo echo $TEST

It does not. How can I get this functionality without using export or a script?

5 Answers5

427

This is because the shell expands the variable in the command line before it actually runs the command and at that time the variable doesn't exist. If you use

TEST=foo; echo $TEST

it will work.

export will make the variable appear in the environment of subsequently executed commands (for on how this works in bash see help export). If you only need the variable to appear in the environment of one command, use what you have tried, i.e.:

TEST=foo your-application

The shell syntax describes this as being functionally equivalent to:

export TEST=foo
your-application
unset TEST

See the specification for details.

Interesting part is, that the export command switches the export flag for the variable name. Thus if you do:

unset TEST
export TEST
TEST="foo"

TEST will be exported even though it was not defined at the time when it was exported. However further unset should remove the export attribute from it.

peterph
  • 30,838
  • 2
    What about /usr/bin/env? that doesn't work either, do you know why? – Benubird Jul 09 '14 at 16:43
  • 8
    The same reason - the shell expands the $TEST before the command line is executed. Once the echo is running (also note that echo will usually translate to the shell built-in command and not to /bin/echo) it sees the variable set in its environment. However, echo $TEST doesn't tell echo to output the contents of variable TEST from its environment. It tells the shell to run echo with argument being whatever currently is in the variable called TEST - and those are two very different things. – peterph Jul 09 '14 at 17:41
  • @peterph If shell expands the variable in the command line before it actually runs the command then why it doesn't expand it in var=value sh -c 'echo "$var"'? – haccks Mar 12 '18 at 21:56
  • 1
    As I explained to you here: it’s because variables are expanded inside double quotes (e.g., "… $var …") but not inside single quotes (e.g., '… $var …').  Since echo "$var" is inside single quotes, that entire string gets passed to the new (sh -c) shell without being interpreted by the outer, interactive shell. … (Cont’d) – G-Man Says 'Reinstate Monica' Mar 14 '18 at 20:21
  • (Cont’d) …  As igal said yesterday, there are two rounds of parsing — although I believe that igal misread your question.  There are not two rounds of parsing apart from the parsing that is done by the parent shell.  There are two rounds of parsing — one done by the outer, interactive (parent) shell,  and one by the new (sh -c) child shell. – G-Man Says 'Reinstate Monica' Mar 14 '18 at 20:21
  • @G-Man; I was almost convinced by his answer but then this came to my mind. But what I guess is that the second round of parsing is not done by a subshell, unlike it is done by a child process which is not a subhsell (like sh -c) . – haccks Mar 14 '18 at 21:12
  • If it's a one time variable that you don't what to be left behind, then unset it after using it. Ex. TEST=foo; echo $TEST; unser TEST – asiby May 29 '19 at 15:06
  • why TEST in TEST=foo your-application becomes env variable for your-application? – Muhammad Umer Jan 18 '21 at 04:21
  • 1
    @MuhammadUmer because it is defined that way - check the link I've added to the answer. – peterph Jan 23 '21 at 18:56
  • If TEST is not defined, will export TEST define the variable TEST as empty value? – Idonknow Mar 06 '21 at 09:38
  • @Idonknow no, but export TEST= will; also see updated answer. And by the way, why not trying it yourself? – peterph Mar 11 '21 at 00:01
  • @peterph Do you know the windows alternative for the same? – rbansal Mar 15 '21 at 17:50
  • @rbansal sorry, wrong site. :) You may want to ask that on SU. I guess you will also need to elaborate on whether you are talking cmd.exe (the command.com descendant) or the PowersHell. (pun intended) – peterph Apr 10 '21 at 22:09
  • 1
    using two commands would work, but the reason why one simple command does not output foo IMO explained incorrectly, another answer is much better: https://unix.stackexchange.com/a/56454/266260: echo is usually a built-in and is not executed, var=value sh -c 'echo "$var"' works as one would expect. – Alex Martian Jan 21 '22 at 03:41
  • What do you use to separate multiple env vars? – Qian Chen Jan 10 '23 at 19:03
87

I suspect you want to have shell variables to have a limited scope, rather than environment variables. Environment variables are a list of strings passed to commands when they are executed.

In

var=value echo whatever

You're passing the var=value string to the environment that echo receives. However, echo doesn't do anything with its environment list¹ and anyway in most shells, echo is built in and therefore not executed.

If you had written

var=value sh -c 'echo "$var"'

That would have been another matter. Here, we're passing var=value to the sh command, and sh does happen to use its environment. Shells convert each of the variables they receive from their environment to a shell variable, so the var environment variable sh receives will be converted to a $var variable, and when it expands it in that echo command line, that will become echo value. Because the environment is by default inherited, echo will also receive var=value in its environment (or would if it were executed), but again, echo doesn't care about the environment.

Now, if as I suspect, what you want is to limit the scope of shell variables, there are several possible approaches.

Portably (Bourne and POSIX):

(var=value; echo "1: $var"); echo "2: $var"

The (...) above starts a sub-shell (a new shell process in most shells), so any variable declared there will only affect that sub-shell, so I'd expect the code above to output "1: value" and "2: " or "2: whatever-var-was-set-to-before".

With most Bourne-like shells (see List of shells that support `local` keyword for defining local variables), you can use functions and the "local" builtin:

f() {
  local var
  var=value
  echo "1: $var"
}
f
echo "2: $var"

With zsh, you can use anonymous functions which like normal functions can have a local scope:

(){ local var=value; echo "1: $var"; }; echo "2: $var"

or:

function { local var=value; echo "1: $var"; }; echo "2: $var"

With bash and zsh (but not ash, pdksh or AT&T ksh), this trick also works:

var=value eval 'echo "1: $var"'; echo "2: $var"

A variant that works in a few more shells (dash, mksh, yash) but not zsh (unless in sh/ksh emulation):

var=value command eval 'echo "1: $var"'; echo "2: $var"

(using command in front of a special builtin (here eval) in POSIX shells removes their specialness (here that variables assignments in front of them remain in effect after they have returned))

With the fish shell, you can make variables local to a begin..end block:

begin
  set -l var value
  echo 1: $var
end
echo 2: $var

With mksh (and soon zsh), you can abuse the ${|cmd} construct which can also have a local scope making sure it expands to nothing by making sure you don't set $REPLY within:

${|local var=value; echo "$var"}; echo "$var"

¹ Stricktly speaking, that's not completely true. Several implementations will care about the localisation environment variables (LANG, LOCPATH, LC_*...), the GNU implementation cares about the POSIXLY_CORRECT environment variable (compare env echo --version with env POSIXLY_CORRECT=1 echo --version on a GNU system).

  • 2
    For my purposes, this is the correct solution. I don't want the variable 'var' to pollute the environment as it does in the accepted answer. – kronenpj Sep 13 '17 at 15:12
  • If the simple subshell approach is "Portable (Bourne and POSIX)", why bother with all the more verbose answers for specific shells? – Noumenon Aug 12 '22 at 08:24
  • 1
    @Noumenon, because that approach has a side effect (runs in a subshell) that makes it unapplicable in man cases. – Stéphane Chazelas Aug 12 '22 at 08:26
25

You're doing it correctly, but the bash syntax is easy to misinterpret: you could think that echo $TEST causes echo to fetch TEST env var then print it, it does not. So given

export TEST=123

then

TEST=456 echo $TEST

involves the following sequence:

  1. The shell parses the whole command line and executes all variable substitutions, so the command line becomes

    TEST=456 echo 123
    
  2. It creates the temp vars set before the command, so it saves the current value of TEST and overwrites it with 456; the command line is now

    echo 123
    
  3. It executes the remaining command, which in this case prints 123 to stdout (so shell command that remains didn't even use the temp value of TEST)

  4. It restores the value of TEST

Use printenv instead, as it does not involve variable substitution:

>> export TEST=123
>> printenv TEST
123
>> TEST=456 printenv TEST
456
>> printenv TEST && TEST=456 printenv TEST && TEST=789 printenv TEST && printenv TEST
123
456
789
123
>>
Oliver
  • 431
  • 1
    +1 I found printenv helpful for testing / proof of concept (behaves like a script does, as opposed to echo) – Daryn Jan 07 '19 at 08:28
10

You can get this working by using:

TEST=foo && echo $TEST
2

As you've already discovered, in Bash, TEST=456 echo $TEST doesn't work like you expected, because you are accessing the variable on the same line that's currently being interpreted by the shell. But rest assured, the variable has been set for the duration of the current command. To verify that, you could do something like this:

show () { echo "${!1}" ;}
TEST=456 show TEST
Pourko
  • 1,844