8

While writing some code I found out that this line:

$ TZ="America/Los_Angeles"       date; echo "$TZ"
Thu Dec 24 14:39:15 PST 2015

Correctly gives the actual time at "Los Angeles" and that the value of the variable TZ is not retained. All as should be expected.

However, with this line, which I used to get some formats to date expanded, and which essentially executes the same thing, retains the value of TZ:

TZ="America/Los_Angeles" eval  date; echo "$TZ"
Thu Dec 24 14:41:34 PST 2015
America/Los_Angeles

After several more tests, I found out that this happens only in some shells. It happens in dash, ksh but not in bash or zsh.

Q's

The question(s) are:

  • Why is the value of TZ being retained in the present shell?
  • How could that be avoided/controlled (if possible)?

Additional.

I ran tests in several shells with this two lines:

myTZ="America/Los_Angeles"
unset TZ; { TZ="$myTZ"      date; } >/dev/null; echo -n "  direct $TZ"
unset TZ; { TZ="$myTZ" eval date; } >/dev/null; echo    "  evaled $TZ"

And this results:

/bin/ash        :   direct   evaled America/Los_Angeles
/bin/dash       :   direct   evaled America/Los_Angeles
/bin/sh         :   direct   evaled America/Los_Angeles
/bin/bash       :   direct   evaled
/bin/ksh93      :   direct   evaled America/Los_Angeles
/bin/lksh       :   direct   evaled America/Los_Angeles
/bin/mksh       :   direct   evaled America/Los_Angeles
/bin/zsh        :   direct   evaled
/bin/zsh4       :   direct   evaled 

TZ value affects the running shell in all shells except bash and zsh.

6

As you've found, that's spec'd behavior. But it also makes sense.

The value is retained in the shell's environment for the same reason the value of other environment variables are retained by other commands when you prefix definitions to their command-lines - you're setting the variables in their environment.

The special builtins are generally the most intrinsic variety in any shell - eval is essentially an accessible name for the shell's parser, set tracks and configures shell options and shell parameters, return/break/continue trigger loop control flow, trap handles signals, exec opens/closes files. These are all fundamental utilities - and are typically implemented with barely-there wrappers over the meat and potatoes of your shell.

Executing most commands involves some layered environment - a subshell environment (which needn't necessarily be a separate process) - which you don't get when calling the special builtins. So when you set environment for one of these commands you set environment for your shell. Because they basically represent your shell.

But they're not the only commands that retain environment that way - functions do the same as well. And errors behave differently for special built-ins - try cat <doesntexist and then try exec <doesntexist or even just : <doesntexist and while the cat command will complain, the exec or : will kill a POSIX shell. The same is true of expansion errors on their command line. They're the main loop, basically.

These commands don't have to retain environment - some shells wrap their internals up more tightly than others, expose less of the core functionality and add more buffer between the programmer and the interface. These same shells might also tend to be a bit slower than others. Definitely they require a lot of non-standard adjustments to make them to conform to spec. And anyway, it's not as if this is a bad thing:

fn(){ bad_command || return=$some_value return; }

That stuff is easy. How else would you preserve the return of bad_command so simply without having to set a bunch of extra environment and yet still do assignments conditionally?

arg=$1 shift; x=$y unset y

That kinda stuff works too. In place swaps are more simple.

IFS=+  set -- "$IFS" x y z
x="$*" IFS=$1 shift
echo "${x#"$IFS"}" "$*"

+x+y+z x y z

...or...

expand(){
    PS4="$*" set -x "" "$PS4" 
    { $1; }  2>&1
    PS4=$2   set +x
}   2>/dev/null

x='echo kill my computer; $y'
y='haha! just kidding!' expand "${x##*[\`\(]*}"

...is another one I like to use...

echo kill my computer; haha! just kidding!
  • @BinaryZebra - but the point is they dont work differently - when you set variables for some other command they persist in that other executable's environment. when you set variables in your shell's environment they also persist. – mikeserv Dec 24 '15 at 23:24
3

It turns out that there is a very specific reason for this behavior.
The description of what happens is a bit longer.

Only assignments.

A command line made (only) of assignments will set the variables for this shell.

$ unset a b c d
$ a=b c=d
$ echo "<$a::$c>"
<b::d>

The value of vars assigned will be retained.

External command.

Assignments before an external command set variables for that shell only:

$ unset a b c d
$ a=b c=d bash -c 'echo "one:|$c|"'; echo "two:<$c>"
one:|d|
two:<>

And I mean "external" as any command that has to be searched in PATH.

This also apply to normal built-ins (like cd, for example):

$ unset a b c d; a=b c=d cd . ; echo "<$a::$c>"
<::>

Up to here all is as is usually expected.

Special Built-Ins.

But for special built-ins, POSIX requires that the values are set for this shell.

  1. Variable assignments specified with special built-in utilities remain in effect after the built-in completes.
$ sh -c 'unset a b c d; a=b c=d export f=g ; echo "<$a::$c::$f>"'
<b::d::g>

I am using a call to sh assuming that sh is a POSIX-compliant shell.

This is not something that is usually used.

This means that assignments place in front of any of this list of special built-ins shall retain the assigned values in the present running shell:

break : continue . eval exec exit export 
readonly return set shift times trap unset

This will happen if a shell works as per POSIX spec.

Conclusion:

It is possible to set variables for only one command, any command, by making sure the command is not a special built-in. The command command is a regular builtin. It only tells the shell to use a command, not a function. This line works in all shells (except ksh93):

$ unset a b c d; a=b c=d command eval 'f=g'; echo "<$a::$c::$f>"
<::::g>

In such case vars a and b are set for the environment of the command command, and discarded after that.

Instead, this will retain assigned values (except bash and zsh):

$ unset a b c d; a=b c=d eval 'f=g'; echo "<$a::$c::$f>"
<b::d::g>

Note that the assignment after eval is single quoted to protect it from unwanted expansions.

So: To place variables in the command environment use command eval:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy