Variable assignments affect present running shell
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!
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.
- 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
: